@ramme-io/ui 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +158 -7
  2. package/dist/blocks/ai/Actions.js +2 -2
  3. package/dist/blocks/ai/PromptInput.js +6 -6
  4. package/dist/component-registry.js +386 -359
  5. package/dist/components/ai/AIChatThread/AIChatThread.js +81 -73
  6. package/dist/components/ai/ArchitectureReferenceCard/ArchitectureReferenceCard.d.ts +28 -0
  7. package/dist/components/ai/ArchitectureReferenceCard/ArchitectureReferenceCard.js +70 -0
  8. package/dist/components/ai/ArchitectureReferenceCard/index.d.ts +2 -0
  9. package/dist/components/ai/CounterpartyGraphCard/CounterpartyGraphCard.d.ts +30 -0
  10. package/dist/components/ai/CounterpartyGraphCard/CounterpartyGraphCard.js +82 -0
  11. package/dist/components/ai/CounterpartyGraphCard/index.d.ts +2 -0
  12. package/dist/components/ai/DivergenceMetricsCard/DivergenceMetricsCard.d.ts +29 -0
  13. package/dist/components/ai/DivergenceMetricsCard/DivergenceMetricsCard.js +69 -0
  14. package/dist/components/ai/DivergenceMetricsCard/index.d.ts +2 -0
  15. package/dist/components/ai/KeyInjectionOverlay.d.ts +10 -0
  16. package/dist/components/ai/KeyInjectionOverlay.js +11 -11
  17. package/dist/components/auth/AuthLayout.js +4 -4
  18. package/dist/components/data-display/CodeBlock.d.ts +33 -7
  19. package/dist/components/data-display/CodeBlock.js +134 -41
  20. package/dist/components/data-display/Table.js +8 -8
  21. package/dist/components/feedback/EmptyState.js +9 -9
  22. package/dist/components/feedback/ToastProvider.js +4 -4
  23. package/dist/components/forms/Checkbox.js +5 -5
  24. package/dist/components/forms/Radio.js +6 -6
  25. package/dist/components/forms/SegmentedControl.d.ts +1 -1
  26. package/dist/components/forms/SegmentedControl.js +28 -11
  27. package/dist/components/forms/Textarea.js +9 -9
  28. package/dist/components/forms/ToggleSwitch.js +9 -9
  29. package/dist/components/layout/AboutRamme.d.ts +8 -0
  30. package/dist/components/layout/Accordion.js +13 -13
  31. package/dist/components/layout/AppShell.js +7 -7
  32. package/dist/components/layout/DetailDrawer.js +2 -2
  33. package/dist/components/layout/Drawer.js +50 -51
  34. package/dist/components/layout/List.js +9 -9
  35. package/dist/components/layout/Modal.js +5 -5
  36. package/dist/components/layout/PageHeader.js +7 -7
  37. package/dist/components/layout/SectionHeader.js +4 -4
  38. package/dist/components/layout/SettingsContainer.d.ts +12 -0
  39. package/dist/components/layout/SettingsContainer.js +20 -18
  40. package/dist/components/layout/Sidebar.js +1 -1
  41. package/dist/components/layout/UserNav.d.ts +13 -0
  42. package/dist/components/layout/UserNav.js +17 -17
  43. package/dist/components/molecular/ConnectorOverlay.d.ts +12 -0
  44. package/dist/components/navigation/Breadcrumbs.js +8 -8
  45. package/dist/components/navigation/Menu.js +6 -6
  46. package/dist/components/navigation/MobileNav.js +1 -1
  47. package/dist/components/navigation/Pagination.js +46 -41
  48. package/dist/components/navigation/Stepper.js +17 -17
  49. package/dist/components/navigation/Tabs.js +12 -12
  50. package/dist/components/theme/ThemeLab.d.ts +7 -1
  51. package/dist/components/theme/ThemeLab.js +175 -154
  52. package/dist/contexts/ThemeContext.js +111 -82
  53. package/dist/data/mockData.d.ts +0 -3
  54. package/dist/index.css +1 -1
  55. package/dist/index.d.ts +8 -0
  56. package/dist/index.js +203 -185
  57. package/dist/lib/hooks/useBodyScrollLock.d.ts +27 -0
  58. package/dist/lib/hooks/useBodyScrollLock.js +18 -0
  59. package/dist/lib/theme/themeUtils.d.ts +21 -0
  60. package/dist/lib/theme/themeUtils.js +31 -17
  61. package/dist/wizardManifest.js +234 -9
  62. package/package.json +14 -15
  63. package/src/wizardManifest.ts +234 -9
  64. package/tailwind.preset.js +98 -10
  65. package/src/index.css +0 -335
