@happyvertical/smrt-ui 0.55.16 → 0.55.18

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 (166) hide show
  1. package/AGENTS.md +14 -0
  2. package/README.md +75 -0
  3. package/dist/components/calendar/Calendar.svelte +59 -56
  4. package/dist/components/calendar/CalendarView.svelte +80 -77
  5. package/dist/components/calendar/DayView.svelte +39 -36
  6. package/dist/components/chat/MessageBubble.svelte +26 -23
  7. package/dist/components/chat/ReactionPicker.svelte +12 -9
  8. package/dist/components/chat/TypingIndicator.svelte +12 -9
  9. package/dist/components/data/CollectionList.svelte +11 -8
  10. package/dist/components/data/CollectionToolbar.svelte +5 -2
  11. package/dist/components/data/DataTable.svelte +74 -59
  12. package/dist/components/data/ListSortSelect.svelte +8 -5
  13. package/dist/components/data/SortableHeader.svelte +10 -7
  14. package/dist/components/display/ConfidenceBadge.svelte +11 -8
  15. package/dist/components/display/CurrencyDisplay.svelte +7 -4
  16. package/dist/components/display/DateDisplay.svelte +6 -3
  17. package/dist/components/display/Icon.svelte +4 -1
  18. package/dist/components/display/StatusBadge.svelte +9 -6
  19. package/dist/components/feedback/Alert.svelte +12 -8
  20. package/dist/components/feedback/ConfirmDialog.svelte +13 -10
  21. package/dist/components/feedback/LoadingOverlay.svelte +37 -34
  22. package/dist/components/feedback/Meter.svelte +4 -1
  23. package/dist/components/feedback/Modal.svelte +26 -21
  24. package/dist/components/feedback/PhoneSheet.svelte +6 -3
  25. package/dist/components/feedback/Progress.svelte +8 -5
  26. package/dist/components/feedback/ProgressBar.svelte +13 -10
  27. package/dist/components/feedback/Spinner.svelte +4 -1
  28. package/dist/components/feedback/ToastViewport.svelte +18 -15
  29. package/dist/components/feedback/WorkingStrip.svelte +6 -3
  30. package/dist/components/forms/CameraCapture.svelte +8 -5
  31. package/dist/components/forms/Checkbox.svelte +11 -8
  32. package/dist/components/forms/Combobox.svelte +5 -2
  33. package/dist/components/forms/DatePicker.svelte +7 -2
  34. package/dist/components/forms/DictationButton.svelte +17 -14
  35. package/dist/components/forms/DictationStatus.svelte +12 -9
  36. package/dist/components/forms/ErrorSummary.svelte +4 -1
  37. package/dist/components/forms/FieldLabel.svelte +9 -6
  38. package/dist/components/forms/Fieldset.svelte +4 -1
  39. package/dist/components/forms/FilePicker.svelte +5 -2
  40. package/dist/components/forms/Form.svelte +1 -1
  41. package/dist/components/forms/FormActionBar.svelte +4 -1
  42. package/dist/components/forms/FormGroup.svelte +6 -3
  43. package/dist/components/forms/Input.svelte +22 -15
  44. package/dist/components/forms/InputGroup.svelte +6 -3
  45. package/dist/components/forms/Listbox.svelte +5 -2
  46. package/dist/components/forms/MultiSelect.svelte +4 -1
  47. package/dist/components/forms/Radio.svelte +9 -6
  48. package/dist/components/forms/RadioGroup.svelte +5 -2
  49. package/dist/components/forms/RangeSlider.svelte +5 -2
  50. package/dist/components/forms/RelationInput.svelte +7 -4
  51. package/dist/components/forms/SearchInput.svelte +7 -4
  52. package/dist/components/forms/SegmentedControl.svelte +6 -3
  53. package/dist/components/forms/Select.svelte +20 -13
  54. package/dist/components/forms/SignaturePad.svelte +8 -5
  55. package/dist/components/forms/Slider.svelte +5 -2
  56. package/dist/components/forms/StagedControlReview.svelte +15 -12
  57. package/dist/components/forms/Switch.svelte +8 -5
  58. package/dist/components/forms/TagsInput.svelte +5 -2
  59. package/dist/components/forms/Textarea.svelte +20 -13
  60. package/dist/components/forms/TimePicker.svelte +7 -2
  61. package/dist/components/forms/Toggle.svelte +14 -11
  62. package/dist/components/forms/ToggleButton.svelte +5 -2
  63. package/dist/components/layout/ActionGroup.svelte +4 -1
  64. package/dist/components/layout/Container.svelte +4 -1
  65. package/dist/components/layout/EmptyState.svelte +11 -8
  66. package/dist/components/layout/Footer.svelte +4 -1
  67. package/dist/components/layout/Grid.svelte +13 -10
  68. package/dist/components/layout/Header.svelte +4 -1
  69. package/dist/components/layout/Masthead.svelte +4 -1
  70. package/dist/components/layout/PageHeader.svelte +12 -9
  71. package/dist/components/layout/PageLayout.svelte +3 -0
  72. package/dist/components/layout/SummaryCard.svelte +9 -6
  73. package/dist/components/memberships/MembershipCard.svelte +5 -2
  74. package/dist/components/memberships/MembershipList.svelte +5 -2
  75. package/dist/components/nav/FilterChips.svelte +13 -10
  76. package/dist/components/nav/Tabs.svelte +75 -41
  77. package/dist/components/permissions/PermissionPicker.svelte +5 -2
  78. package/dist/components/roles/RoleBadge.svelte +8 -5
  79. package/dist/components/roles/RoleSelector.svelte +22 -19
  80. package/dist/components/theme/ThemeProvider.svelte +7 -4
  81. package/dist/components/ui/Accordion.svelte +4 -1
  82. package/dist/components/ui/AccordionItem.svelte +5 -2
  83. package/dist/components/ui/Avatar.svelte +10 -7
  84. package/dist/components/ui/Badge.svelte +6 -2
  85. package/dist/components/ui/Button.svelte +49 -37
  86. package/dist/components/ui/Card.svelte +18 -14
  87. package/dist/components/ui/Chip.svelte +41 -14
  88. package/dist/components/ui/Disclosure.svelte +5 -2
  89. package/dist/components/ui/Dropdown.svelte +33 -25
  90. package/dist/components/ui/IconToggle.svelte +12 -9
  91. package/dist/components/ui/IconToggle.svelte.d.ts +1 -1
  92. package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -1
  93. package/dist/components/ui/IconToggleGroup.svelte +8 -5
  94. package/dist/components/ui/Pagination.svelte +11 -8
  95. package/dist/components/ui/Popover.svelte +9 -6
  96. package/dist/components/ui/Skeleton.svelte +10 -7
  97. package/dist/components/ui/Tooltip.svelte +13 -9
  98. package/dist/components/ui/Tree.svelte +12 -9
  99. package/dist/styles/layers.css +8 -0
  100. package/dist/styles/layers.d.ts +9 -0
  101. package/dist/styles/layers.d.ts.map +1 -0
  102. package/dist/styles/layers.js +8 -0
  103. package/dist/styles/tokens.css +27 -69
  104. package/dist/styles/unstyled.css +65 -0
  105. package/dist/svelte/playground/BaseControlsPreview.svelte +3 -0
  106. package/dist/svelte/playground/CalendarPreview.svelte +5 -2
  107. package/dist/svelte/playground/CapturePreview.svelte +3 -0
  108. package/dist/svelte/playground/CodeSelectorsPreview.svelte +3 -0
  109. package/dist/svelte/playground/CollectionsPreview.svelte +3 -0
  110. package/dist/svelte/playground/DataTablePreview.svelte +3 -0
  111. package/dist/svelte/playground/FeedbackPreview.svelte +3 -0
  112. package/dist/svelte/playground/InteractiveControlsPreview.svelte +3 -0
  113. package/dist/svelte/playground/StylelessPreview.svelte +110 -0
  114. package/dist/svelte/playground/StylelessPreview.svelte.d.ts +4 -0
  115. package/dist/svelte/playground/StylelessPreview.svelte.d.ts.map +1 -0
  116. package/dist/svelte/playground.d.ts.map +1 -1
  117. package/dist/svelte/playground.js +10 -0
  118. package/dist/test-support/setup.js +9 -0
  119. package/dist/theme/ThemeProvider.svelte +3 -0
  120. package/dist/themes/CUSTOM_THEME_GUIDE.md +40 -0
  121. package/dist/themes/README.md +151 -0
  122. package/dist/themes/ThemeProvider.svelte +181 -12
  123. package/dist/themes/ThemeProvider.svelte.d.ts +7 -2
  124. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
  125. package/dist/themes/components/ColorSchemeToggle.svelte +17 -14
  126. package/dist/themes/components/ThemeSwitcher.svelte +14 -11
  127. package/dist/themes/create-theme.d.ts +22 -1
  128. package/dist/themes/create-theme.d.ts.map +1 -1
  129. package/dist/themes/create-theme.js +100 -9
  130. package/dist/themes/css-generator.d.ts.map +1 -1
  131. package/dist/themes/css-generator.js +20 -20
  132. package/dist/themes/derive.d.ts +31 -0
  133. package/dist/themes/derive.d.ts.map +1 -0
  134. package/dist/themes/derive.js +183 -0
  135. package/dist/themes/glass/index.d.ts.map +1 -1
  136. package/dist/themes/glass/index.js +127 -14
  137. package/dist/themes/happyvertical/index.d.ts.map +1 -1
  138. package/dist/themes/happyvertical/index.js +101 -20
  139. package/dist/themes/index.d.ts +2 -2
  140. package/dist/themes/index.d.ts.map +1 -1
  141. package/dist/themes/index.js +1 -1
  142. package/dist/themes/material/index.d.ts.map +1 -1
  143. package/dist/themes/material/index.js +129 -28
  144. package/dist/themes/required-tokens.generated.json +201 -0
  145. package/dist/themes/shared.d.ts +35 -0
  146. package/dist/themes/shared.d.ts.map +1 -1
  147. package/dist/themes/shared.js +32 -0
  148. package/dist/themes/smrt/index.d.ts +3 -0
  149. package/dist/themes/smrt/index.d.ts.map +1 -1
  150. package/dist/themes/smrt/index.js +108 -26
  151. package/dist/themes/studio/index.d.ts.map +1 -1
  152. package/dist/themes/studio/index.js +116 -40
  153. package/dist/themes/styles/base.css +229 -1
  154. package/dist/themes/styles/glass.css +107 -20
  155. package/dist/themes/styles/happyvertical.css +111 -15
  156. package/dist/themes/styles/material.css +127 -35
  157. package/dist/themes/styles/smrt.css +126 -31
  158. package/dist/themes/styles/studio.css +132 -37
  159. package/dist/themes/theme-script.d.ts +3 -1
  160. package/dist/themes/theme-script.d.ts.map +1 -1
  161. package/dist/themes/theme-script.js +7 -2
  162. package/dist/themes/types.d.ts +118 -6
  163. package/dist/themes/types.d.ts.map +1 -1
  164. package/dist/utils/theme/typography.d.ts +21 -21
  165. package/dist/utils/theme/typography.js +21 -21
  166. package/package.json +8 -2
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ import Alert from '../../components/feedback/Alert.svelte';
3
+ import Input from '../../components/forms/Input.svelte';
4
+ import Select from '../../components/forms/Select.svelte';
5
+ import Tabs from '../../components/nav/Tabs.svelte';
6
+ import Badge from '../../components/ui/Badge.svelte';
7
+ import Button from '../../components/ui/Button.svelte';
8
+ import Card from '../../components/ui/Card.svelte';
9
+ import Chip from '../../components/ui/Chip.svelte';
10
+ import unstyledCss from '../../styles/unstyled.css?inline';
11
+
12
+ // Native checkbox, outside the catalogue, so the toggle itself is never stripped.
13
+ let styleless = $state(false);
14
+ let active = $state('overview');
15
+
16
+ // Svelte does not interpolate text inside a style element, so the stylesheet is
17
+ // appended to the document head imperatively while the toggle is on.
18
+ $effect(() => {
19
+ if (!styleless) return;
20
+ const style = document.createElement('style');
21
+ style.setAttribute('data-smrt-styleless', '');
22
+ style.textContent = unstyledCss;
23
+ document.head.appendChild(style);
24
+ return () => style.remove();
25
+ });
26
+ const tabs = [
27
+ { id: 'overview', label: 'Overview' },
28
+ { id: 'activity', label: 'Activity' },
29
+ { id: 'settings', label: 'Settings' },
30
+ ];
31
+ </script>
32
+
33
+
34
+
35
+ <div class="workbench">
36
+ <header>
37
+ <div>
38
+ <p class="eyebrow">Styleless mode</p>
39
+ <h4>Layout kept, look removed</h4>
40
+ </div>
41
+ <label class="toggle">
42
+ <input type="checkbox" bind:checked={styleless} />
43
+ Apply unstyled.css
44
+ </label>
45
+ </header>
46
+
47
+ <p class="note">
48
+ With the toggle on, every component below keeps its layout and loses its
49
+ themed look. The page chrome is not a smrt component, so it is unaffected.
50
+ </p>
51
+
52
+ <div class="catalogue">
53
+ <section>
54
+ <h5>Buttons and badges</h5>
55
+ <div class="row">
56
+ <Button>Primary</Button>
57
+ <Button variant="secondary">Secondary</Button>
58
+ <Badge>Draft</Badge>
59
+ <Chip label="Filter" />
60
+ </div>
61
+ </section>
62
+
63
+ <section>
64
+ <h5>Card</h5>
65
+ <Card>
66
+ {#snippet header()}<strong>Workspace</strong>{/snippet}
67
+ <p>Card body text keeps its spacing and flow.</p>
68
+ {#snippet footer()}<Button size="sm">Open</Button>{/snippet}
69
+ </Card>
70
+ </section>
71
+
72
+ <section>
73
+ <h5>Alert</h5>
74
+ <Alert title="Information">The index refreshes in the background.</Alert>
75
+ </section>
76
+
77
+ <section>
78
+ <h5>Form controls</h5>
79
+ <div class="row">
80
+ <Input name="styleless-name" placeholder="Name" />
81
+ <Select name="styleless-role">
82
+ <option value="owner">Owner</option>
83
+ <option value="member">Member</option>
84
+ </Select>
85
+ </div>
86
+ </section>
87
+
88
+ <section>
89
+ <h5>Tabs</h5>
90
+ <Tabs {tabs} {active} onchange={(id) => (active = id)} />
91
+ </section>
92
+ </div>
93
+ </div>
94
+
95
+ <style>
96
+ @layer smrt.reset, smrt.structure, smrt.theme;
97
+ @layer smrt.theme {
98
+ .workbench { display: grid; gap: var(--smrt-spacing-6); color: var(--smrt-color-on-surface); }
99
+ header { display: flex; align-items: center; justify-content: space-between; gap: var(--smrt-spacing-3); padding-bottom: var(--smrt-spacing-4); border-bottom: 1px solid var(--smrt-color-outline-variant); }
100
+ h4, h5, p { margin: 0; }
101
+ h4 { font: var(--smrt-typography-headline-small-font); }
102
+ h5 { margin-bottom: var(--smrt-spacing-3); font: var(--smrt-typography-title-medium-font); }
103
+ .eyebrow { color: var(--smrt-color-primary); font: var(--smrt-typography-label-small-font); letter-spacing: .1em; text-transform: uppercase; }
104
+ .note { color: var(--smrt-color-on-surface-variant); font: var(--smrt-typography-body-medium-font); }
105
+ .toggle { display: inline-flex; align-items: center; gap: var(--smrt-spacing-2); }
106
+ .catalogue { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--smrt-spacing-5); }
107
+ .catalogue > section { display: grid; align-content: start; gap: var(--smrt-spacing-2); }
108
+ .row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--smrt-spacing-3); }
109
+ }
110
+ </style>
@@ -0,0 +1,4 @@
1
+ declare const StylelessPreview: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type StylelessPreview = ReturnType<typeof StylelessPreview>;
3
+ export default StylelessPreview;
4
+ //# sourceMappingURL=StylelessPreview.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StylelessPreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/StylelessPreview.svelte.ts"],"names":[],"mappings":"AA+GA,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../src/svelte/playground.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,wBAyGE"}
1
+ {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../src/svelte/playground.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaA,wBAmHE"}
@@ -6,6 +6,7 @@ const loadCollections = () => import('./playground/CollectionsPreview.svelte');
6
6
  const loadCalendar = () => import('./playground/CalendarPreview.svelte');
7
7
  const loadCapture = () => import('./playground/CapturePreview.svelte');
8
8
  const loadPermissionPicker = () => import('./playground/PermissionPickerPreview.svelte');
9
+ const loadStyleless = () => import('./playground/StylelessPreview.svelte');
9
10
  export default {
10
11
  packageName: '@happyvertical/smrt-ui',
11
12
  displayName: 'UI Foundation',
@@ -100,5 +101,14 @@ export default {
100
101
  tags: ['forms', 'camera', 'signature', 'files'],
101
102
  modes: { mock: { label: 'Interactive' } },
102
103
  },
104
+ {
105
+ id: 'styleless-mode',
106
+ title: 'Styleless Mode',
107
+ description: 'Toggle unstyled.css across the catalogue: components keep layout and drop their themed look, so consumer CSS can restyle them.',
108
+ loadComponent: loadStyleless,
109
+ order: 10,
110
+ tags: ['styling', 'styleless', 'layers', 'restyling'],
111
+ modes: { mock: { label: 'Interactive' } },
112
+ },
103
113
  ],
104
114
  };
