@happyvertical/smrt-ui 0.55.20 → 0.55.22

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 (67) hide show
  1. package/dist/components/calendar/DayView.svelte +1 -1
  2. package/dist/components/data/CollectionList.svelte +1 -1
  3. package/dist/components/data/DataTable.svelte +5 -0
  4. package/dist/components/data/ListSortSelect.svelte +1 -1
  5. package/dist/components/feedback/Alert.svelte +86 -20
  6. package/dist/components/feedback/LoadingOverlay.svelte +2 -2
  7. package/dist/components/feedback/PhoneSheet.svelte +1 -1
  8. package/dist/components/feedback/Progress.svelte +7 -0
  9. package/dist/components/feedback/ToastViewport.svelte +1 -1
  10. package/dist/components/feedback/WorkingStrip.svelte +4 -4
  11. package/dist/components/forms/CameraCapture.svelte +1 -1
  12. package/dist/components/forms/Checkbox.svelte +133 -30
  13. package/dist/components/forms/Combobox.svelte +2 -2
  14. package/dist/components/forms/Fieldset.svelte +1 -1
  15. package/dist/components/forms/FilePicker.svelte +1 -1
  16. package/dist/components/forms/FormActionBar.svelte +2 -2
  17. package/dist/components/forms/Input.svelte +66 -61
  18. package/dist/components/forms/InputGroup.svelte +1 -1
  19. package/dist/components/forms/Listbox.svelte +1 -1
  20. package/dist/components/forms/MultiSelect.svelte +2 -2
  21. package/dist/components/forms/Radio.svelte +9 -0
  22. package/dist/components/forms/RangeSlider.svelte +1 -1
  23. package/dist/components/forms/SegmentedControl.svelte +7 -2
  24. package/dist/components/forms/Select.svelte +67 -61
  25. package/dist/components/forms/SignaturePad.svelte +1 -1
  26. package/dist/components/forms/Slider.svelte +1 -1
  27. package/dist/components/forms/StagedControlReview.svelte +4 -4
  28. package/dist/components/forms/Switch.svelte +130 -28
  29. package/dist/components/forms/TagsInput.svelte +1 -1
  30. package/dist/components/forms/Toggle.svelte +12 -0
  31. package/dist/components/forms/ToggleButton.svelte +6 -1
  32. package/dist/components/layout/Footer.svelte +1 -1
  33. package/dist/components/layout/Masthead.svelte +1 -1
  34. package/dist/components/memberships/MembershipCard.svelte +3 -3
  35. package/dist/components/nav/FilterChips.svelte +6 -1
  36. package/dist/components/nav/Tabs.svelte +192 -183
  37. package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
  38. package/dist/components/permissions/PermissionPicker.svelte +4 -4
  39. package/dist/components/roles/RoleSelector.svelte +2 -2
  40. package/dist/components/ui/Accordion.svelte +1 -1
  41. package/dist/components/ui/AccordionItem.svelte +1 -1
  42. package/dist/components/ui/Badge.svelte +26 -24
  43. package/dist/components/ui/Button.svelte +101 -115
  44. package/dist/components/ui/Button.svelte.d.ts +1 -1
  45. package/dist/components/ui/Card.svelte +44 -57
  46. package/dist/components/ui/Chip.svelte +68 -58
  47. package/dist/components/ui/Disclosure.svelte +1 -1
  48. package/dist/components/ui/IconToggle.svelte +8 -2
  49. package/dist/styles/unstyled.css +2 -2
  50. package/dist/svelte/playground/BaseControlsPreview.svelte +2 -2
  51. package/dist/svelte/playground/CollectionsPreview.svelte +1 -1
  52. package/dist/svelte/playground/DataTablePreview.svelte +5 -5
  53. package/dist/svelte/playground/FeedbackPreview.svelte +1 -1
  54. package/dist/svelte/playground/InteractiveControlsPreview.svelte +1 -1
  55. package/dist/svelte/playground/StylelessPreview.svelte +35 -2
  56. package/dist/svelte/playground/StylelessPreview.svelte.d.ts.map +1 -1
  57. package/dist/themes/README.md +58 -4
  58. package/dist/themes/ThemeProvider.svelte +9 -0
  59. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
  60. package/dist/themes/components/ColorSchemeToggle.svelte +3 -3
  61. package/dist/themes/components/ThemeSwitcher.svelte +3 -3
  62. package/dist/themes/forced-colors.d.ts +2 -0
  63. package/dist/themes/forced-colors.d.ts.map +1 -0
  64. package/dist/themes/forced-colors.js +49 -0
  65. package/dist/themes/styles/base.css +80 -1
  66. package/dist/themes/styles/forced-colors.css +42 -0
  67. package/package.json +2 -2
