fix: web/ quick wins — CSS cleanup, ARIA tabs, and file header comments
CSS: - Remove duplicate li::before block in app-search.css - Fix typo: search-reuslt → search-result in app-search.css - Remove commented-out CSS rules in app-flash.css - Add descriptive header comment to all 27 CSS component files JS: - Complete WAI-ARIA Tabs pattern: generate IDs, add aria-controls on tab buttons and aria-labelledby on tab panels (app-tabs.js) - Add JSDoc header comments to ~33 JS files (core + components) - Add explanatory block comment to app-boot.js (why classic IIFE) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Active filter chips — removable pill indicators showing applied grid filters. */
|
||||
.app-active-filters {
|
||||
margin: 0 0 0.875rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Badge pills — inline status/role/tag indicators with color variants. */
|
||||
.badge[role="button"],
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Blockquote styling — left-bordered quote blocks with accent color. */
|
||||
blockquote {
|
||||
--blockquote-bg: var(--app-blockquote-background-color);
|
||||
--blockquote-color: inherit;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Branding — tenant logo display in sidebar and login. */
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Breadcrumb navigation — separator-delimited path links. */
|
||||
.app-breadcrumb {
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Action button variants — success and danger colors with outline and focus states. */
|
||||
.app-action-success:is(button, [type="submit"], [type="button"], [role="button"]) {
|
||||
--app-background-color: var(--app-action-success-background);
|
||||
--app-border-color: var(--app-action-success-border);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Confirm dialog — modal overlay for destructive action confirmation with danger/warning variants. */
|
||||
dialog[data-app-confirm-dialog] > article.app-confirm-dialog {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Dashboard title bar — welcome heading with tenant context. */
|
||||
.app-dashboard-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Detail cards — bordered content sections within detail pages. */
|
||||
details.app-details-card {
|
||||
margin-bottom: calc(var(--app-spacing) * 0.75);
|
||||
border: 1px solid
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Detail page title bar — sticky save/close actions with unsaved-changes indicator. */
|
||||
.app-details-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Validation summary — error list displayed at top of detail forms. */
|
||||
.app-details-errors {
|
||||
padding-block-start: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Detail page layout — aside panel, card sections, sticky titlebar integration. */
|
||||
.app-details-container aside {
|
||||
padding-top: var(--app-spacing);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Empty state placeholders — dashed border cards with icon, message, and optional action. */
|
||||
.app-empty-state {
|
||||
border: 1px dashed var(--app-empty-border-color);
|
||||
border-radius: var(--app-border-radius);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Filter drawer — slide-in overlay panel with apply/reset/close actions and focus trap. */
|
||||
.app-visually-hidden {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
@layer components {
|
||||
/* Flash notices — success/warning/error/info feedback banners with auto-dismiss timer. */
|
||||
.notice {
|
||||
--app-notice-border-color: var(--app-muted-border-color);
|
||||
--app-notice-background-color: var(--app-card-background-color);
|
||||
--app-notice-accent-color: var(--app-color);
|
||||
/* border: var(--app-border-width) solid var(--app-notice-border-color); */
|
||||
/* border-radius: var(--app-border-radius); */
|
||||
background: var(--app-notice-background-color);
|
||||
padding: calc(var(--app-spacing) * .5) calc(var(--app-spacing) * 1);
|
||||
padding-right: calc(var(--app-spacing) * .5);
|
||||
/* margin-bottom: var(--app-spacing); */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -113,10 +111,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* .flash-stack:has(.notice) {
|
||||
margin-top: var(--app-spacing);
|
||||
} */
|
||||
|
||||
.app-main-content .flash-stack {
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Form info tile — inline help/context card within form sections. */
|
||||
.app-form-info-tiles-title {
|
||||
display: block;
|
||||
margin-bottom: calc(var(--app-spacing) * 0.55);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Form element enhancements — validation hints, color inputs, checkbox/radio labels, disabled states. */
|
||||
.form-hint {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* List table tweaks — minor overrides for Grid.js table rendering. */
|
||||
.app-list-table table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* List page tab bar — horizontal filter tabs above data grids. */
|
||||
.app-list-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* List page title bar — heading, action buttons, and breadcrumb row. */
|
||||
.app-list-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* List toolbar — page-size selector, filter toggle, and search controls above grids. */
|
||||
.app-list-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@layer components {
|
||||
|
||||
/* Page copy block — rendered markdown content for CMS pages. */
|
||||
|
||||
.app-page-copy .app-field {
|
||||
min-width: 200px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Page editor — Editor.js integration styles for CMS content editing. */
|
||||
.app-page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Global sidebar search — input, result list, preview rows, and search result cards. */
|
||||
.app-search {
|
||||
position: relative;
|
||||
padding: 0 var(--app-spacing);
|
||||
@@ -83,18 +84,6 @@ ul.app-search-preview {
|
||||
.search-details-hidden ul.app-search-preview {
|
||||
display: none;
|
||||
}
|
||||
ul.app-search-preview li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 17px;
|
||||
bottom: 11px;
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-left: 1px solid var(--app-border);
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
ul.app-search-preview li {
|
||||
position: relative;
|
||||
@@ -146,7 +135,7 @@ ul.app-search-preview:has(li) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-reuslt-title-container {
|
||||
.search-result-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Tab navigation — scrollable strip with chevron overflow, active/invalid states, no-JS fallback. */
|
||||
.app-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* Info tiles — key-value display cards used in detail page sidebars. */
|
||||
.app-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer components {
|
||||
/* CSS-only tooltips — positioned via data-tooltip-pos with arrow indicator. */
|
||||
[data-tooltip] {
|
||||
--app-card-background-color: inherit;
|
||||
position: relative;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* Render-blocking boot script — runs synchronously before first paint.
|
||||
*
|
||||
* Intentionally a classic IIFE (not an ES module) so the browser executes it
|
||||
* immediately while parsing <head>. This prevents layout flicker by applying
|
||||
* persisted UI state (sidebar collapsed, contrast mode) before any content
|
||||
* is rendered. Uses `var` for broadest compatibility in the sync path.
|
||||
*/
|
||||
(function () {
|
||||
var root = document.documentElement;
|
||||
root.classList.remove('no-js');
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Renders removable pill chips for currently active grid filters.
|
||||
*/
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Shows/hides bulk action buttons based on Grid.js row selection state.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function bindBulkVisibility({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Toggles color input between custom value and default/inherited color.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const toggles = document.querySelectorAll('[data-color-default-toggle]');
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Declarative confirm-before-action — intercepts clicks and submits on [data-confirm-message] elements.
|
||||
*/
|
||||
import { confirmDialog, requestSubmitWithFallback } from '../core/app-confirm-dialog.js';
|
||||
|
||||
const CONFIRM_ATTR = 'data-confirm-message';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* High-contrast mode toggle with localStorage persistence.
|
||||
*/
|
||||
import { requireEl } from '../core/app-dom.js';
|
||||
|
||||
const STORAGE_KEY = 'app-contrast';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Click-to-copy badge — copies text to clipboard with visual feedback.
|
||||
*/
|
||||
const COPY_SELECTOR = '.badge[data-copy="true"]';
|
||||
const COPIED_CLASS = 'is-copied';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Shows/hides custom field option list based on field type selection.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const TYPES_WITH_OPTIONS = new Set(['select', 'multiselect']);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Initializes detail page open-state persistence and action policy.
|
||||
*/
|
||||
import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';
|
||||
|
||||
const controllerRegistry = new WeakMap();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Slide-in filter drawer with focus trap, scroll lock, and apply/reset/discard lifecycle.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initFilterDrawer(options = {}) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Auto-dismisses flash notices after their data-flash-timeout expires.
|
||||
*/
|
||||
export function initFlashAutoDismiss(options = {}) {
|
||||
const {
|
||||
selector = '.flash-stack .notice[data-flash-timeout]',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Triggers FSLightbox refresh after dynamic content changes.
|
||||
*/
|
||||
const refreshFsLightbox = () => {
|
||||
if (typeof window.requestFsLightboxRefresh === 'function') {
|
||||
return window.requestFsLightboxRefresh();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Global search sidebar — debounced fetch with live result counts and Ctrl+K shortcut.
|
||||
*/
|
||||
import { requireEl, optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const input = requireEl('#side-search', { module: 'global-search' });
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Cascading multi-select — filters child options based on parent selection.
|
||||
*/
|
||||
import { getMultiSelectInstance } from './app-multiselect-init.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Initializes MultiSelect.js instances from select[multiple] elements.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
let multiSelectLoader = null;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Browser history integration — handles back/forward navigation state.
|
||||
*/
|
||||
const updateHistoryButtons = () => {
|
||||
const back = document.querySelector('#global-back');
|
||||
const forward = document.querySelector('#global-forward');
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Editor.js integration — initializes block editor for CMS page content.
|
||||
*/
|
||||
import EditorJS from '../../vendor/editorjs/editorjs.mjs';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Real-time password validation hints — checks length, uppercase, digit, special char.
|
||||
*/
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const rules = {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Show/hide password toggle button injected next to password inputs.
|
||||
*/
|
||||
export function initPasswordToggles() {
|
||||
const inputs = document.querySelectorAll('input[type="password"]');
|
||||
if (!inputs.length) {return;}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Tracks settings page interactions for telemetry.
|
||||
*/
|
||||
const initTelemetrySettings = () => {
|
||||
const toggle = document.querySelector('[data-telemetry-enabled-toggle]');
|
||||
const samplingFieldset = document.querySelector('[data-telemetry-sampling-fieldset]');
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Wires up the standard detail page factory with titlebar and form state.
|
||||
*/
|
||||
import { autoInitStandardDetailPages } from '../core/app-detail-page-factory.js';
|
||||
|
||||
export function initStandardDetailPages(root = document) {
|
||||
|
||||
@@ -90,13 +90,18 @@ export function initTabs(root = document) {
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// ARIA: role="tablist" on the nav, not the outer container
|
||||
// ARIA: role, aria-controls, aria-labelledby (WAI-ARIA Tabs pattern)
|
||||
// ----------------------------------------------------------------
|
||||
const nav = container.querySelector('.app-tabs-nav');
|
||||
const tabIdPrefix = container.id || `tabs-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
// Initialize tabs
|
||||
tabs.forEach(tab => {
|
||||
const tabName = tab.dataset.tab;
|
||||
const tabId = `${tabIdPrefix}-tab-${tabName}`;
|
||||
const panelId = `${tabIdPrefix}-panel-${tabName}`;
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.id = tabId;
|
||||
tab.setAttribute('aria-controls', panelId);
|
||||
tab.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
activateTab(tab.dataset.tab);
|
||||
@@ -106,7 +111,12 @@ export function initTabs(root = document) {
|
||||
});
|
||||
|
||||
panels.forEach(panel => {
|
||||
const panelName = panel.dataset.tabPanel;
|
||||
const panelId = `${tabIdPrefix}-panel-${panelName}`;
|
||||
const tabId = `${tabIdPrefix}-tab-${panelName}`;
|
||||
panel.setAttribute('role', 'tabpanel');
|
||||
panel.id = panelId;
|
||||
panel.setAttribute('aria-labelledby', tabId);
|
||||
});
|
||||
|
||||
if (nav) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Shows/hides SSO configuration fields based on tenant SSO toggle.
|
||||
*/
|
||||
const roots = document.querySelectorAll('[data-tenant-sso-root]');
|
||||
|
||||
const syncControlState = (container, show) => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Tenant context switcher dropdown with logo display.
|
||||
*/
|
||||
import { showAsyncFlash } from './app-async-flash.js';
|
||||
import { optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Theme switcher — light/dark toggle and menu with server-side persistence.
|
||||
*/
|
||||
import { optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const setTheme = (theme) => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Aside panel switcher — opens/closes named sidebar panels.
|
||||
*/
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Toggles visibility of the detail page aside column.
|
||||
*/
|
||||
import { optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initDetailsAsideToggle(options = {}) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Sidebar collapse/show toggle with localStorage persistence and Ctrl+B shortcut.
|
||||
*/
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initSidebarToggle(options = {}) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Promise-based confirm dialog controller with variant inference, focus trap, and native fallback.
|
||||
*/
|
||||
import { warnOnce } from './app-dom.js';
|
||||
|
||||
const VARIANTS = new Set(['default', 'warning', 'danger']);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Factory for standard detail pages — form dirty-tracking, unsaved indicator, validation routing, and keyboard save.
|
||||
*/
|
||||
import { initDetailActionPolicy } from './app-details-action-policy.js';
|
||||
import { confirmDialog } from './app-confirm-dialog.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Form submit policy for detail pages — confirm dialogs, double-submit prevention, and aria-busy.
|
||||
*/
|
||||
import { confirmDialog, requestSubmitWithFallback } from './app-confirm-dialog.js';
|
||||
|
||||
const escapeAttr = (value) => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Persists open/closed state of details elements to localStorage by key.
|
||||
*/
|
||||
const toElement = (value) => {
|
||||
if (value instanceof HTMLElement) {
|
||||
return value;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* DOM utility helpers — safe element lookup with missing-element warnings and telemetry.
|
||||
*/
|
||||
import { telemetry } from './app-telemetry.js';
|
||||
|
||||
const warned = new Set();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Reads JSON configuration embedded in the page via script#page-config-{id} elements.
|
||||
*/
|
||||
import { warnOnce } from './app-dom.js';
|
||||
|
||||
export function readPageConfig(configId, root = document) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Frontend event telemetry — captures UI events with deduplication, sampling, and rate limiting.
|
||||
*/
|
||||
const DEFAULT_SAMPLE_RATE = 0.2;
|
||||
const DEFAULT_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_MAX_EVENTS_PER_MINUTE = 20;
|
||||
|
||||
Reference in New Issue
Block a user