@@ -40,3 +40,12 @@ if (typeof HTMLDialogElement !== 'undefined') {
40
40
  };
41
41
  }
42
42
  }
43
+ // jsdom has no Web Animations API. Svelte's `animate:` directive (list FLIP)
44
+ // and transitions call `element.getAnimations()`; an empty list is accurate
45
+ // here because jsdom never runs CSS animations.
46
+ if (typeof Element !== 'undefined' &&
47
+ typeof Element.prototype.getAnimations !== 'function') {
48
+ Element.prototype.getAnimations = function getAnimations() {
49
+ return [];
50
+ };
51
+ }
@@ -200,8 +200,11 @@ $effect(() => {
200
200
  </div>
201
201
 
202
202
  <style>
203
+ @layer smrt.reset, smrt.structure, smrt.theme;
204
+ @layer smrt.theme {
203
205
  .smrt-theme-root {
204
206
  /* Ensure theme variables cascade to all children */
205
207
  display: contents;
206
208
  }
209
+ }
207
210
  </style>
@@ -318,6 +318,46 @@ npm install ./packages/my-brand-theme
318
318
  <ThemeSwitcher variant="segmented" />
319
319
  ```
320
320
 
321
+ ## Required Tokens
322
+
323
+ A theme is complete only when it defines every token the components consume. The list below is generated from the `var(--smrt-*)` references in the packages' `.svelte` files; `src/themes/__tests__/required-tokens.test.ts` asserts every preset and `createTheme()` output defines each one.
324
+
325
+ <!-- smrt-required-tokens:start -->
326
+
327
+ ### Required tokens (generated)
328
+
329
+ Every theme (presets and `createTheme()` output) must define these names. Generated by
330
+ `scripts/generate-smrt-required-tokens.mjs`; do not edit by hand. Component-private
331
+ `--smrt-<component>-*` knobs are excluded.
332
+
333
+ - **color** (40): `--smrt-color-background`, `--smrt-color-error`, `--smrt-color-error-container`, `--smrt-color-inverse-on-surface`, `--smrt-color-inverse-surface`, `--smrt-color-on-background`, `--smrt-color-on-error`, `--smrt-color-on-error-container`, `--smrt-color-on-primary`, `--smrt-color-on-primary-container`, `--smrt-color-on-secondary`, `--smrt-color-on-secondary-container`, `--smrt-color-on-success-container`, `--smrt-color-on-surface`, `--smrt-color-on-surface-variant`, `--smrt-color-on-tertiary`, `--smrt-color-on-tertiary-container`, `--smrt-color-on-warning-container`, `--smrt-color-outline`, `--smrt-color-outline-variant`, `--smrt-color-primary`, `--smrt-color-primary-container`, `--smrt-color-scrim`, `--smrt-color-secondary`, `--smrt-color-secondary-container`, `--smrt-color-shadow`, `--smrt-color-success`, `--smrt-color-success-container`, `--smrt-color-surface`, `--smrt-color-surface-container`, `--smrt-color-surface-container-high`, `--smrt-color-surface-container-highest`, `--smrt-color-surface-container-low`, `--smrt-color-surface-container-lowest`, `--smrt-color-surface-dim`, `--smrt-color-surface-variant`, `--smrt-color-tertiary`, `--smrt-color-tertiary-container`, `--smrt-color-warning`, `--smrt-color-warning-container`
334
+ - **control** (4): `--smrt-control-height`, `--smrt-control-height-small`, `--smrt-control-radius`, `--smrt-control-radius-small`
335
+ - **duration** (6): `--smrt-duration-fast`, `--smrt-duration-medium1`, `--smrt-duration-normal`, `--smrt-duration-short2`, `--smrt-duration-short3`, `--smrt-duration-short4`
336
+ - **easing** (3): `--smrt-easing-emphasized`, `--smrt-easing-emphasized-decelerate`, `--smrt-easing-standard`
337
+ - **elevation** (5): `--smrt-elevation-1`, `--smrt-elevation-2`, `--smrt-elevation-3`, `--smrt-elevation-4`, `--smrt-elevation-5`
338
+ - **focus** (3): `--smrt-focus-ring-color`, `--smrt-focus-ring-offset`, `--smrt-focus-ring-width`
339
+ - **font** (2): `--smrt-font-family`, `--smrt-font-family-mono`
340
+ - **glass** (4): `--smrt-glass-blur`, `--smrt-glass-saturation`, `--smrt-glass-surface`, `--smrt-glass-surface-strong`
341
+ - **hairline** (4): `--smrt-hairline`, `--smrt-hairline-color`, `--smrt-hairline-strong`, `--smrt-hairline-strong-color`
342
+ - **inset** (1): `--smrt-inset-highlight`
343
+ - **radius** (11): `--smrt-radius-2xl`, `--smrt-radius-extra-large`, `--smrt-radius-extra-small`, `--smrt-radius-full`, `--smrt-radius-large`, `--smrt-radius-lg`, `--smrt-radius-md`, `--smrt-radius-medium`, `--smrt-radius-sm`, `--smrt-radius-small`, `--smrt-radius-xl`
344
+ - **scrollbar** (2): `--smrt-scrollbar-thumb`, `--smrt-scrollbar-track`
345
+ - **spacing** (19): `--smrt-spacing-0_5`, `--smrt-spacing-1`, `--smrt-spacing-10`, `--smrt-spacing-11`, `--smrt-spacing-12`, `--smrt-spacing-1_5`, `--smrt-spacing-2`, `--smrt-spacing-2xl`, `--smrt-spacing-3`, `--smrt-spacing-3xl`, `--smrt-spacing-4`, `--smrt-spacing-5`, `--smrt-spacing-6`, `--smrt-spacing-8`, `--smrt-spacing-lg`, `--smrt-spacing-md`, `--smrt-spacing-sm`, `--smrt-spacing-xl`, `--smrt-spacing-xs`
346
+ - **state** (5): `--smrt-state-disabled`, `--smrt-state-dragged`, `--smrt-state-hover`, `--smrt-state-pressed`, `--smrt-state-selected`
347
+ - **surface** (6): `--smrt-surface-control`, `--smrt-surface-ground`, `--smrt-surface-overlay`, `--smrt-surface-panel`, `--smrt-surface-raised`, `--smrt-surface-sunken`
348
+ - **touch** (1): `--smrt-touch-target-min`
349
+ - **typography** (56): `--smrt-typography-body-large-font`, `--smrt-typography-body-large-line-height`, `--smrt-typography-body-large-size`, `--smrt-typography-body-large-tracking`, `--smrt-typography-body-medium-font`, `--smrt-typography-body-medium-line-height`, `--smrt-typography-body-medium-size`, `--smrt-typography-body-medium-tracking`, `--smrt-typography-body-medium-weight`, `--smrt-typography-body-small-font`, `--smrt-typography-body-small-line-height`, `--smrt-typography-body-small-size`, `--smrt-typography-body-small-weight`, `--smrt-typography-display-medium-line-height`, `--smrt-typography-display-medium-size`, `--smrt-typography-display-small-size`, `--smrt-typography-headline-large-line-height`, `--smrt-typography-headline-large-size`, `--smrt-typography-headline-large-weight`, `--smrt-typography-headline-medium-line-height`, `--smrt-typography-headline-medium-size`, `--smrt-typography-headline-medium-weight`, `--smrt-typography-headline-small-font`, `--smrt-typography-headline-small-line-height`, `--smrt-typography-headline-small-size`, `--smrt-typography-headline-small-weight`, `--smrt-typography-label-large-font`, `--smrt-typography-label-large-size`, `--smrt-typography-label-large-tracking`, `--smrt-typography-label-large-weight`, `--smrt-typography-label-medium-font`, `--smrt-typography-label-medium-size`, `--smrt-typography-label-medium-tracking`, `--smrt-typography-label-medium-weight`, `--smrt-typography-label-small-font`, `--smrt-typography-label-small-line-height`, `--smrt-typography-label-small-size`, `--smrt-typography-label-small-tracking`, `--smrt-typography-label-small-weight`, `--smrt-typography-title-large-font`, `--smrt-typography-title-large-line-height`, `--smrt-typography-title-large-size`, `--smrt-typography-title-medium-font`, `--smrt-typography-title-medium-font-family`, `--smrt-typography-title-medium-line-height`, `--smrt-typography-title-medium-size`, `--smrt-typography-title-medium-tracking`, `--smrt-typography-title-medium-weight`, `--smrt-typography-title-small-font`, `--smrt-typography-title-small-size`, `--smrt-typography-title-small-tracking`, `--smrt-typography-title-small-weight`, `--smrt-typography-weight-bold`, `--smrt-typography-weight-medium`, `--smrt-typography-weight-normal`, `--smrt-typography-weight-semibold`
350
+ - **z** (7): `--smrt-z-index-dialog`, `--smrt-z-index-dropdown`, `--smrt-z-index-modal`, `--smrt-z-index-overlay`, `--smrt-z-index-sticky`, `--smrt-z-index-toast`, `--smrt-z-index-tooltip`
351
+
352
+ <!-- smrt-required-tokens:end -->
353
+
354
+ ## Runtime overrides
355
+
356
+ `ThemeProvider` layers user choices over a theme without touching the theme itself:
357
+ `borderRadius` (control radius tokens), `primaryColor` (full derived primary set, also
358
+ available as `derivePrimaryOverrides(color, scheme)`), `density` and `contrast`. See the
359
+ ThemeProvider props table in the README for the exact behaviour.
360
+
321
361
  ## Best Practices
322
362
 
323
363
  1. **Extend a base theme**: Start with `extend: 'material'` or another base to get sensible defaults
@@ -74,6 +74,9 @@ const autoTheme = createThemeFromColor('#6366f1', 'indigo', 'Indigo Theme');
74
74
  registerTheme(autoTheme);
75
75
  ```
76
76
 
77
+ Generated dark themes use tonal primary colors so primary text and icon
78
+ containers remain readable even when the brand seed is very dark.
79
+
77
80
  ### Full Custom Theme with All Options
78
81
 
79
82
  ```typescript
@@ -302,8 +305,29 @@ import '@happyvertical/smrt-ui/themes/styles/happyvertical.css';
302
305
  | `borderRadius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Border radius scale |
303
306
  | `overrides` | `Record<string, string>` | `{}` | Custom CSS variable overrides |
304
307
  | `persist` | `boolean` | `true` | Persist to localStorage |
308
+ | `density` | `'comfortable' \| 'touch'` | - | Default control target size (`--smrt-control-target-min`); persisted |
309
+ | `contrast` | `'auto' \| 'standard' \| 'more'` | `'auto'` | `auto` follows `prefers-contrast: more`; persisted when explicit |
305
310
  | `storageKey` | `string` | `'smrt-theme'` | localStorage key |
306
311
 
312
+ Behaviour notes (state exposed on context: `contrast`, `isChangingScheme`):
313
+
314
+ - `borderRadius` other than `md` is emitted inline as `--smrt-control-radius` /
315
+ `--smrt-control-radius-small` (one scale step smaller for the small radius);
316
+ `md` leaves the preset's own control radius in place. The generic
317
+ `--smrt-radius-*` scale is not changed.
318
+ - `primaryColor` emits the whole derived set for the resolved scheme through
319
+ `derivePrimaryOverrides(color, scheme)` (also exported): `color-primary`, `on-primary`
320
+ (best of white/black), `primary-container`, `on-primary-container`, `inverse-primary`,
321
+ and the focus ring, accent, caret and selection tokens.
322
+ - After the resolved scheme changes post-mount, `data-smrt-theme-changing` is set on
323
+ the root and `<html>` for one `--smrt-duration-scheme`, so styles can crossfade.
324
+ Never on first paint; skipped under `prefers-reduced-motion: reduce`.
325
+ - Contrast: `data-contrast="standard|more|forced"` on `<html>` and the root;
326
+ `forced-colors: active` also sets `data-forced-colors`. Contract for components in
327
+ forced-colors mode: do not paint over system colours, hairlines become `CanvasText`
328
+ and focus rings `Highlight` (component side: #3867 follow-up).
329
+ - `themeScript()` stamps `data-contrast` and (when persisted) `data-density` pre-paint.
330
+
307
331
  ## Theme Switching
308
332
 
309
333
  Use the context to switch themes programmatically:
@@ -605,6 +629,133 @@ All themes expose CSS custom properties:
605
629
  --smrt-easing-standard
606
630
  ```
607
631
 
632
+ ### One vocabulary: `--smrt-*` only
633
+
634
+ `--smrt-*` is the only token namespace. Component CSS consumes these tokens and
635
+ nothing else; the legacy `--color-*`, `--spacing-*`, `--radius-*`, `--md-sys-*`
636
+ families were removed (issue #3822). Three rules follow:
637
+
638
+ - **Consume a token, don't invent one.** Use `var(--smrt-color-primary)` etc. Hard
639
+ colour or spacing literals belong in a theme, not a component.
640
+ - **A component-private variable is `--smrt-<component>-<name>`**, declared on the
641
+ component root (for example `--smrt-toast-viewport-inset`). A parent or app may set
642
+ it. It is not a theme token and should not be read from outside the component.
643
+ - **Overrides go through a theme or a documented hook**, never a `:global` rule or
644
+ `!important`. Unlayered app CSS still wins over the component's own styles.
645
+
646
+ `packages/smrt-ui/src/styles/tokens.css` defines the theme-invariant baseline only
647
+ (spacing, radius, duration, touch targets). Colour and typography come from a theme.
648
+
649
+ The lint gate `scripts/check-smrt-token-vocabulary.mjs` runs in `pnpm lint` and fails
650
+ on any custom property outside `--smrt-*` in `packages/*/src`. The one-time rewrite
651
+ was `scripts/codemod-smrt-tokens.mjs`; it is idempotent, so re-run it after a rebase
652
+ instead of hand-merging renamed names.
653
+
654
+ ### Premium baseline token vocabulary
655
+
656
+ Every preset and every `createTheme()` result defines all of these in both schemes
657
+ (sections of `Theme` in `types.ts`; defaults derived in `derive.ts`; emitted by
658
+ `generateThemeVariables`). Themes and presets define them; **components only consume
659
+ them**. Do not redeclare a theme token on a component root (CSS or `style:`): the
660
+ required-tokens scanner (`scripts/smrt-token-scan-lib.mjs`) treats any `--smrt-*` name
661
+ a component defines on its own root as component-private and drops it from the required
662
+ list repo-wide. A per-variant override uses a component-prefixed name that falls back to
663
+ the theme token, for example `background: color-mix(in srgb, currentColor
664
+ calc(var(--smrt-button-state-hover, var(--smrt-state-hover)) * 100%), transparent)`.
665
+
666
+ | Token | Meaning | Consumed by |
667
+ | --- | --- | --- |
668
+ | `--smrt-surface-ground` | Page ground | `body`/shell background |
669
+ | `--smrt-surface-panel` | Cards, sidebars, panels on the ground | `background` of Card, sidebars, header, footer |
670
+ | `--smrt-surface-control` | Input and control fill | `background` of Input, Select, Textarea, Button (outlined/tonal) |
671
+ | `--smrt-surface-raised` | Menus, popovers, dialogs above panels | `background` of overlays |
672
+ | `--smrt-surface-sunken` | Wells and recessed regions | `background` of tables headers, code, inset areas |
673
+ | `--smrt-surface-overlay` | Modal scrim | `background` of Modal/Drawer backdrops |
674
+ | `--smrt-hairline-color` / `-strong-color` | Quiet and strong edge colours | `border-color`, `outline-color`, dividers |
675
+ | `--smrt-hairline` / `--smrt-hairline-strong` | `1px solid` edge shorthands | `border`, `border-top` etc. on panels (quiet) and controls (strong) |
676
+ | `--smrt-inset-highlight` | Top inner light line (`none` on light) | `box-shadow` (layer with an elevation) on raised dark surfaces |
677
+ | `--smrt-state-{hover,pressed,selected,focus,dragged,disabled}` | Unitless state-layer opacities, 0 to 1 | Mix percentage of a state overlay (`calc(var(--smrt-state-hover) * 100%)`); `disabled` as `opacity` |
678
+ | `--smrt-shadow-color` | `rgb(r g b)` shadow base, per scheme | `box-shadow` colour: `rgb(from var(--smrt-shadow-color) r g b / 0.2)` |
679
+ | `--smrt-elevation-0..5` | Existing shadow scale | `box-shadow` |
680
+ | `--smrt-control-radius` / `-radius-small` | Control corner radii | `border-radius` of controls |
681
+ | `--smrt-control-height` / `-height-small` | Control heights | `min-height`/`block-size` of controls |
682
+ | `--smrt-control-weight` | Control label weight | `font-weight` of buttons and inputs |
683
+ | `--smrt-control-border` | Control edge shorthand (`var(--smrt-hairline-strong)`) | `border` of controls |
684
+ | `--smrt-control-target-min` | Density-driven minimum hit size (set by ThemeProvider `density`) | `min-height`/`min-width` of interactive controls |
685
+ | `--smrt-control-hit-size` | Hit area of small icon controls | `inline-size`/`block-size` of icon buttons |
686
+ | `--smrt-focus-ring-{color,width,offset}` | Focus indicator | `outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color)`, `outline-offset` on `:focus-visible` |
687
+ | `--smrt-accent-color` | Native control accent | `accent-color` (checkbox, radio, range, date pickers) |
688
+ | `--smrt-selection-{background,color}` | Text selection | `::selection` |
689
+ | `--smrt-caret-color` | Text caret | `caret-color` |
690
+ | `--smrt-scrollbar-{thumb,track}` | Scrollbar colours | `scrollbar-color: thumb track` |
691
+ | `--smrt-glass-blur` / `-saturation` | Backdrop filter amounts (`0px`/`100%` outside Glass) | `backdrop-filter: blur(var(--smrt-glass-blur)) saturate(var(--smrt-glass-saturation))` |
692
+ | `--smrt-glass-surface` / `-surface-strong` | Translucent (Glass) or opaque surface fills; opacity is part of the colour | `background` of floating chrome and overlays |
693
+ | `--smrt-glass-border` | Glass edge colour | `border-color` of glass surfaces |
694
+ | `--smrt-duration-scheme` | Scheme crossfade duration | `transition-duration` when the colour scheme changes |
695
+ | `--smrt-font-feature-settings` | OpenType features (`"tnum" 1, "cv11" 1` on Inter presets) | `font-feature-settings` on the root and numeric cells |
696
+
697
+ #### Semantic info colour
698
+
699
+ `ColorPalette` carries an info family beside success, warning and error:
700
+ `--smrt-color-info`, `--smrt-color-on-info`, `--smrt-color-info-container` and
701
+ `--smrt-color-on-info-container`. Presets without a distinct information hue define
702
+ it as their tertiary family (Material, Glass, Studio, Day Shift); **smrt** uses a
703
+ steel blue (`#2c6a96` light, `#6fa8d6` dark) so Information no longer shares amber
704
+ with Warning. `createTheme()` follows a supplied `tertiary` unless `info` is also
705
+ given. Components that show information status (for example `Alert` `variant="info"`)
706
+ should consume these, not tertiary.
707
+
708
+ #### Preset identities on the baseline
709
+
710
+ | Preset | Dark ladder (ground / panel / control / raised) | Depth | State layers (hover / pressed / selected) | Controls |
711
+ | --- | --- | --- | --- | --- |
712
+ | Material | `#121316` / `#1b1c20` / `#26282c` / `#34363a` (M3 tonal, no borders needed; control border is transparent) | contact + ambient; dark adds the inset highlight | .08 / .12 / .12 | radius md / sm, 2.75rem |
713
+ | Glass | `#0b0b0f` ground; panel 72% and popover 88% frosted glass, input a recessed 28% black well; blur 20px, saturation 180% | softest, widest ambient | .10 / .14 / .14 | radius lg / md, 2.75rem |
714
+ | Studio | `#131416` / `#131416` / `#1c1d20` / `#292a2d` (flat, crisp hairlines, panel equals ground) | elevation 1 is the hairline; tight shadows | .06 / .10 / .10 | radius 0.375rem / sm, 2.5rem |
715
+ | smrt | `#0b0c0e` / `#121417` / `#181b1f` / `#1d2126` (instrument dark, amber signal, steel-blue info) | elevation 1 is the hairline; calm two-layer shadows (no far-spread) | .10 / .16 / .14 | radius sm, 2.5rem |
716
+ | Day Shift | enamel ground, faceplate panel, control a hair lighter, raised = faceplate; hairlines are the bezel | elevation 1 is the bezel hairline; minimal | .06 / .10 / .10 | radius md (6px) / sm, 2.5rem |
717
+
718
+ Elevation 2 to 5 are two-layer shadows built with `shadow()` in `shared.ts`: a contact
719
+ layer (`0 1px 2px`) plus an ambient layer (`0 Npx Mpx` with negative spread), both
720
+ `color-mix`ed from `--smrt-shadow-color`. Dark schemes prepend
721
+ `var(--smrt-inset-highlight)`. Weight is 500 on every preset. Native chrome follows the
722
+ baseline: accent and caret are primary, selection is the primary container, and the
723
+ scrollbar thumb is the strong hairline over a transparent track.
724
+
725
+ `--smrt-glass-border-opacity` and `--smrt-glass-background-opacity` are deprecated: still emitted (from `GlassEffects.borderOpacity`/`backgroundOpacity`, `1` when unset) for one release and removed in the next minor; the former
726
+ tokens, and the ThemeProvider-local `--smrt-glass-surface`/`--smrt-glass-border`,
727
+ were folded into the glass surface colours above.
728
+
729
+ ### Typography
730
+
731
+ `themes/styles/base.css` (import once at the app entry) maps headings to type roles
732
+ inside `@layer smrt.structure`, scoped under `body` and `.smrt-theme-root`, so presets'
733
+ display faces reach plain markup. Hosts restyle with unlayered CSS.
734
+
735
+ | Element | Role |
736
+ | --- | --- |
737
+ | `h1` | `display-small` |
738
+ | `h2` | `headline-large` |
739
+ | `h3` | `headline-medium` |
740
+ | `h4` | `headline-small` |
741
+ | `h5` | `title-large` |
742
+ | `h6` | `title-medium` |
743
+
744
+ Each heading takes `font-family`, `font-size`, `line-height`, `font-weight` and
745
+ `letter-spacing` from the role's typography tokens (for example `--smrt-typography-headline-large-size`), and a `margin-block` from
746
+ `--smrt-spacing-*` (first children lose the top margin). Utilities
747
+ `.smrt-{display,headline,title,body,label}-{large,medium,small}` apply the same five
748
+ properties to any element.
749
+
750
+ Tabular figures: the root sets `font-feature-settings: var(--smrt-font-feature-settings)`,
751
+ and `table`, `time`, `[data-smrt-component="DataTable"]`, `[data-smrt-component="Badge"]`
752
+ and `.smrt-tabular` set `font-variant-numeric: tabular-nums`.
753
+
754
+ Preset rhythm: display tracking tightens with size (about -0.02em at display, -0.01em at
755
+ headline, 0 at title), labels weigh 500 (Day Shift keeps its documented bold B612 labels),
756
+ body is 400, and display plus headline roles use the preset's display face where it has one
757
+ (smrt: Space Grotesk; Day Shift: its documented stack).
758
+
608
759
  ## Advanced Usage
609
760
 
610
761
  ### Build-Time Theming