@@ -45,16 +45,16 @@ const {
45
45
  </script>
46
46
 
47
47
  <span
48
- class="chip {size}"
49
- class:selected
50
- class:disabled
51
- class:selectable
52
- class:closeable data-smrt-component="Chip"
48
+ class="smrt-chip__root smrt-chip__{size}"
49
+ class:smrt-chip__selected={selected}
50
+ class:smrt-chip__disabled={disabled}
51
+ class:smrt-chip__selectable={selectable}
52
+ class:smrt-chip__closeable={closeable} data-smrt-component="Chip"
53
53
  >
54
54
  {#if selectable}
55
55
  <button
56
56
  type="button"
57
- class="chip__body"
57
+ class="smrt-chip__body" data-smrt-part="body"
58
58
  aria-pressed={selected}
59
59
  {disabled}
60
60
  onclick={() => onselect?.()}
@@ -62,7 +62,7 @@ const {
62
62
  {#if children}{@render children()}{:else}{label}{/if}
63
63
  </button>
64
64
  {:else}
65
- <span class="chip__body">
65
+ <span class="smrt-chip__body" data-smrt-part="body">
66
66
  {#if children}{@render children()}{:else}{label}{/if}
67
67
  </span>
68
68
  {/if}
@@ -70,7 +70,7 @@ const {
70
70
  {#if closeable}
71
71
  <button
72
72
  type="button"
73
- class="chip__close"
73
+ class="smrt-chip__close" data-smrt-part="close"
74
74
  aria-label={label ? `Remove ${label}` : 'Remove'}
75
75
  {disabled}
76
76
  onclick={() => onclose?.()}
@@ -94,101 +94,111 @@ const {
94
94
 
95
95
  <style>
96
96
  @layer smrt.reset, smrt.structure, smrt.theme;
97
- @layer smrt.theme {
98
- .chip {
97
+ @layer smrt.structure {
98
+ .smrt-chip__root {
99
99
  position: relative;
100
100
  display: inline-flex;
101
101
  align-items: center;
102
102
  gap: var(--smrt-spacing-1);
103
+ max-width: 100%;
104
+ }
105
+ .smrt-chip__root::after {
106
+ content: '';
107
+ position: absolute;
108
+ inset: 0;
109
+ pointer-events: none;
110
+ }
111
+ .smrt-chip__root.smrt-chip__sm {
112
+ padding: 0 var(--smrt-spacing-1);
113
+ }
114
+ .smrt-chip__root.smrt-chip__md {
115
+ min-block-size: var(--smrt-control-height-small);
116
+ padding: 0 var(--smrt-spacing-2);
117
+ }
118
+ .smrt-chip__root.smrt-chip__disabled {
119
+ pointer-events: none;
120
+ }
121
+ .smrt-chip__body {
122
+ padding: var(--smrt-spacing-1) var(--smrt-spacing-1);
123
+ cursor: default;
124
+ white-space: nowrap;
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ }
128
+ .smrt-chip__root.smrt-chip__selectable .smrt-chip__body {
129
+ cursor: pointer;
130
+ }
131
+ .smrt-chip__close {
132
+ display: inline-flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ cursor: pointer;
136
+ padding: var(--smrt-spacing-1);
137
+ }
138
+ }
139
+ @layer smrt.theme {
140
+ .smrt-chip__root {
103
141
  border-radius: var(--smrt-control-radius);
104
142
  background: var(--smrt-surface-control);
105
143
  color: var(--smrt-color-on-surface);
106
144
  border: var(--smrt-hairline);
107
145
  font-weight: var(--smrt-control-weight);
108
- max-width: 100%;
109
146
  }
110
-
111
- /* State layer: currentColor mixed at the theme's state opacity. */
112
- .chip::after {
113
- content: '';
114
- position: absolute;
115
- inset: 0;
147
+ .smrt-chip__root::after {
116
148
  border-radius: inherit;
117
149
  background: transparent;
118
- pointer-events: none;
119
150
  }
120
-
121
- .chip.selectable:hover::after {
151
+ .smrt-chip__root.smrt-chip__selectable:hover::after {
122
152
  background: color-mix(in srgb, currentColor calc(var(--smrt-state-hover) * 100%), transparent);
123
153
  }
124
-
125
- .chip.selectable:active::after {
154
+ .smrt-chip__root.smrt-chip__selectable:active::after {
126
155
  background: color-mix(in srgb, currentColor calc(var(--smrt-state-pressed) * 100%), transparent);
127
156
  }
128
-
129
- .chip.sm {
130
- padding: 0 var(--smrt-spacing-1);
131
- font-size: var(--smrt-typography-label-small-size, 0.6875rem);
157
+ .smrt-chip__root.smrt-chip__sm {
158
+ font-size: var(--smrt-typography-label-small-size);
132
159
  }
133
- .chip.sm {
160
+ .smrt-chip__root.smrt-chip__sm {
134
161
  border-radius: var(--smrt-control-radius-small);
135
162
  }
136
- .chip.md {
137
- min-block-size: var(--smrt-control-height-small);
138
- padding: 0 var(--smrt-spacing-2);
139
- font-size: var(--smrt-typography-label-large-size, 0.875rem);
163
+ .smrt-chip__root.smrt-chip__md {
164
+ font-size: var(--smrt-typography-label-large-size);
140
165
  }
141
-
142
- .chip.selected {
166
+ .smrt-chip__root.smrt-chip__selected {
143
167
  background: var(--smrt-color-primary-container);
144
168
  color: var(--smrt-color-on-primary-container);
145
169
  border-color: var(--smrt-color-primary);
146
170
  }
147
-
148
- .chip.disabled {
171
+ .smrt-chip__root.smrt-chip__disabled {
149
172
  opacity: var(--smrt-state-disabled);
150
- pointer-events: none;
151
173
  }
152
-
153
- .chip__body {
174
+ .smrt-chip__body {
154
175
  appearance: none;
155
176
  background: none;
156
177
  border: none;
157
178
  color: inherit;
158
179
  font: inherit;
159
- padding: var(--smrt-spacing-1) var(--smrt-spacing-1);
160
- cursor: default;
161
- white-space: nowrap;
162
- overflow: hidden;
163
- text-overflow: ellipsis;
164
180
  }
165
-
166
- .chip.selectable .chip__body {
167
- cursor: pointer;
168
- }
169
-
170
- .chip__close {
181
+ .smrt-chip__close {
171
182
  appearance: none;
172
- display: inline-flex;
173
- align-items: center;
174
- justify-content: center;
175
183
  background: none;
176
184
  border: none;
177
185
  color: inherit;
178
- cursor: pointer;
179
- padding: var(--smrt-spacing-1);
180
186
  border-radius: var(--smrt-radius-full);
181
187
  opacity: 0.7;
182
188
  }
183
- .chip__close:hover {
189
+ .smrt-chip__close:hover {
184
190
  opacity: 1;
185
191
  background: color-mix(in srgb, currentColor calc(var(--smrt-state-hover) * 100%), transparent);
186
192
  }
187
-
188
- .chip__body:focus-visible,
189
- .chip__close:focus-visible {
193
+ .smrt-chip__body:focus-visible,
194
+ .smrt-chip__close:focus-visible {
190
195
  outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color);
191
196
  outline-offset: var(--smrt-focus-ring-offset);
192
197
  }
198
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
199
+ @media (forced-colors: active) {
200
+ .smrt-chip__root.smrt-chip__selected { border-color: Highlight; outline: var(--smrt-focus-ring-width) solid Highlight; outline-offset: calc(-1 * var(--smrt-focus-ring-width)); }
201
+ .smrt-chip__root.smrt-chip__disabled, .smrt-chip__root.smrt-chip__selected.smrt-chip__disabled { opacity: 1; color: GrayText; border-color: GrayText; outline-color: GrayText; }
202
+ }
193
203
  }
194
204
  </style>
@@ -46,7 +46,7 @@ function handleToggle(event: Event) {
46
46
  <style>
47
47
  @layer smrt.reset, smrt.structure, smrt.theme;
48
48
  @layer smrt.theme {
49
- .disclosure { border-bottom: 1px solid var(--smrt-color-outline-variant); color: var(--smrt-color-on-surface); }
49
+ .disclosure { border-bottom: var(--smrt-hairline); color: var(--smrt-color-on-surface); }
50
50
  summary { display: flex; justify-content: space-between; gap: var(--smrt-spacing-3); padding: var(--smrt-spacing-3) 0; font: var(--smrt-typography-title-small-font); cursor: pointer; list-style: none; }
51
51
  summary::-webkit-details-marker { display: none; }
52
52
  summary:focus-visible { outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color); outline-offset: var(--smrt-focus-ring-offset); }
@@ -79,7 +79,7 @@ const name = $derived(count === undefined ? label : `${label} (${count})`);
79
79
  min-width: 2.75rem;
80
80
  min-height: 2.75rem;
81
81
  padding: 0;
82
- border: 1px solid var(--smrt-color-outline-variant, var(--smrt-color-outline));
82
+ border: var(--smrt-hairline);
83
83
  border-radius: var(--smrt-radius-full);
84
84
  background: var(--smrt-color-surface-container, var(--smrt-color-surface));
85
85
  color: var(--smrt-color-on-surface);
@@ -129,7 +129,7 @@ const name = $derived(count === undefined ? label : `${label} (${count})`);
129
129
  /* Outlined, not filled: a surface-coloured face (so the button's edge does
130
130
  not show through where the badge overlaps it), a 1px theme outline and
131
131
  neutral text, readable on light and dark themes alike. */
132
- border: 1px solid var(--smrt-color-outline);
132
+ border: var(--smrt-hairline-strong);
133
133
  background: var(--smrt-color-surface);
134
134
  color: var(--smrt-color-on-surface-variant);
135
135
  font-size: 0.68rem;
@@ -138,5 +138,11 @@ const name = $derived(count === undefined ? label : `${label} (${count})`);
138
138
  text-align: center;
139
139
  font-variant-numeric: tabular-nums;
140
140
  }
141
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
142
+ @media (forced-colors: active) {
143
+ .icon-toggle.pressed { background: Highlight; color: HighlightText; border-color: Highlight; }
144
+ .icon-toggle.pressed:not(:focus-visible) { outline: var(--smrt-focus-ring-width) solid CanvasText; outline-offset: calc(-2 * var(--smrt-focus-ring-width)); }
145
+ .icon-toggle:disabled, .icon-toggle.pressed:disabled { opacity: 1; background: Canvas; color: GrayText; border-color: GrayText; }
146
+ }
141
147
  }
142
148
  </style>
@@ -10,8 +10,8 @@
10
10
  * CSS, the --smrt-* tokens, or the data-smrt-component / data-smrt-part hooks.
11
11
  *
12
12
  * The reverted property list is explicit on purpose. `all: revert-layer` would
13
- * also drop layout declarations, because until per-package structure/theme
14
- * sorting lands every component rule sits in smrt.theme. Keep this list in
13
+ * also drop layout from unsorted components, including those nested inside
14
+ * the ten wave-2 pilot primitives. Keep this list in
15
15
  * step with the "Styleless mode" section of agents/styling.md.
16
16
  */
17
17
  @layer smrt.reset, smrt.structure, smrt.theme;
@@ -133,7 +133,7 @@ function handleSubmit() {
133
133
 
134
134
  .workbench__header {
135
135
  padding-bottom: var(--smrt-spacing-4);
136
- border-bottom: 1px solid var(--smrt-color-outline-variant);
136
+ border-bottom: var(--smrt-hairline);
137
137
  }
138
138
 
139
139
  h4,
@@ -193,7 +193,7 @@ function handleSubmit() {
193
193
  .actions {
194
194
  align-items: flex-start;
195
195
  padding-top: var(--smrt-spacing-5);
196
- border-top: 1px solid var(--smrt-color-outline-variant);
196
+ border-top: var(--smrt-hairline);
197
197
  }
198
198
 
199
199
  .save-status {
@@ -55,7 +55,7 @@ const filtered = $derived(
55
55
  @layer smrt.reset, smrt.structure, smrt.theme;
56
56
  @layer smrt.theme {
57
57
  .workbench { display: grid; gap: var(--smrt-spacing-4); color: var(--smrt-color-on-surface); }
58
- header { padding-bottom: var(--smrt-spacing-4); border-bottom: 1px solid var(--smrt-color-outline-variant); } h4, p { margin: 0; } h4 { font: var(--smrt-typography-headline-small-font); } header > p:last-child { max-width: 48rem; margin-top: var(--smrt-spacing-1); color: var(--smrt-color-on-surface-variant); }
58
+ header { padding-bottom: var(--smrt-spacing-4); border-bottom: var(--smrt-hairline); } h4, p { margin: 0; } h4 { font: var(--smrt-typography-headline-small-font); } header > p:last-child { max-width: 48rem; margin-top: var(--smrt-spacing-1); color: var(--smrt-color-on-surface-variant); }
59
59
  .eyebrow { color: var(--smrt-color-primary); font: var(--smrt-typography-label-small-font); letter-spacing: .1em; text-transform: uppercase; }
60
60
  .status { padding: var(--smrt-spacing-1) var(--smrt-spacing-2); border-radius: var(--smrt-radius-full); background: var(--smrt-color-secondary-container); color: var(--smrt-color-on-secondary-container); font: var(--smrt-typography-label-small-font); }
61
61
  }
@@ -514,7 +514,7 @@ function selectAllMatching() {
514
514
  justify-content: space-between;
515
515
  gap: var(--smrt-spacing-4);
516
516
  padding-bottom: var(--smrt-spacing-4);
517
- border-bottom: 1px solid var(--smrt-color-outline-variant);
517
+ border-bottom: var(--smrt-hairline);
518
518
  }
519
519
 
520
520
  h4,
@@ -561,7 +561,7 @@ function selectAllMatching() {
561
561
  .table-frame {
562
562
  overflow: auto;
563
563
  max-width: 100%;
564
- border: 1px solid var(--smrt-color-outline-variant);
564
+ border: var(--smrt-hairline);
565
565
  border-radius: var(--smrt-radius-medium);
566
566
  }
567
567
 
@@ -569,14 +569,14 @@ function selectAllMatching() {
569
569
  display: grid;
570
570
  gap: var(--smrt-spacing-3);
571
571
  padding-top: var(--smrt-spacing-4);
572
- border-top: 1px solid var(--smrt-color-outline-variant);
572
+ border-top: var(--smrt-hairline);
573
573
  }
574
574
 
575
575
  .report-fixture {
576
576
  display: grid;
577
577
  gap: var(--smrt-spacing-3);
578
578
  padding-top: var(--smrt-spacing-4);
579
- border-top: 1px solid var(--smrt-color-outline-variant);
579
+ border-top: var(--smrt-hairline);
580
580
  }
581
581
 
582
582
  .manual-fixture,
@@ -585,7 +585,7 @@ function selectAllMatching() {
585
585
  display: grid;
586
586
  gap: var(--smrt-spacing-3);
587
587
  padding-top: var(--smrt-spacing-4);
588
- border-top: 1px solid var(--smrt-color-outline-variant);
588
+ border-top: var(--smrt-hairline);
589
589
  }
590
590
 
591
591
  .table-frame--virtual {
@@ -41,7 +41,7 @@ const menu = [
41
41
  @layer smrt.reset, smrt.structure, smrt.theme;
42
42
  @layer smrt.theme {
43
43
  .workbench { display: grid; gap: var(--smrt-spacing-6); color: var(--smrt-color-on-surface); }
44
- header, .actions, .interaction-row { display: flex; align-items: center; gap: var(--smrt-spacing-3); } header { justify-content: space-between; padding-bottom: var(--smrt-spacing-4); border-bottom: 1px solid var(--smrt-color-outline-variant); }
44
+ header, .actions, .interaction-row { display: flex; align-items: center; gap: var(--smrt-spacing-3); } header { justify-content: space-between; padding-bottom: var(--smrt-spacing-4); border-bottom: var(--smrt-hairline); }
45
45
  h4, h5, p { margin: 0; } h4 { font: var(--smrt-typography-headline-small-font); } h5 { margin-bottom: var(--smrt-spacing-3); font: var(--smrt-typography-title-medium-font); }
46
46
  .eyebrow { color: var(--smrt-color-primary); font: var(--smrt-typography-label-small-font); letter-spacing: .1em; text-transform: uppercase; }
47
47
  .stack { display: grid; gap: var(--smrt-spacing-2); }
@@ -118,7 +118,7 @@ const identity = (controlId: string) => ({
118
118
  @layer smrt.reset, smrt.structure, smrt.theme;
119
119
  @layer smrt.theme {
120
120
  .workbench { display: grid; gap: var(--smrt-spacing-5); color: var(--smrt-color-on-surface); }
121
- header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--smrt-spacing-5); padding-bottom: var(--smrt-spacing-4); border-bottom: 1px solid var(--smrt-color-outline-variant); }
121
+ header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--smrt-spacing-5); padding-bottom: var(--smrt-spacing-4); border-bottom: var(--smrt-hairline); }
122
122
  h4, h5, p { margin: 0; } h4 { font: var(--smrt-typography-headline-small-font); } h5 { margin-bottom: var(--smrt-spacing-3); font: var(--smrt-typography-title-medium-font); }
123
123
  header p:not(.eyebrow) { max-width: 40rem; color: var(--smrt-color-on-surface-variant); }
124
124
  .eyebrow { color: var(--smrt-color-primary); font: var(--smrt-typography-label-small-font); letter-spacing: .1em; text-transform: uppercase; }
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import Alert from '../../components/feedback/Alert.svelte';
3
+ import Checkbox from '../../components/forms/Checkbox.svelte';
3
4
  import Input from '../../components/forms/Input.svelte';
4
5
  import Select from '../../components/forms/Select.svelte';
6
+ import Switch from '../../components/forms/Switch.svelte';
5
7
  import Tabs from '../../components/nav/Tabs.svelte';
6
8
  import Badge from '../../components/ui/Badge.svelte';
7
9
  import Button from '../../components/ui/Button.svelte';
@@ -11,6 +13,30 @@ import unstyledCss from '../../styles/unstyled.css?inline';
11
13
 
12
14
  // Native checkbox, outside the catalogue, so the toggle itself is never stripped.
13
15
  let styleless = $state(false);
16
+ let allProperties = $state(false);
17
+ // Experimental wave-2 reset. Keep production unstyled.css on its explicit
18
+ // property list until unsorted components nested inside these roots migrate.
19
+ const pilotRoots = [
20
+ 'Button',
21
+ 'Badge',
22
+ 'Chip',
23
+ 'Card',
24
+ 'Alert',
25
+ 'Input',
26
+ 'Select',
27
+ 'Checkbox',
28
+ 'Switch',
29
+ 'Tabs',
30
+ ]
31
+ .map((name) => `[data-smrt-component="${name}"]`)
32
+ .join(', ');
33
+ const pilotScope = `:is(${pilotRoots})`;
34
+ const pilotSelector = `:is(${pilotScope}, ${pilotScope} *):not(#\\#):not(#\\#):not(#\\#)`;
35
+ const pilotCss = `@layer smrt.theme {
36
+ ${pilotSelector}, ${pilotSelector}::before, ${pilotSelector}::after {
37
+ all: revert-layer;
38
+ }
39
+ }`;
14
40
  let active = $state('overview');
15
41
 
16
42
  // Svelte does not interpolate text inside a style element, so the stylesheet is
@@ -19,7 +45,7 @@ $effect(() => {
19
45
  if (!styleless) return;
20
46
  const style = document.createElement('style');
21
47
  style.setAttribute('data-smrt-styleless', '');
22
- style.textContent = unstyledCss;
48
+ style.textContent = unstyledCss + (allProperties ? pilotCss : '');
23
49
  document.head.appendChild(style);
24
50
  return () => style.remove();
25
51
  });
@@ -49,6 +75,11 @@ const tabs = [
49
75
  themed look. The page chrome is not a smrt component, so it is unaffected.
50
76
  </p>
51
77
 
78
+ <label class="toggle">
79
+ <input type="checkbox" bind:checked={allProperties} />
80
+ Try all: revert-layer on the ten pilot primitives
81
+ </label>
82
+
52
83
  <div class="catalogue">
53
84
  <section>
54
85
  <h5>Buttons and badges</h5>
@@ -82,6 +113,8 @@ const tabs = [
82
113
  <option value="owner">Owner</option>
83
114
  <option value="member">Member</option>
84
115
  </Select>
116
+ <Checkbox label="Send updates" />
117
+ <Switch label="Enable alerts" />
85
118
  </div>
86
119
  </section>
87
120
 
@@ -96,7 +129,7 @@ const tabs = [
96
129
  @layer smrt.reset, smrt.structure, smrt.theme;
97
130
  @layer smrt.theme {
98
131
  .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); }
132
+ header { display: flex; align-items: center; justify-content: space-between; gap: var(--smrt-spacing-3); padding-bottom: var(--smrt-spacing-4); border-bottom: var(--smrt-hairline); }
100
133
  h4, h5, p { margin: 0; }
101
134
  h4 { font: var(--smrt-typography-headline-small-font); }
102
135
  h5 { margin-bottom: var(--smrt-spacing-3); font: var(--smrt-typography-title-medium-font); }
@@ -1 +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
+ {"version":3,"file":"StylelessPreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/StylelessPreview.svelte.ts"],"names":[],"mappings":"AAkJA,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -323,11 +323,65 @@ Behaviour notes (state exposed on context: `contrast`, `isChangingScheme`):
323
323
  the root and `<html>` for one `--smrt-duration-scheme`, so styles can crossfade.
324
324
  Never on first paint; skipped under `prefers-reduced-motion: reduce`.
325
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).
326
+ `forced-colors: active` also sets `data-forced-colors`. See [Forced colors](#forced-colors).
329
327
  - `themeScript()` stamps `data-contrast` and (when persisted) `data-density` pre-paint.
330
328
 
329
+ ## Forced colors
330
+
331
+ Windows High Contrast (`forced-colors: active`) replaces author colours with the user's
332
+ system palette. The theme layer and the components cooperate so nothing paints over it.
333
+
334
+ **Token level** (`styles/base.css`, mirrored by `FORCED_COLORS_TOKENS` in
335
+ `forced-colors.ts`). Under `@media (forced-colors: active)` and `[data-forced-colors]`
336
+ (stamped by `ThemeProvider` and `themeScript()` only while the media query matches; a test
337
+ can set it without emulation) the theme tokens below are redefined. Surface, text and
338
+ primary colours are not mapped: in forced-colors mode the browser already replaces them
339
+ with the system palette, so the map covers only what it cannot see through (shadows,
340
+ blur, translucent state layers, custom-property colours used outside colour properties):
341
+
342
+ | Token | Forced value |
343
+ | --- | --- |
344
+ | `--smrt-hairline-color` / `-strong-color` and the `--smrt-hairline*` shorthands | `CanvasText` |
345
+ | `--smrt-focus-ring-color` / `-width` / `-offset` | `Highlight` / `3px` / `2px` |
346
+ | `--smrt-inset-highlight`, `--smrt-elevation-0..5` | transparent shadows (never `none`: components compose `box-shadow` lists) |
347
+ | `--smrt-glass-blur` / `-saturation` | `0px` / `100%` |
348
+ | `--smrt-glass-surface(-strong)`, `--smrt-surface-overlay` | `Canvas` (scrim and glass edges also `CanvasText`) |
349
+ | `--smrt-state-hover/pressed/selected/focus/dragged` | `0` (a translucent fill cannot be seen) |
350
+ | `--smrt-accent-color`, selection, caret, scrollbar | `Highlight`, `HighlightText`, `CanvasText` |
351
+
352
+ These blocks are deliberately **unlayered**. Preset stylesheets declare their tokens
353
+ unlayered on `[data-theme][data-color-scheme]`, and an unlayered rule always beats a
354
+ layered one, so a layered override would never apply. The selectors out-rank the preset
355
+ block by one attribute. While forced, `ThemeProvider` also appends the same map to the
356
+ root inline style (after generated variables and `primaryColor`, before explicit
357
+ `overrides`), so runtime themes and `primaryColor` cannot win.
358
+
359
+ **Generic rules** (in `smrt.theme`, zero specificity, so a component rule always wins):
360
+ hover is a 1px `Highlight` outline instead of a state-layer fill, disabled controls use
361
+ `GrayText`, and elements that rely on the UA focus ring get the token ring.
362
+
363
+ **Components** add `@media (forced-colors: active)` rules only where colour alone carried
364
+ meaning, using system colours: `Canvas`, `CanvasText`, `Highlight`, `HighlightText`,
365
+ `GrayText` (and `LinkText` and friends for chart series). Switch, Toggle, Checkbox and
366
+ Radio fill their track, box or mark with `Highlight`; Tabs draw the `Highlight` indicator
367
+ only on the current tab; Chip, FilterChips, SegmentedControl, ToggleButton, IconToggle
368
+ and the selected DataTable row get a `Highlight` fill or outline; Badge and Alert keep
369
+ text and border without the tinted fill; Progress uses `Highlight`/`CanvasText`. The shell
370
+ (TenantNav, phone tab bar, dock toggles, account trigger, focus tools) keeps its selected
371
+ and open states, e.g. the tenant nav start bar becomes `Highlight` and the row is bold.
372
+ Component rules use the media query only (no attribute mirror); the attribute path exists
373
+ for the token layer.
374
+
375
+ **Exceptions to `forced-color-adjust: auto`**: none are needed today, because system
376
+ colour keywords survive the adjustment. A component that must show an author colour to be
377
+ understood (a colour picker or swatch) sets `forced-color-adjust: none` on that element
378
+ only and documents why. Chart series cycle system colours by slot and the legend always
379
+ carries label, value and share as text.
380
+
381
+ Verification: Chromium e2e with `page.emulateMedia({ forcedColors: 'active' })` in
382
+ `e2e/forced-colors.spec.ts` (smrt-ui), `packages/smrt-svelte/e2e/forced-colors.spec.ts`
383
+ (shell) and the playground host. Unit parity test: `src/themes/__tests__/forced-colors.test.ts`.
384
+
331
385
  ## Theme Switching
332
386
 
333
387
  Use the context to switch themes programmatically:
@@ -673,7 +727,7 @@ calc(var(--smrt-button-state-hover, var(--smrt-state-hover)) * 100%), transparen
673
727
  | `--smrt-surface-overlay` | Modal scrim | `background` of Modal/Drawer backdrops |
674
728
  | `--smrt-hairline-color` / `-strong-color` | Quiet and strong edge colours | `border-color`, `outline-color`, dividers |
675
729
  | `--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 |
730
+ | `--smrt-inset-highlight` | Top inner light line (transparent shadow on light, never `none` in a shadow list) | `box-shadow` (layer with an elevation) on raised dark surfaces |
677
731
  | `--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
732
  | `--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
733
  | `--smrt-elevation-0..5` | Existing shadow scale | `box-shadow` |
@@ -5,6 +5,7 @@ import { SMRT_CASCADE_LAYERS_CSS } from '../styles/layers.js';
5
5
  import { setThemeContext, type ThemeContext } from './context.svelte.js';
6
6
  import { derivePrimaryOverrides } from './create-theme.js';
7
7
  import { generateThemeVariables } from './css-generator.js';
8
+ import { FORCED_COLORS_TOKENS } from './forced-colors.js';
8
9
  import { availablePresets, getTheme, isValidPreset } from './registry.js';
9
10
  import type {
10
11
  ColorScheme,
@@ -190,6 +191,14 @@ const styleString = $derived.by(() => {
190
191
  );
191
192
  }
192
193
 
194
+ // Forced colors (#3908): system palette beats every preset, runtime theme and
195
+ // primaryColor value; explicit `overrides` still win below.
196
+ if (resolvedContrast === 'forced') {
197
+ for (const [key, value] of Object.entries(FORCED_COLORS_TOKENS)) {
198
+ declarations.push(`${key}: ${value}`);
199
+ }
200
+ }
201
+
193
202
  // Apply custom overrides
194
203
  for (const [key, value] of Object.entries(config.overrides ?? {})) {
195
204
  declarations.push(`${key}: ${value}`);
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeProvider.svelte.d.ts","sourceRoot":"","sources":["../../src/themes/ThemeProvider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOtC,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAGlB,WAAW,EACX,WAAW,EAEZ,MAAM,YAAY,CAAC;AAIpB,UAAU,KAAK;IACb,wFAAwF;IACxF,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC;IAC3C,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAqdD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"ThemeProvider.svelte.d.ts","sourceRoot":"","sources":["../../src/themes/ThemeProvider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQtC,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAGlB,WAAW,EACX,WAAW,EAEZ,MAAM,YAAY,CAAC;AAIpB,UAAU,KAAK;IACb,wFAAwF;IACxF,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC;IAC3C,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AA8dD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -122,7 +122,7 @@ const labels = {
122
122
  align-items: center;
123
123
  gap: var(--smrt-spacing-2, 0.5rem);
124
124
  padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
125
- border: 1px solid var(--smrt-color-outline);
125
+ border: var(--smrt-hairline-strong);
126
126
  border-radius: var(--smrt-radius-full);
127
127
  background: var(--smrt-color-surface-container-low);
128
128
  color: var(--smrt-color-on-surface, inherit);
@@ -145,7 +145,7 @@ const labels = {
145
145
  align-items: center;
146
146
  gap: var(--smrt-spacing-1, 0.25rem);
147
147
  padding: var(--smrt-spacing-1_5, 0.375rem) var(--smrt-spacing-3, 0.75rem);
148
- border: 1px solid var(--smrt-color-outline);
148
+ border: var(--smrt-hairline-strong);
149
149
  border-radius: var(--smrt-radius-md, 0.5rem);
150
150
  background: var(--smrt-color-surface-container-low);
151
151
  color: var(--smrt-color-on-surface, inherit);
@@ -167,7 +167,7 @@ const labels = {
167
167
  .smrt-color-scheme-toggle__segmented {
168
168
  display: inline-flex;
169
169
  background: var(--smrt-color-surface-container-lowest);
170
- border: 1px solid var(--smrt-color-outline);
170
+ border: var(--smrt-hairline-strong);
171
171
  border-radius: var(--smrt-radius-lg, 0.75rem);
172
172
  padding: var(--smrt-spacing-0_5, 0.125rem);
173
173
  }
@@ -118,7 +118,7 @@ const themeIcons: Record<ThemePreset, string> = {
118
118
  .smrt-theme-switcher__select {
119
119
  padding: var(--smrt-spacing-1_5, 0.375rem) var(--smrt-spacing-3, 0.75rem);
120
120
  border-radius: var(--smrt-radius-md, 0.5rem);
121
- border: 1px solid var(--smrt-color-outline);
121
+ border: var(--smrt-hairline-strong);
122
122
  background: var(--smrt-color-surface, white);
123
123
  color: var(--smrt-color-on-surface, inherit);
124
124
  font-size: var(--smrt-typography-body-medium-size, 0.875rem);
@@ -135,7 +135,7 @@ const themeIcons: Record<ThemePreset, string> = {
135
135
  align-items: center;
136
136
  gap: var(--smrt-spacing-1, 0.25rem);
137
137
  padding: var(--smrt-spacing-1_5, 0.375rem) var(--smrt-spacing-3, 0.75rem);
138
- border: 1px solid var(--smrt-color-outline);
138
+ border: var(--smrt-hairline-strong);
139
139
  border-radius: var(--smrt-radius-md, 0.5rem);
140
140
  background: var(--smrt-color-surface-container-low);
141
141
  color: var(--smrt-color-on-surface, inherit);
@@ -157,7 +157,7 @@ const themeIcons: Record<ThemePreset, string> = {
157
157
  .smrt-theme-switcher__segmented {
158
158
  display: inline-flex;
159
159
  background: var(--smrt-color-surface-container-lowest);
160
- border: 1px solid var(--smrt-color-outline);
160
+ border: var(--smrt-hairline-strong);
161
161
  border-radius: var(--smrt-radius-lg, 0.75rem);
162
162
  padding: var(--smrt-spacing-0_5, 0.125rem);
163
163
  }
@@ -0,0 +1,2 @@
1
+ export declare const FORCED_COLORS_TOKENS: Readonly<Record<string, string>>;
2
+ //# sourceMappingURL=forced-colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forced-colors.d.ts","sourceRoot":"","sources":["../../src/themes/forced-colors.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgCjE,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Forced-colors (Windows High Contrast) token overrides (#3908).
3
+ *
4
+ * The single source for the theme-level contract documented in
5
+ * `themes/README.md` ("Forced colors"). `styles/base.css` carries the same
6
+ * declarations for stylesheet-only delivery (under `@media (forced-colors:
7
+ * active)` and `[data-forced-colors]`); `ThemeProvider` appends them to the
8
+ * root inline style while forced colors are active so a runtime theme or an
9
+ * inline `primaryColor` cannot paint over the system palette. A unit test keeps
10
+ * the two copies in step.
11
+ *
12
+ * Values are system colour keywords or inert shadows. Elevations are
13
+ * transparent shadows, never `none`, because components compose `box-shadow`
14
+ * lists such as `var(--smrt-inset-highlight), var(--smrt-elevation-3)`.
15
+ */
16
+ const INERT_SHADOW = '0 0 0 0 transparent';
17
+ export const FORCED_COLORS_TOKENS = {
18
+ '--smrt-hairline-color': 'CanvasText',
19
+ '--smrt-hairline-strong-color': 'CanvasText',
20
+ '--smrt-hairline': '1px solid CanvasText',
21
+ '--smrt-hairline-strong': '1px solid CanvasText',
22
+ '--smrt-inset-highlight': 'inset 0 0 0 0 transparent',
23
+ '--smrt-elevation-0': INERT_SHADOW,
24
+ '--smrt-elevation-1': INERT_SHADOW,
25
+ '--smrt-elevation-2': INERT_SHADOW,
26
+ '--smrt-elevation-3': INERT_SHADOW,
27
+ '--smrt-elevation-4': INERT_SHADOW,
28
+ '--smrt-elevation-5': INERT_SHADOW,
29
+ '--smrt-focus-ring-color': 'Highlight',
30
+ '--smrt-focus-ring-width': '3px',
31
+ '--smrt-focus-ring-offset': '2px',
32
+ '--smrt-glass-blur': '0px',
33
+ '--smrt-glass-saturation': '100%',
34
+ '--smrt-glass-surface': 'Canvas',
35
+ '--smrt-glass-surface-strong': 'Canvas',
36
+ '--smrt-glass-border': 'CanvasText',
37
+ '--smrt-surface-overlay': 'Canvas',
38
+ '--smrt-state-hover': '0',
39
+ '--smrt-state-pressed': '0',
40
+ '--smrt-state-selected': '0',
41
+ '--smrt-state-focus': '0',
42
+ '--smrt-state-dragged': '0',
43
+ '--smrt-accent-color': 'Highlight',
44
+ '--smrt-caret-color': 'CanvasText',
45
+ '--smrt-selection-background': 'Highlight',
46
+ '--smrt-selection-color': 'HighlightText',
47
+ '--smrt-scrollbar-thumb': 'CanvasText',
48
+ '--smrt-scrollbar-track': 'Canvas',
49
+ };