From c43da8921ec441be4146a7a13d0a9c2cb16dc023 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Tue, 19 May 2026 10:15:36 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(ui):=20polish=20=E2=80=94=20design=20t?= =?UTF-8?q?okens,=20focus-visible,=20a11y,=20Dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — Token foundation - Add BrandSpacing/BrandDensity/BrandMotion/BrandFocusRing/BrandElevation interfaces (src/brands/types.ts) - Emit new --mieweb-* CSS custom props for spacing (xs..2xl), density-scale, motion (fast/base/slow + 3 easings), focus-ring (width/offset/style), elevation (1..6 + inner) - Wire defaults into base.css :root + @theme so all 8 brands inherit automatically - BlueHive populated with full token values (src/brands/bluehive.{ts,css}) - Tailwind preset exposes shadow elevation-*, spacing brand-*, transition duration/timing, ring width/offset utilities - :focus-visible consumes brand focus-ring tokens - Compact density via [data-density=compact] (preferred) + legacy body.condensed alias - prefers-reduced-motion already honored Phase 2 — Component cleanup - Badge: hardcoded green/yellow/red → semantic success/warning/destructive scales (dark-mode aware) - HRISProviderSelector pending-sync alert: yellow-* → warning tokens + role=status - ProviderOverview skeleton: gray-200/gray-700 → bg-muted (theme-aware) - Global sweep: focus:ring → focus-visible:ring across all components (mouse focus no longer shows ring) - Table: for screen-reader column header semantics Phase 3 — Dashboard - New composable component (src/components/Dashboard/Dashboard.tsx) - Slots: Dashboard.Header / Title / Subtitle / Actions / Grid / Widget - Responsive 1 → 6 → 12 column grid (mobile / tablet / desktop) - Density + brand-spacing aware - Exported from src/index.ts - Removed stale .bak / .backup / .broken story files Phase 4 — Verification - New tests/visual/a11y.spec.ts with axe-core (WCAG 2.1 A+AA) - 14 core stories scanned, zero critical/serious violations - @axe-core/playwright added as devDependency Validation - pnpm typecheck PASS - pnpm lint PASS - pnpm test (vitest) 113/113 PASS - pnpm build (tsup ESM+CJS+DTS) PASS - pnpm test:visual: 20/20 baseline regressions PASS (within 5% tolerance) - pnpm test:visual: 14/14 a11y PASS --- .storybook/preview.tsx | 46 +- package.json | 7 +- pnpm-lock.yaml | 19 + src/brands/bluehive.css | 55 +- src/brands/bluehive.ts | 41 + src/brands/types.ts | 204 +++- src/components/AI/AIChatModal.tsx | 4 +- src/components/AI/MCPToolCall.tsx | 4 +- src/components/Address/Address.tsx | 2 +- src/components/AppHeader/AppHeader.tsx | 4 +- src/components/AuthDialog/AuthDialog.tsx | 20 +- src/components/Badge/Badge.tsx | 7 +- .../BookingDialog/BookingDialog.tsx | 19 +- src/components/CountBadge/CountBadge.tsx | 6 +- .../CountryCodeDropdown.tsx | 4 +- .../Dashboard/Dashboard.stories.tsx.backup | 972 ----------------- .../Dashboard/Dashboard.stories.tsx.bak | 972 ----------------- .../Dashboard/Dashboard.stories.tsx.broken | 978 ------------------ src/components/Dashboard/Dashboard.tsx | 181 ++++ src/components/Dashboard/index.ts | 5 + src/components/DateInput/DateInput.tsx | 6 +- .../DateRangePicker/DateRangePicker.tsx | 4 +- .../EmployerServiceModal.tsx | 4 +- .../HRISProviderSelector.tsx | 5 +- src/components/Input/Input.tsx | 4 +- .../InviteUserModal/InviteUserModal.tsx | 2 +- .../LanguageSelector/LanguageSelector.tsx | 4 +- src/components/Messaging/AttachmentPicker.tsx | 6 +- .../Messaging/ConversationHeader.tsx | 2 +- src/components/Messaging/MessageBubble.tsx | 6 +- src/components/Messaging/MessageComposer.tsx | 6 +- src/components/Messaging/MessageList.tsx | 4 +- src/components/Messaging/MessageThread.tsx | 2 +- .../OrderConfirmationWizard.tsx | 10 +- src/components/OrderList/OrderList.tsx | 2 +- .../PatientHeader/PatientHeader.tsx | 10 +- .../PaymentMethod/PaymentMethod.tsx | 4 +- src/components/PhoneInput/PhoneInput.tsx | 4 +- .../ProviderDetailHeader.tsx | 6 +- .../ProviderOverview/ProviderOverview.tsx | 9 +- .../ProviderSearchFilters.tsx | 12 +- .../ProviderSelector/ProviderSelector.tsx | 4 +- .../RecurringServiceCard.tsx | 6 +- .../RejectionModal/RejectionModal.tsx | 4 +- .../SchedulePicker/SchedulePicker.tsx | 8 +- src/components/Select/Select.tsx | 6 +- src/components/ServiceBadge/ServiceBadge.tsx | 16 +- .../ServiceGeneralSettings.tsx | 6 +- .../ServiceShippingSettings.tsx | 2 +- .../SetupServiceModal/SetupServiceModal.tsx | 4 +- src/components/Sidebar/Sidebar.tsx | 6 +- .../StepIndicator/StepIndicator.tsx | 10 +- src/components/Table/Table.tsx | 1 + src/components/Textarea/Textarea.tsx | 4 +- src/components/ThemeProvider/ThemeToggle.tsx | 2 +- src/components/Toast/Toast.tsx | 4 +- src/components/WebsiteInput/WebsiteInput.tsx | 4 +- src/index.ts | 1 + src/styles/base.css | 88 +- src/tailwind-preset.ts | 97 ++ tests/visual/a11y.spec.ts | 67 ++ tests/visual/components.spec.ts | 5 +- 62 files changed, 939 insertions(+), 3068 deletions(-) delete mode 100644 src/components/Dashboard/Dashboard.stories.tsx.backup delete mode 100644 src/components/Dashboard/Dashboard.stories.tsx.bak delete mode 100644 src/components/Dashboard/Dashboard.stories.tsx.broken create mode 100644 src/components/Dashboard/Dashboard.tsx create mode 100644 src/components/Dashboard/index.ts create mode 100644 tests/visual/a11y.spec.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index cb1e316f..36c4e492 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -12,6 +12,13 @@ import { miewebBrand } from '../src/brands/mieweb'; import { wagglelineBrand } from '../src/brands/waggleline'; import { webchartBrand } from '../src/brands/webchart'; import type { BrandConfig } from '../src/brands/types'; +import { + defaultDensity, + defaultElevation, + defaultFocusRing, + defaultMotion, + defaultSpacing, +} from '../src/brands/types'; // Map of available brands const brands: Record = { @@ -46,8 +53,10 @@ function applyGlobalTheme(globals: Record) { // Toggle condensed density class on body if (isCondensed) { document.body.classList.add('condensed'); + document.documentElement.setAttribute('data-density', 'compact'); } else { document.body.classList.remove('condensed'); + document.documentElement.setAttribute('data-density', 'comfortable'); } document.body.style.backgroundColor = semanticColors.background; @@ -88,9 +97,13 @@ try { // Function to apply brand CSS variables to document function applyBrandStyles(brand: BrandConfig, isDark: boolean) { - const root = document.documentElement; const colors = brand.colors; const semanticColors = isDark ? colors.dark : colors.light; + const spacing = brand.spacing ?? defaultSpacing; + const motion = brand.motion ?? defaultMotion; + const focusRing = brand.focusRing ?? defaultFocusRing; + const density = brand.density ?? defaultDensity; + const shadow = brand.boxShadow; // Remove any existing brand style tag const existingStyle = document.getElementById('mieweb-brand-styles'); @@ -138,9 +151,34 @@ function applyBrandStyles(brand: BrandConfig, isDark: boolean) { --mieweb-radius-xl: ${brand.borderRadius.xl} !important; --mieweb-radius-2xl: ${brand.borderRadius['2xl']} !important; --mieweb-radius-full: ${brand.borderRadius.full} !important; - --mieweb-shadow-card: ${brand.boxShadow.card} !important; - --mieweb-shadow-dropdown: ${brand.boxShadow.dropdown} !important; - --mieweb-shadow-modal: ${brand.boxShadow.modal} !important; + --mieweb-shadow-card: ${shadow.card} !important; + --mieweb-shadow-dropdown: ${shadow.dropdown} !important; + --mieweb-shadow-modal: ${shadow.modal} !important; + --mieweb-shadow-1: ${shadow[1] ?? defaultElevation[1]} !important; + --mieweb-shadow-2: ${shadow[2] ?? defaultElevation[2]} !important; + --mieweb-shadow-3: ${shadow[3] ?? defaultElevation[3]} !important; + --mieweb-shadow-4: ${shadow[4] ?? defaultElevation[4]} !important; + --mieweb-shadow-5: ${shadow[5] ?? defaultElevation[5]} !important; + --mieweb-shadow-6: ${shadow[6] ?? defaultElevation[6]} !important; + --mieweb-shadow-inner: ${shadow.inner ?? defaultElevation.inner} !important; + --mieweb-spacing-xs: ${spacing.xs} !important; + --mieweb-spacing-sm: ${spacing.sm} !important; + --mieweb-spacing-md: ${spacing.md} !important; + --mieweb-spacing-lg: ${spacing.lg} !important; + --mieweb-spacing-xl: ${spacing.xl} !important; + --mieweb-spacing-2xl: ${spacing['2xl']} !important; + --mieweb-duration-fast: ${motion.durations.fast} !important; + --mieweb-duration-base: ${motion.durations.base} !important; + --mieweb-duration-slow: ${motion.durations.slow} !important; + --mieweb-ease-standard: ${motion.easings.standard} !important; + --mieweb-ease-emphasized: ${motion.easings.emphasized} !important; + --mieweb-ease-decelerate: ${motion.easings.decelerate} !important; + --mieweb-focus-ring-width: ${focusRing.width} !important; + --mieweb-focus-ring-offset: ${focusRing.offset} !important; + --mieweb-focus-ring-style: ${focusRing.style ?? 'solid'} !important; + } + [data-density='compact'], body.condensed { + --mieweb-density-scale: ${density.compactScale} !important; } `; document.head.appendChild(styleTag); diff --git a/package.json b/package.json index 7b918a16..83995128 100644 --- a/package.json +++ b/package.json @@ -235,12 +235,13 @@ "tailwind-merge": "^2.6.1" }, "devDependencies": { - "@eslint/js": "^9.39.3", - "@esheet/builder": "link:./packages/esheet/packages/builder", - "@esheet/renderer": "link:./packages/esheet/packages/renderer", + "@axe-core/playwright": "^4.11.3", "@esheet/adapters": "link:./packages/esheet/packages/adapters", + "@esheet/builder": "link:./packages/esheet/packages/builder", "@esheet/core": "link:./packages/esheet/packages/core", "@esheet/fields": "link:./packages/esheet/packages/fields", + "@esheet/renderer": "link:./packages/esheet/packages/renderer", + "@eslint/js": "^9.39.3", "@ozwell/react": "file:./packages/ozwell/packages/react", "@playwright/test": "^1.58.2", "@storybook/addon-a11y": "^10.2.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7d9b389..bc0b20e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: specifier: ^2.6.1 version: 2.6.1 devDependencies: + '@axe-core/playwright': + specifier: ^4.11.3 + version: 4.11.3(playwright-core@1.58.2) '@esheet/adapters': specifier: link:packages/esheet/packages/adapters version: link:packages/esheet/packages/adapters @@ -203,6 +206,11 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@axe-core/playwright@4.11.3': + resolution: {integrity: sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1828,6 +1836,10 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} + axe-core@4.11.4: + resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + engines: {node: '>=4'} + axios@1.15.2: resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} @@ -4548,6 +4560,11 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@axe-core/playwright@4.11.3(playwright-core@1.58.2)': + dependencies: + axe-core: 4.11.4 + playwright-core: 1.58.2 + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -6178,6 +6195,8 @@ snapshots: axe-core@4.11.1: {} + axe-core@4.11.4: {} + axios@1.15.2: dependencies: follow-redirects: 1.16.0 diff --git a/src/brands/bluehive.css b/src/brands/bluehive.css index d35c444c..c68c6791 100644 --- a/src/brands/bluehive.css +++ b/src/brands/bluehive.css @@ -75,8 +75,59 @@ 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --mieweb-shadow-modal: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + /* Elevation Ramp */ + --mieweb-shadow-1: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --mieweb-shadow-2: + 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --mieweb-shadow-3: + 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --mieweb-shadow-4: + 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --mieweb-shadow-5: + 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --mieweb-shadow-6: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --mieweb-shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + + /* Spacing Scale (brand-owned; multiplied by --mieweb-density-scale) */ + --mieweb-spacing-xs: 0.25rem; + --mieweb-spacing-sm: 0.5rem; + --mieweb-spacing-md: 0.75rem; + --mieweb-spacing-lg: 1rem; + --mieweb-spacing-xl: 1.5rem; + --mieweb-spacing-2xl: 2rem; + + /* Density (1 = comfortable, 0.75 = compact) */ + --mieweb-density-scale: 1; + + /* Motion */ + --mieweb-duration-fast: 120ms; + --mieweb-duration-base: 200ms; + --mieweb-duration-slow: 320ms; + --mieweb-ease-standard: cubic-bezier(0.2, 0, 0, 1); + --mieweb-ease-emphasized: cubic-bezier(0.3, 0, 0, 1); + --mieweb-ease-decelerate: cubic-bezier(0, 0, 0.2, 1); + + /* Focus Ring */ + --mieweb-focus-ring-width: 2px; + --mieweb-focus-ring-offset: 2px; + --mieweb-focus-ring-style: solid; } +/* Compact density — preferred selector, plus legacy .condensed alias */ +[data-density='compact'], +body.condensed { + --mieweb-density-scale: 0.75; +} + +/* Honor reduced-motion preference at the token level */ +@media (prefers-reduced-motion: reduce) { + :root { + --mieweb-duration-fast: 0ms; + --mieweb-duration-base: 0ms; + --mieweb-duration-slow: 0ms; + } + /* Dark Mode */ [data-theme='dark'], .dark { @@ -152,5 +203,7 @@ kbd { } .focus-ring:focus-visible { - outline: 2px solid var(--mieweb-ring); + outline: var(--mieweb-focus-ring-width, 2px) var(--mieweb-focus-ring-style, solid) + var(--mieweb-ring); + outline-offset: var(--mieweb-focus-ring-offset, 2px); } diff --git a/src/brands/bluehive.ts b/src/brands/bluehive.ts index bf6ab86a..10d6c22b 100644 --- a/src/brands/bluehive.ts +++ b/src/brands/bluehive.ts @@ -93,6 +93,47 @@ export const bluehiveBrand: BrandConfig = { dropdown: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', modal: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + 1: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 2: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 3: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + 4: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + 5: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', + 6: '0 25px 50px -12px rgb(0 0 0 / 0.25)', + inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', + }, + + spacing: { + xs: '0.25rem', + sm: '0.5rem', + md: '0.75rem', + lg: '1rem', + xl: '1.5rem', + '2xl': '2rem', + }, + + density: { + default: 'comfortable', + compactScale: 0.75, + }, + + motion: { + durations: { + fast: '120ms', + base: '200ms', + slow: '320ms', + }, + easings: { + standard: 'cubic-bezier(0.2, 0, 0, 1)', + emphasized: 'cubic-bezier(0.3, 0, 0, 1)', + decelerate: 'cubic-bezier(0, 0, 0.2, 1)', + }, + }, + + focusRing: { + width: '2px', + offset: '2px', + color: 'ring', + style: 'solid', }, }; diff --git a/src/brands/types.ts b/src/brands/types.ts index e192884d..886fce46 100644 --- a/src/brands/types.ts +++ b/src/brands/types.ts @@ -114,12 +114,73 @@ export interface BrandBorderRadius { } /** - * Box shadow configuration. + * Box shadow configuration. `card`, `dropdown`, `modal` remain for back-compat; + * the numbered `1`–`6` levels form a consistent elevation ramp, with `inner` for + * inset surfaces. */ export interface BrandBoxShadow { card: string; dropdown: string; modal: string; + /** Elevation ramp — increasing depth from 1 (subtle) to 6 (pronounced) */ + 1?: string; + 2?: string; + 3?: string; + 4?: string; + 5?: string; + 6?: string; + /** Inset shadow */ + inner?: string; +} + +/** + * Spacing scale (rem). Used for component padding, gap, and rhythm. + * Brands may override to set their overall density/breathing room. + */ +export interface BrandSpacing { + xs: string; + sm: string; + md: string; + lg: string; + xl: string; + '2xl': string; +} + +/** + * Density configuration. `comfortable` is the default; `compact` multiplies + * spacing tokens by `compactScale` (e.g. 0.75) when `[data-density='compact']` + * (or the legacy `body.condensed` class) is active. + */ +export interface BrandDensity { + default: 'comfortable' | 'compact'; + compactScale: number; +} + +/** + * Motion tokens. Durations in ms, easings as CSS timing functions. + * Honors `prefers-reduced-motion` via the `usePrefersReducedMotion` hook. + */ +export interface BrandMotion { + durations: { + fast: string; + base: string; + slow: string; + }; + easings: { + standard: string; + emphasized: string; + decelerate: string; + }; +} + +/** + * Focus ring tokens. `color` is a semantic key (defaults to `ring`). + */ +export interface BrandFocusRing { + width: string; + offset: string; + color?: 'ring' | 'primary' | 'destructive'; + style?: 'solid' | 'dashed' | 'dotted'; } /** @@ -140,8 +201,67 @@ export interface BrandConfig { borderRadius: BrandBorderRadius; /** Box shadow definitions */ boxShadow: BrandBoxShadow; + /** Spacing scale (optional — falls back to library defaults) */ + spacing?: BrandSpacing; + /** Density configuration (optional — defaults to comfortable, 0.75 compact) */ + density?: BrandDensity; + /** Motion / transition tokens (optional — falls back to library defaults) */ + motion?: BrandMotion; + /** Focus ring tokens (optional — falls back to 2px/2px solid ring) */ + focusRing?: BrandFocusRing; } +/** + * Library-wide defaults for the optional token groups. Brands can override any + * subset; missing fields fall back here. + */ +export const defaultSpacing: BrandSpacing = { + xs: '0.25rem', + sm: '0.5rem', + md: '0.75rem', + lg: '1rem', + xl: '1.5rem', + '2xl': '2rem', +}; + +export const defaultDensity: BrandDensity = { + default: 'comfortable', + compactScale: 0.75, +}; + +export const defaultMotion: BrandMotion = { + durations: { + fast: '120ms', + base: '200ms', + slow: '320ms', + }, + easings: { + standard: 'cubic-bezier(0.2, 0, 0, 1)', + emphasized: 'cubic-bezier(0.3, 0, 0, 1)', + decelerate: 'cubic-bezier(0, 0, 0.2, 1)', + }, +}; + +export const defaultFocusRing: BrandFocusRing = { + width: '2px', + offset: '2px', + color: 'ring', + style: 'solid', +}; + +/** + * Default elevation ramp; used for any brand that doesn't override `boxShadow.1`–`6`/`inner`. + */ +export const defaultElevation = { + 1: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 2: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 3: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + 4: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + 5: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', + 6: '0 25px 50px -12px rgb(0 0 0 / 0.25)', + inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', +} as const; + // ============================================================================ // CSS Generation // ============================================================================ @@ -236,6 +356,54 @@ ${scaleBlocks} --mieweb-shadow-card: ${boxShadow.card}; --mieweb-shadow-dropdown: ${boxShadow.dropdown}; --mieweb-shadow-modal: ${boxShadow.modal}; + + /* Elevation Ramp */ + --mieweb-shadow-1: ${boxShadow[1] ?? defaultElevation[1]}; + --mieweb-shadow-2: ${boxShadow[2] ?? defaultElevation[2]}; + --mieweb-shadow-3: ${boxShadow[3] ?? defaultElevation[3]}; + --mieweb-shadow-4: ${boxShadow[4] ?? defaultElevation[4]}; + --mieweb-shadow-5: ${boxShadow[5] ?? defaultElevation[5]}; + --mieweb-shadow-6: ${boxShadow[6] ?? defaultElevation[6]}; + --mieweb-shadow-inner: ${boxShadow.inner ?? defaultElevation.inner}; + + /* Spacing Scale */ + --mieweb-spacing-xs: ${(brand.spacing ?? defaultSpacing).xs}; + --mieweb-spacing-sm: ${(brand.spacing ?? defaultSpacing).sm}; + --mieweb-spacing-md: ${(brand.spacing ?? defaultSpacing).md}; + --mieweb-spacing-lg: ${(brand.spacing ?? defaultSpacing).lg}; + --mieweb-spacing-xl: ${(brand.spacing ?? defaultSpacing).xl}; + --mieweb-spacing-2xl: ${(brand.spacing ?? defaultSpacing)['2xl']}; + + /* Density */ + --mieweb-density-scale: 1; + + /* Motion */ + --mieweb-duration-fast: ${(brand.motion ?? defaultMotion).durations.fast}; + --mieweb-duration-base: ${(brand.motion ?? defaultMotion).durations.base}; + --mieweb-duration-slow: ${(brand.motion ?? defaultMotion).durations.slow}; + --mieweb-ease-standard: ${(brand.motion ?? defaultMotion).easings.standard}; + --mieweb-ease-emphasized: ${(brand.motion ?? defaultMotion).easings.emphasized}; + --mieweb-ease-decelerate: ${(brand.motion ?? defaultMotion).easings.decelerate}; + + /* Focus Ring */ + --mieweb-focus-ring-width: ${(brand.focusRing ?? defaultFocusRing).width}; + --mieweb-focus-ring-offset: ${(brand.focusRing ?? defaultFocusRing).offset}; + --mieweb-focus-ring-style: ${(brand.focusRing ?? defaultFocusRing).style ?? 'solid'}; +} + +/* Compact density — scales spacing tokens. Applies to both data-density and legacy .condensed. */ +[data-density='compact'], +body.condensed { + --mieweb-density-scale: ${(brand.density ?? defaultDensity).compactScale}; +} + +/* Honor reduced-motion preference at the token level */ +@media (prefers-reduced-motion: reduce) { + :root { + --mieweb-duration-fast: 0ms; + --mieweb-duration-base: 0ms; + --mieweb-duration-slow: 0ms; + } } /* Dark Mode */ @@ -270,6 +438,9 @@ ${scaleBlocks} */ export function generateTailwindTheme(brand: BrandConfig) { const { colors, typography, borderRadius, boxShadow } = brand; + const spacing = brand.spacing ?? defaultSpacing; + const motion = brand.motion ?? defaultMotion; + const focusRing = brand.focusRing ?? defaultFocusRing; // Build color config including all provided optional scales const colorConfig: Record = { @@ -314,6 +485,37 @@ export function generateTailwindTheme(brand: BrandConfig) { card: boxShadow.card, dropdown: boxShadow.dropdown, modal: boxShadow.modal, + 'elevation-1': boxShadow[1] ?? defaultElevation[1], + 'elevation-2': boxShadow[2] ?? defaultElevation[2], + 'elevation-3': boxShadow[3] ?? defaultElevation[3], + 'elevation-4': boxShadow[4] ?? defaultElevation[4], + 'elevation-5': boxShadow[5] ?? defaultElevation[5], + 'elevation-6': boxShadow[6] ?? defaultElevation[6], + 'elevation-inner': boxShadow.inner ?? defaultElevation.inner, + }, + spacing: { + 'brand-xs': spacing.xs, + 'brand-sm': spacing.sm, + 'brand-md': spacing.md, + 'brand-lg': spacing.lg, + 'brand-xl': spacing.xl, + 'brand-2xl': spacing['2xl'], + }, + transitionDuration: { + fast: motion.durations.fast, + base: motion.durations.base, + slow: motion.durations.slow, + }, + transitionTimingFunction: { + standard: motion.easings.standard, + emphasized: motion.easings.emphasized, + decelerate: motion.easings.decelerate, + }, + ringWidth: { + focus: focusRing.width, + }, + ringOffsetWidth: { + focus: focusRing.offset, }, }; } diff --git a/src/components/AI/AIChatModal.tsx b/src/components/AI/AIChatModal.tsx index dc8fa9ed..11573851 100644 --- a/src/components/AI/AIChatModal.tsx +++ b/src/components/AI/AIChatModal.tsx @@ -54,8 +54,8 @@ export function AIChatTrigger({ 'fixed z-50 flex h-14 w-14 items-center justify-center rounded-full shadow-lg', 'bg-primary-800 text-white', 'hover:bg-primary-900', - 'focus:ring-primary-500 focus:ring-2 focus:ring-offset-2 focus:outline-none', - 'dark:focus:ring-offset-neutral-900', + 'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', + 'dark:focus-visible:ring-offset-neutral-900', 'transition-all duration-200', isOpen && 'scale-0 opacity-0', positionClasses[position], diff --git a/src/components/AI/MCPToolCall.tsx b/src/components/AI/MCPToolCall.tsx index ec12e5ad..f7fa36e1 100644 --- a/src/components/AI/MCPToolCall.tsx +++ b/src/components/AI/MCPToolCall.tsx @@ -410,8 +410,8 @@ export function ResourceLink({ link, onClick, className }: ResourceLinkProps) { 'bg-primary-50 text-primary-900 hover:bg-primary-100', 'dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/50', 'text-sm font-medium transition-colors', - 'focus:ring-primary-500 focus:ring-2 focus:ring-offset-2 focus:outline-none', - 'dark:focus:ring-offset-neutral-900', + 'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', + 'dark:focus-visible:ring-offset-neutral-900', className )} > diff --git a/src/components/Address/Address.tsx b/src/components/Address/Address.tsx index 75a79a15..05c0d65e 100644 --- a/src/components/Address/Address.tsx +++ b/src/components/Address/Address.tsx @@ -272,7 +272,7 @@ export function Address({ className={cn( baseStyles, 'hover:text-primary-800 dark:hover:text-primary-400 hover:underline', - 'focus:ring-primary-500 rounded focus:ring-2 focus:ring-offset-2 focus:outline-none' + 'focus-visible:ring-primary-500 rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2' )} {...props} > diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index d84e0a82..379d87e4 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -226,7 +226,7 @@ export function AppHeaderIconButton({ 'relative rounded-lg p-2 transition-colors', 'text-muted-foreground', 'hover:bg-gray-100 dark:hover:bg-gray-800', - 'focus:ring-primary-500 focus:ring-2 focus:outline-none', + 'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring-2', isActive && 'text-primary-800 dark:text-primary-400 bg-gray-100 dark:bg-gray-800', className @@ -379,7 +379,7 @@ export function AppHeaderUserMenu({ className={cn( 'flex items-center gap-3 rounded-lg px-2 py-1.5 transition-colors', 'hover:bg-gray-100 dark:hover:bg-gray-800', - 'focus:ring-primary-500 focus:ring-2 focus:outline-none', + 'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring-2', isOpen && 'bg-gray-100 dark:bg-gray-800', className )} diff --git a/src/components/AuthDialog/AuthDialog.tsx b/src/components/AuthDialog/AuthDialog.tsx index 5624bcd6..3d69cb3c 100644 --- a/src/components/AuthDialog/AuthDialog.tsx +++ b/src/components/AuthDialog/AuthDialog.tsx @@ -435,7 +435,7 @@ function LoginForm({ onSubmit, isLoading, onForgotPassword }: LoginFormProps) { onChange={(e) => setEmail(e.target.value)} required autoComplete="email" - className="focus:ring-primary-500 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-transparent focus:ring-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" + className="focus-visible:ring-primary-500 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-transparent focus-visible:ring-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="you@example.com" /> @@ -454,7 +454,7 @@ function LoginForm({ onSubmit, isLoading, onForgotPassword }: LoginFormProps) { onChange={(e) => setPassword(e.target.value)} required autoComplete="current-password" - className="focus:ring-primary-500 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-gray-900 focus:border-transparent focus:ring-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" + className="focus-visible:ring-primary-500 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-gray-900 focus:border-transparent focus-visible:ring-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="••••••••" /> - - - - - - - {/* Actions */} -
- - - - - - - - - - New user registered - Server update complete - New comment on post - - View all notifications - - - - - - - - - Profile - Settings - - Sign out - - -
- - - ); -} - -// ============================================================================ -// Stats Cards -// ============================================================================ - -function StatsCards() { - const stats = [ - { label: 'Total Users', value: '2,543', change: '+12.5%', positive: true }, - { - label: 'Active Sessions', - value: '1,234', - change: '+5.2%', - positive: true, - }, - { label: 'Revenue', value: '$45,231', change: '+8.1%', positive: true }, - { label: 'Bounce Rate', value: '23.5%', change: '-2.3%', positive: false }, - ]; - - return ( -
- {stats.map((stat) => ( - - - {stat.label} - - -
- {stat.value} - - {stat.change} - -
-
-
- ))} -
- ); -} - -// ============================================================================ -// Recent Users Table -// ============================================================================ - -function RecentUsersTable() { - const users = [ - { - id: 1, - name: 'Alice Johnson', - email: 'alice@example.com', - role: 'Admin', - status: 'Active', - joined: '2024-01-15', - }, - { - id: 2, - name: 'Bob Smith', - email: 'bob@example.com', - role: 'User', - status: 'Active', - joined: '2024-01-14', - }, - { - id: 3, - name: 'Carol Williams', - email: 'carol@example.com', - role: 'Moderator', - status: 'Pending', - joined: '2024-01-13', - }, - { - id: 4, - name: 'David Brown', - email: 'david@example.com', - role: 'User', - status: 'Inactive', - joined: '2024-01-12', - }, - { - id: 5, - name: 'Eva Martinez', - email: 'eva@example.com', - role: 'User', - status: 'Active', - joined: '2024-01-11', - }, - ]; - - return ( - - -
-
- Recent Users - - A list of recently registered users - -
- -
-
- - - - - User - Role - Status - Joined - - - - {users.map((user) => ( - - -
- -
-
{user.name}
-
- {user.email} -
-
-
-
- {user.role} - - - {user.status} - - - - {user.joined} - -
- ))} -
-
-
- - {}} - /> - -
- ); -} - -// ============================================================================ -// Activity Panel -// ============================================================================ - -function ActivityPanel() { - return ( - - - Recent Activity - Activity in your workspace - - - {[ - { - user: 'Alice', - action: 'created a new project', - time: '2 minutes ago', - }, - { - user: 'Bob', - action: 'uploaded a document', - time: '15 minutes ago', - }, - { user: 'Carol', action: 'commented on a task', time: '1 hour ago' }, - { user: 'David', action: 'completed milestone', time: '2 hours ago' }, - ].map((activity, index) => ( -
- -
-

- {activity.user}{' '} - {activity.action} -

-

{activity.time}

-
-
- ))} -
-
- ); -} - -// ============================================================================ -// Progress Overview -// ============================================================================ - -function ProgressOverview() { - return ( - - - Project Progress - Current project completion status - - -
-
- Website Redesign - 75% -
- -
-
-
- Mobile App - 45% -
- -
-
-
- API Integration - 90% -
- -
-
-
- -

Overall

-
-
-
-
- ); -} - -// ============================================================================ -// Form Demo Panel -// ============================================================================ - -function FormDemoPanel() { - const [isModalOpen, setIsModalOpen] = React.useState(false); - const [formData, setFormData] = React.useState({ - name: '', - email: '', - role: '', - department: '', - startDate: '', - bio: '', - notifications: true, - newsletter: false, - theme: 'system', - permissions: [] as string[], - }); - - return ( - <> - - - Quick Actions - Common tasks and form controls - - - - - Add User - Settings - Loading - - - -
- - setFormData({ ...formData, name: e.target.value }) - } - /> - - setFormData({ ...formData, email: e.target.value }) - } - /> -
- -
- - - -
- - - setFormData({ ...formData, startDate: e.target.value }) - } - /> - -