@@ -402,6 +402,25 @@ export const wizardManifest: WizardManifest = {
402
402
  "humanLabel": "AIChatThread",
403
403
  "humanDescription": "A complete chat surface — message history, streaming indicator, error banner, provider/model attribution, and the composer. Strategic purpose: the presentational half of a chat feature, driven entirely by props from kernel's useRammeAI(). It holds no transport of its own, and takes a `renderMessage` override so products can add markdown or rich rendering without pulling those deps into @ramme-io/ui."
404
404
  },
405
+ {
406
+ "id": "architecture-reference-card",
407
+ "name": "ArchitectureReferenceCard",
408
+ "version": "1.0.0",
409
+ "description": "AI-generated card summarizing this platform's own data architecture — stack, data model, AI pipelines, and sentinel fleet",
410
+ "tags": [
411
+ "ai",
412
+ "architecture",
413
+ "reference",
414
+ "generative-ui"
415
+ ],
416
+ "category": "ai",
417
+ "props": [],
418
+ "importPath": "./components/ai/ArchitectureReferenceCard/ArchitectureReferenceCard",
419
+ "filePath": "src/components/ai/ArchitectureReferenceCard/ArchitectureReferenceCard.tsx",
420
+ "sku": "@molecule/ai/architecture-reference-card",
421
+ "humanLabel": "ArchitectureReferenceCard",
422
+ "humanDescription": "AI-generated card summarizing this platform's own data architecture — stack, data model, AI pipelines, and sentinel fleet"
423
+ },
405
424
  {
406
425
  "id": "company-profile-card",
407
426
  "name": "CompanyProfileCard",
@@ -429,6 +448,77 @@ export const wizardManifest: WizardManifest = {
429
448
  "humanLabel": "CompanyProfileCard",
430
449
  "humanDescription": "A company summary for a ticker — sector, industry, and description — fetched on demand and cached. Strategic purpose: a generative-UI block an AI tool call can render directly into a chat thread. Surfaces its own provenance, distinguishing a live fetch from a cached or stale answer instead of presenting all three as equally fresh."
431
450
  },
451
+ {
452
+ "id": "counterparty-graph-card",
453
+ "name": "CounterpartyGraphCard",
454
+ "version": "1.0.0",
455
+ "description": "AI-generated counterparty relationship card showing suppliers, customers, partners, and competitors inferred from alternative signals",
456
+ "tags": [
457
+ "ai",
458
+ "counterparty",
459
+ "graph",
460
+ "relationships",
461
+ "generative-ui"
462
+ ],
463
+ "category": "ai",
464
+ "props": [],
465
+ "importPath": "./components/ai/CounterpartyGraphCard/CounterpartyGraphCard",
466
+ "filePath": "src/components/ai/CounterpartyGraphCard/CounterpartyGraphCard.tsx",
467
+ "sku": "@molecule/ai/counterparty-graph-card",
468
+ "humanLabel": "CounterpartyGraphCard",
469
+ "humanDescription": "AI-generated counterparty relationship card showing suppliers, customers, partners, and competitors inferred from alternative signals"
470
+ },
471
+ {
472
+ "id": "divergence-metrics-card",
473
+ "name": "DivergenceMetricsCard",
474
+ "version": "1.0.0",
475
+ "description": "AI-generated card showing operational and customer divergence scores — a signed delta between expected and observed performance",
476
+ "tags": [
477
+ "ai",
478
+ "divergence",
479
+ "metrics",
480
+ "generative-ui"
481
+ ],
482
+ "category": "ai",
483
+ "props": [],
484
+ "importPath": "./components/ai/DivergenceMetricsCard/DivergenceMetricsCard",
485
+ "filePath": "src/components/ai/DivergenceMetricsCard/DivergenceMetricsCard.tsx",
486
+ "sku": "@molecule/ai/divergence-metrics-card",
487
+ "humanLabel": "DivergenceMetricsCard",
488
+ "humanDescription": "AI-generated card showing operational and customer divergence scores — a signed delta between expected and observed performance"
489
+ },
490
+ {
491
+ "id": "key-injection-overlay",
492
+ "name": "KeyInjectionOverlay",
493
+ "version": "1.0.0",
494
+ "description": "Blocks the app and collects an AI provider key when the gateway reports one is missing or expired. Strategic purpose: the BYOK prompt, in one place, so a key request looks the same everywhere and never becomes an inline form somebody rebuilds per feature. Renders nothing unless status is 'BYOK_REQUIRED' or 'KEY_EXPIRED', so it can stay mounted at the shell level.",
495
+ "tags": [
496
+ "ai",
497
+ "byok",
498
+ "overlay",
499
+ "auth",
500
+ "gateway",
501
+ "key"
502
+ ],
503
+ "category": "ai",
504
+ "props": [
505
+ {
506
+ "name": "status",
507
+ "type": "string",
508
+ "description": "Gateway status. The overlay shows only for 'BYOK_REQUIRED' or 'KEY_EXPIRED'."
509
+ },
510
+ {
511
+ "name": "setSessionKey",
512
+ "type": "function",
513
+ "description": "Receives the entered key when the form is submitted."
514
+ }
515
+ ],
516
+ "importPath": "./components/ai/KeyInjectionOverlay",
517
+ "filePath": "src/components/ai/KeyInjectionOverlay.tsx",
518
+ "sku": "@molecule/ai/key-injection-overlay",
519
+ "humanLabel": "KeyInjectionOverlay",
520
+ "humanDescription": "Blocks the app and collects an AI provider key when the gateway reports one is missing or expired. Strategic purpose: the BYOK prompt, in one place, so a key request looks the same everywhere and never becomes an inline form somebody rebuilds per feature. Renders nothing unless status is 'BYOK_REQUIRED' or 'KEY_EXPIRED', so it can stay mounted at the shell level."
521
+ },
432
522
  {
433
523
  "id": "live-quote-card",
434
524
  "name": "LiveQuoteCard",
@@ -755,31 +845,58 @@ export const wizardManifest: WizardManifest = {
755
845
  {
756
846
  "id": "code-block",
757
847
  "name": "CodeBlock",
758
- "version": "1.0.0",
759
- "description": "A component for displaying formatted and syntax-highlighted code snippets.",
848
+ "version": "1.0.1",
849
+ "description": "The one way to show code. Syntax-highlighted, theme-aware, with a copy button that works on touch and outside secure contexts. Use for every snippet in docs and tutorials — do not hand-roll a <pre>. Long lines scroll inside the block so the page never scrolls sideways on a phone.",
760
850
  "tags": [
761
851
  "ui",
762
852
  "data-display",
763
- "code"
853
+ "code",
854
+ "docs",
855
+ "tutorial"
764
856
  ],
765
857
  "category": "ui",
766
858
  "props": [
767
859
  {
768
860
  "name": "code",
769
861
  "type": "string",
770
- "description": "The string of code to display."
862
+ "description": "The snippet to display. Outer blank lines are trimmed."
771
863
  },
772
864
  {
773
865
  "name": "language",
774
866
  "type": "string",
775
- "description": "The programming language for syntax highlighting (e.g., 'tsx', 'css', 'javascript')."
867
+ "description": "Prism language id — 'tsx', 'bash', 'css', 'json'. Defaults to 'tsx'."
868
+ },
869
+ {
870
+ "name": "filename",
871
+ "type": "string",
872
+ "description": "Header label. Prefer a real path like 'src/App.tsx'; falls back to the language."
873
+ },
874
+ {
875
+ "name": "showLineNumbers",
876
+ "type": "boolean",
877
+ "description": "Show gutter line numbers. Use when the surrounding prose cites line numbers."
878
+ },
879
+ {
880
+ "name": "highlightLines",
881
+ "type": "number[]",
882
+ "description": "1-based lines to emphasise, for \"change this line\" steps."
883
+ },
884
+ {
885
+ "name": "wrap",
886
+ "type": "boolean",
887
+ "description": "Wrap long lines instead of scrolling them. Off by default."
888
+ },
889
+ {
890
+ "name": "hideCopy",
891
+ "type": "boolean",
892
+ "description": "Hide the copy button, for snippets that are not meant to be run."
776
893
  }
777
894
  ],
778
895
  "importPath": "./components/data-display/CodeBlock",
779
896
  "filePath": "src/components/data-display/CodeBlock.tsx",
780
897
  "sku": "@atom/primitive/code-block",
781
898
  "humanLabel": "CodeBlock",
782
- "humanDescription": "A component for displaying formatted and syntax-highlighted code snippets."
899
+ "humanDescription": "The one way to show code. Syntax-highlighted, theme-aware, with a copy button that works on touch and outside secure contexts. Use for every snippet in docs and tutorials — do not hand-roll a <pre>. Long lines scroll inside the block so the page never scrolls sideways on a phone."
783
900
  },
784
901
  {
785
902
  "id": "data-table",
@@ -1752,8 +1869,8 @@ export const wizardManifest: WizardManifest = {
1752
1869
  {
1753
1870
  "id": "segmented-control",
1754
1871
  "name": "SegmentedControl",
1755
- "version": "1.0.0",
1756
- "description": "A theme-aware linear toggle component. Ideal for switching between discrete modes (e.g., \"Map / List\").",
1872
+ "version": "1.0.1",
1873
+ "description": "A theme-aware linear toggle component. Ideal for switching between discrete modes (e.g., \"Map / List\"). Every size clears the 24px WCAG 2.5.8 (AA) target-size minimum — xs 24px, sm ~25px, md ~36px. None reaches the 44px 2.5.5 (AAA) figure, so for a primary control on a touch screen prefer md or give the control its own 44px row; xs and sm are density sizes for pointer-driven toolbars.",
1757
1874
  "tags": [
1758
1875
  "input",
1759
1876
  "toggle",
@@ -1767,7 +1884,7 @@ export const wizardManifest: WizardManifest = {
1767
1884
  "filePath": "src/components/forms/SegmentedControl.tsx",
1768
1885
  "sku": "@atom/form/segmented-control",
1769
1886
  "humanLabel": "SegmentedControl",
1770
- "humanDescription": "A theme-aware linear toggle component. Ideal for switching between discrete modes (e.g., \"Map / List\")."
1887
+ "humanDescription": "A theme-aware linear toggle component. Ideal for switching between discrete modes (e.g., \"Map / List\"). Every size clears the 24px WCAG 2.5.8 (AA) target-size minimum — xs 24px, sm ~25px, md ~36px. None reaches the 44px 2.5.5 (AAA) figure, so for a primary control on a touch screen prefer md or give the control its own 44px row; xs and sm are density sizes for pointer-driven toolbars."
1771
1888
  },
1772
1889
  {
1773
1890
  "id": "select",
@@ -2242,6 +2359,48 @@ export const wizardManifest: WizardManifest = {
2242
2359
  "humanLabel": "SectionHeader",
2243
2360
  "humanDescription": "A simple, consistent header for organizing sections of content within a page."
2244
2361
  },
2362
+ {
2363
+ "id": "connector-overlay",
2364
+ "name": "ConnectorOverlay",
2365
+ "version": "1.0.0",
2366
+ "description": "Covers a panel while its data source is connecting, and explains itself when the connection fails. Strategic purpose: the standard treatment for a live region that has no data yet — telemetry panels, market feeds, device dashboards — so every product says the same thing about a dead socket instead of inventing a spinner. Renders nothing once status is 'connected', so it can stay mounted.",
2367
+ "tags": [
2368
+ "feedback",
2369
+ "overlay",
2370
+ "connection",
2371
+ "realtime",
2372
+ "loading",
2373
+ "error"
2374
+ ],
2375
+ "category": "feedback",
2376
+ "props": [
2377
+ {
2378
+ "name": "status",
2379
+ "type": "string",
2380
+ "description": "'connecting' | 'connected' | 'error'. Renders null when connected."
2381
+ },
2382
+ {
2383
+ "name": "message",
2384
+ "type": "ReactNode",
2385
+ "description": "Replaces the default copy for the current status."
2386
+ },
2387
+ {
2388
+ "name": "onRetry",
2389
+ "type": "function",
2390
+ "description": "Shows a retry action in the error state when provided."
2391
+ },
2392
+ {
2393
+ "name": "className",
2394
+ "type": "string",
2395
+ "description": "Additional CSS classes."
2396
+ }
2397
+ ],
2398
+ "importPath": "./components/molecular/ConnectorOverlay",
2399
+ "filePath": "src/components/molecular/ConnectorOverlay.tsx",
2400
+ "sku": "@molecule/feedback/connector-overlay",
2401
+ "humanLabel": "ConnectorOverlay",
2402
+ "humanDescription": "Covers a panel while its data source is connecting, and explains itself when the connection fails. Strategic purpose: the standard treatment for a live region that has no data yet — telemetry panels, market feeds, device dashboards — so every product says the same thing about a dead socket instead of inventing a spinner. Renders nothing once status is 'connected', so it can stay mounted."
2403
+ },
2245
2404
  {
2246
2405
  "id": "data-card",
2247
2406
  "name": "DataCard",
@@ -2982,6 +3141,26 @@ export const wizardManifest: WizardManifest = {
2982
3141
  "humanLabel": "AuthLayout",
2983
3142
  "humanDescription": "The centred, chrome-free frame for unauthenticated screens, rendering the matched child route via an Outlet. Strategic purpose: sign-in and sign-up must not show the app shell's sidebar or header — wrap those routes in this instead of conditionally hiding chrome inside AppShell."
2984
3143
  },
3144
+ {
3145
+ "id": "about-ramme",
3146
+ "name": "AboutRamme",
3147
+ "version": "1.0.0",
3148
+ "description": "The \"About\" panel for a Settings Console — what the framework is, which packages make it up, and why it is built this way. Strategic purpose: ships the explanation with the framework so every product's about screen stays accurate as the architecture moves, instead of each one carrying a copy that goes stale. Takes no props; drop it into a settings tab.",
3149
+ "tags": [
3150
+ "layout",
3151
+ "settings",
3152
+ "about",
3153
+ "documentation",
3154
+ "panel"
3155
+ ],
3156
+ "category": "layout",
3157
+ "props": [],
3158
+ "importPath": "./components/layout/AboutRamme",
3159
+ "filePath": "src/components/layout/AboutRamme.tsx",
3160
+ "sku": "@chassis/layout/about-ramme",
3161
+ "humanLabel": "AboutRamme",
3162
+ "humanDescription": "The \"About\" panel for a Settings Console — what the framework is, which packages make it up, and why it is built this way. Strategic purpose: ships the explanation with the framework so every product's about screen stays accurate as the architecture moves, instead of each one carrying a copy that goes stale. Takes no props; drop it into a settings tab."
3163
+ },
2985
3164
  {
2986
3165
  "id": "accordion-item",
2987
3166
  "name": "AccordionItem",
@@ -4126,6 +4305,52 @@ export const wizardManifest: WizardManifest = {
4126
4305
  "sku": "@chassis/layout/user-nav",
4127
4306
  "humanLabel": "UserNav",
4128
4307
  "humanDescription": "The signed-in user's avatar menu — identity plus account actions. Strategic purpose: the standard account affordance for the app shell's header, with logout, settings, profile, and an optional theme toggle already wired. Compose extra entries with ButtonItem rather than rebuilding the menu."
4308
+ },
4309
+ {
4310
+ "id": "button-item",
4311
+ "name": "ButtonItem",
4312
+ "version": "1.0.0",
4313
+ "description": "A single row inside a UserNav menu — icon, label, action. Strategic purpose: the sanctioned way to add an entry to the account menu, so extra actions match the built-in ones instead of arriving as a hand-styled button. Use `variant=\"destructive\"` for anything that removes or signs out.",
4314
+ "tags": [
4315
+ "layout",
4316
+ "navigation",
4317
+ "menu",
4318
+ "item",
4319
+ "user-nav"
4320
+ ],
4321
+ "category": "layout",
4322
+ "props": [
4323
+ {
4324
+ "name": "icon",
4325
+ "type": "string",
4326
+ "description": "Lucide icon name shown before the label."
4327
+ },
4328
+ {
4329
+ "name": "label",
4330
+ "type": "string",
4331
+ "description": "The menu entry text."
4332
+ },
4333
+ {
4334
+ "name": "onClick",
4335
+ "type": "function",
4336
+ "description": "Called when the entry is activated."
4337
+ },
4338
+ {
4339
+ "name": "variant",
4340
+ "type": "string",
4341
+ "description": "'default' | 'destructive'. Destructive tints the row for sign-out and delete."
4342
+ },
4343
+ {
4344
+ "name": "className",
4345
+ "type": "string",
4346
+ "description": "Additional CSS classes."
4347
+ }
4348
+ ],
4349
+ "importPath": "./components/layout/UserNav",
4350
+ "filePath": "src/components/layout/UserNav.tsx",
4351
+ "sku": "@chassis/layout/button-item",
4352
+ "humanLabel": "ButtonItem",
4353
+ "humanDescription": "A single row inside a UserNav menu — icon, label, action. Strategic purpose: the sanctioned way to add an entry to the account menu, so extra actions match the built-in ones instead of arriving as a hand-styled button. Use `variant=\"destructive\"` for anything that removes or signs out."
4129
4354
  }
4130
4355
  ],
4131
4356
  "utils": [],
@@ -71,6 +71,14 @@ module.exports = {
71
71
  },
72
72
 
73
73
  // ─── Specialized Sub-Systems ───
74
+ // Categorical accents for colour-coded content. Distinct from `chart`:
75
+ // chart series collapse to one hue under a custom theme (see
76
+ // themeUtils.ts), these stay four hues on all five themes.
77
+ 'accent-1': 'rgb(var(--app-accent-1) / <alpha-value>)',
78
+ 'accent-2': 'rgb(var(--app-accent-2) / <alpha-value>)',
79
+ 'accent-3': 'rgb(var(--app-accent-3) / <alpha-value>)',
80
+ 'accent-4': 'rgb(var(--app-accent-4) / <alpha-value>)',
81
+
74
82
  chart: {
75
83
  1: 'rgb(var(--app-chart-1) / <alpha-value>)',
76
84
  2: 'rgb(var(--app-chart-2) / <alpha-value>)',
@@ -105,17 +113,97 @@ module.exports = {
105
113
  mono: ['var(--app-font-mono)', 'Menlo', 'monospace'],
106
114
  },
107
115
 
108
- // ─── Fluid Typography Scale (clamp) ───────────────
109
- // Scales smoothly between 320px and 1280px viewport.
110
- // Usage: text-fluid-xs, text-fluid-sm, text-fluid-base, etc.
116
+ // ─── Fluid Typography Scale ───────────────────────
117
+ // The values live in framework/ui/src/index.css as --app-step-*, generated
118
+ // with the Utopia method (utopia.fyi/type/calculator) and fluid between a
119
+ // 360px and a 1240px viewport. They are referenced here rather than inlined
120
+ // for the same reason borderRadius is: a CSS variable can be re-pointed per
121
+ // theme (.dark/.corporate/.midnight/.blueprint) and adjusted at runtime
122
+ // through useTheme, which a literal in this file cannot.
123
+ //
124
+ // These REPLACE Tailwind's static defaults, so every existing text-sm /
125
+ // text-2xl / … call site becomes fluid with no edit. Each entry is a
126
+ // [size, { lineHeight, letterSpacing }] tuple — Tailwind's own scale pairs a
127
+ // line-height with every size, and dropping to a bare string (as the old
128
+ // fluid-* keys did) silently inherits whatever leading is in scope.
111
129
  fontSize: {
112
- 'fluid-xs': 'clamp(0.625rem, 0.55rem + 0.25vw, 0.75rem)', // 10px → 12px
113
- 'fluid-sm': 'clamp(0.75rem, 0.65rem + 0.33vw, 0.875rem)', // 12px → 14px
114
- 'fluid-base': 'clamp(0.875rem, 0.75rem + 0.42vw, 1rem)', // 14px → 16px
115
- 'fluid-lg': 'clamp(1rem, 0.83rem + 0.56vw, 1.25rem)', // 16px → 20px
116
- 'fluid-xl': 'clamp(1.25rem, 1rem + 0.83vw, 1.5rem)', // 20px → 24px
117
- 'fluid-2xl': 'clamp(1.5rem, 1.17rem + 1.11vw, 2rem)', // 24px → 32px
118
- 'fluid-3xl': 'clamp(2rem, 1.5rem + 1.67vw, 2.75rem)', // 32px → 44px
130
+ '2xs': ['var(--app-step--3)', { lineHeight: 'var(--app-leading-base)' }],
131
+ xs: ['var(--app-step--2)', { lineHeight: 'var(--app-leading-base)' }],
132
+ sm: ['var(--app-step--1)', { lineHeight: 'var(--app-leading-relaxed)' }],
133
+ base: ['var(--app-step-0)', { lineHeight: 'var(--app-leading-relaxed)' }],
134
+ lg: ['var(--app-step-1)', { lineHeight: 'var(--app-leading-relaxed)' }],
135
+ xl: ['var(--app-step-2)', { lineHeight: 'var(--app-leading-snug)' }],
136
+ '2xl': ['var(--app-step-3)', { lineHeight: 'var(--app-leading-snug)' }],
137
+ '3xl': ['var(--app-step-4)', { lineHeight: 'var(--app-leading-snug)' }],
138
+ '4xl': ['var(--app-step-5)', { lineHeight: 'var(--app-leading-tight)' }],
139
+ '5xl': ['var(--app-step-6)', { lineHeight: 'var(--app-leading-tight)' }],
140
+ '6xl': ['var(--app-step-7)', { lineHeight: 'var(--app-leading-tight)' }],
141
+ '7xl': ['var(--app-step-8)', { lineHeight: 'var(--app-leading-none)' }],
142
+ '8xl': ['var(--app-step-9)', { lineHeight: 'var(--app-leading-none)' }],
143
+ '9xl': ['var(--app-step-10)', { lineHeight: 'var(--app-leading-none)' }],
144
+
145
+ // Back-compat aliases. text-fluid-* predates this scale and is still used
146
+ // in Card, ActivityFeed and MobileNav; they now resolve to the same steps
147
+ // as their plain counterparts. Prefer the plain names in new code.
148
+ 'fluid-xs': ['var(--app-step--2)', { lineHeight: 'var(--app-leading-base)' }],
149
+ 'fluid-sm': ['var(--app-step--1)', { lineHeight: 'var(--app-leading-relaxed)' }],
150
+ 'fluid-base': ['var(--app-step-0)', { lineHeight: 'var(--app-leading-relaxed)' }],
151
+ 'fluid-lg': ['var(--app-step-1)', { lineHeight: 'var(--app-leading-relaxed)' }],
152
+ 'fluid-xl': ['var(--app-step-2)', { lineHeight: 'var(--app-leading-snug)' }],
153
+ 'fluid-2xl': ['var(--app-step-3)', { lineHeight: 'var(--app-leading-snug)' }],
154
+ 'fluid-3xl': ['var(--app-step-4)', { lineHeight: 'var(--app-leading-snug)' }],
155
+ },
156
+
157
+ // ─── Leading ──────────────────────────────────────
158
+ // Unitless, so they multiply whichever step they land on.
159
+ lineHeight: {
160
+ none: 'var(--app-leading-none)',
161
+ tight: 'var(--app-leading-tight)',
162
+ snug: 'var(--app-leading-snug)',
163
+ normal: 'var(--app-leading-base)',
164
+ relaxed: 'var(--app-leading-relaxed)',
165
+ },
166
+
167
+ // ─── Tracking ─────────────────────────────────────
168
+ // Replaces the hand-written tracking-[0.12em..0.3em] arbitrary values.
169
+ letterSpacing: {
170
+ tighter: 'var(--app-tracking-tighter)',
171
+ tight: 'var(--app-tracking-tight)',
172
+ normal: 'var(--app-tracking-normal)',
173
+ wide: 'var(--app-tracking-wide)',
174
+ wider: 'var(--app-tracking-wider)',
175
+ widest: 'var(--app-tracking-widest)',
176
+ },
177
+
178
+ // ─── Readable measure ─────────────────────────────
179
+ // ch-based, so line length tracks font size. max-w-prose is rem-based and
180
+ // drifts off a comfortable measure as soon as the base size changes.
181
+ maxWidth: {
182
+ measure: 'var(--app-measure)',
183
+ 'measure-narrow': 'var(--app-measure-narrow)',
184
+ },
185
+
186
+ // ─── Layering Scale ───────────────────────────────
187
+ // One named order for everything that overlays, because "z-50" was the
188
+ // only overlay layer this framework had: Modal, Drawer, DetailDrawer,
189
+ // ToastProvider, Menu, UserNav, Sidebar and AIChatPanel all sat on it,
190
+ // so which one won was decided by portal mount order rather than intent.
191
+ // A toast confirming what you just did could land behind the modal you
192
+ // did it in.
193
+ //
194
+ // Read it top to bottom — each layer must clear the one above it in this
195
+ // list. Use the name, never a bare number, so the order stays arguable in
196
+ // one place instead of in eleven files.
197
+ zIndex: {
198
+ raised: '10', // lifted within its own pane — cards, in-flow overlays
199
+ sticky: '20', // sticky headers and toolbars inside a scroll region
200
+ nav: '30', // app chrome that outranks sticky page content
201
+ fab: '40', // floating action buttons over the page
202
+ scrim: '45', // a backdrop, under its own panel but over the app
203
+ drawer: '50', // drawers, modals, full-screen surfaces and overlays
204
+ popover: '60', // menus, dropdowns, tooltips — must clear a drawer
205
+ // they were opened from
206
+ toast: '70', // notifications — must clear everything, always
119
207
  },
120
208
 
121
209
  // ─── Fluid Spacing Scale (clamp) ──────────────────