@happyvertical/smrt-ui 0.55.20 → 0.55.21

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.
@@ -2351,5 +2351,10 @@ $effect(() => {
2351
2351
  .data-table-container--narrow .data-table { table-layout: fixed; }
2352
2352
  .data-table-container--narrow .data-table__cell { white-space: normal; overflow-wrap: anywhere; }
2353
2353
  .data-table-container--narrow .data-table__sort-button { max-width: 100%; text-align: left; white-space: normal; overflow-wrap: anywhere; }
2354
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
2355
+ @media (forced-colors: active) {
2356
+ .data-table__row--selected, .data-table__row--selected .data-table__cell--pinned-start, .data-table__row--selected .data-table__cell--pinned-end { background: Canvas; }
2357
+ .data-table__row--selected { outline: var(--smrt-focus-ring-width) solid Highlight; outline-offset: calc(-1 * var(--smrt-focus-ring-width)); }
2358
+ }
2354
2359
  }
2355
2360
  </style>
@@ -60,5 +60,10 @@ const role = $derived(
60
60
  .dismiss { width: 2rem; height: 2rem; margin: -.35rem; border: 0; border-radius: var(--smrt-radius-full); background: transparent; color: inherit; font-size: 1.25rem; cursor: pointer; }
61
61
  .dismiss:hover { background: color-mix(in srgb, currentColor calc(var(--smrt-state-hover) * 100%), transparent); }
62
62
  .dismiss:focus-visible { outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color); outline-offset: var(--smrt-focus-ring-offset); }
63
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
64
+ @media (forced-colors: active) {
65
+ .alert { background: Canvas; border-color: CanvasText; }
66
+ .marker { background: CanvasText; }
67
+ }
63
68
  }
64
69
  </style>
@@ -68,5 +68,12 @@ const displayValue = $derived(
68
68
  .progress--circular > span { position: absolute; font: var(--smrt-typography-label-small-font); }
69
69
  @keyframes linear-progress { from { left: -40%; } to { left: 105%; } } @keyframes spin { to { transform: rotate(270deg); } }
70
70
  @media (prefers-reduced-motion: reduce) { .progress--linear .indicator, .progress--circular .indicator { transition: none; } .progress--linear.indeterminate .indicator { animation-duration: 2.5s; } }
71
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
72
+ @media (forced-colors: active) {
73
+ .progress--linear .track { background: Canvas; border: var(--smrt-hairline-strong); }
74
+ .progress--linear .indicator { background: Highlight; }
75
+ .progress--circular .track { stroke: CanvasText; }
76
+ .progress--circular .indicator { stroke: Highlight; }
77
+ }
71
78
  }
72
79
  </style>
@@ -193,5 +193,14 @@ useControlRegistration(() => {
193
193
  .checkbox[data-density='touch'] { --smrt-control-target-min: var(--smrt-touch-target-min); }
194
194
  .checkbox[data-density='comfortable'] { --smrt-control-target-min: 0px; }
195
195
  .checkbox { min-height: var(--smrt-control-target-min, 0px); min-width: var(--smrt-control-target-min, 0px); }
196
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
197
+ @media (forced-colors: active) {
198
+ .checkbox__box { background: Canvas; border-color: CanvasText; }
199
+ input:checked + .checkbox__box, input:indeterminate + .checkbox__box { background: Highlight; border-color: Highlight; color: HighlightText; }
200
+ .checkbox__dash { background: HighlightText; }
201
+ .checkbox--disabled { opacity: 1; color: GrayText; }
202
+ .checkbox--disabled .checkbox__box, .checkbox--disabled input:checked + .checkbox__box, .checkbox--disabled input:indeterminate + .checkbox__box { background: Canvas; border-color: GrayText; color: GrayText; }
203
+ .checkbox--disabled .checkbox__dash { background: GrayText; }
204
+ }
196
205
  }
197
206
  </style>
@@ -48,5 +48,14 @@ function handleChange(event: Event & { currentTarget: HTMLInputElement }) {
48
48
 
49
49
  /* Table cells: grow vertically only, never into a neighbouring cell. */
50
50
  :global(:is(td, th)) .radio::before { inline-size: var(--smrt-radio-box); margin-inline-start: calc(var(--smrt-radio-box) / -2); }
51
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
52
+ @media (forced-colors: active) {
53
+ .radio__mark { border-color: CanvasText; }
54
+ .radio__mark::after { background: Highlight; }
55
+ input:checked + .radio__mark { border-color: Highlight; }
56
+ .radio.disabled { opacity: 1; color: GrayText; }
57
+ .radio.disabled .radio__mark, .radio.disabled input:checked + .radio__mark { border-color: GrayText; }
58
+ .radio.disabled .radio__mark::after { background: GrayText; }
59
+ }
51
60
  }
52
61
  </style>
@@ -263,5 +263,10 @@ useControlRegistration(() => {
263
263
  .segmented[data-density='comfortable'] { --smrt-control-target-min: 0px; }
264
264
  label { min-height: max(2.5rem, var(--smrt-control-target-min, 0px)); min-width: var(--smrt-control-target-min, 0px); }
265
265
  input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: inherit; }
266
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
267
+ @media (forced-colors: active) {
268
+ label.selected { background: Highlight; color: HighlightText; }
269
+ label.disabled, label:has(input:disabled), label.selected.disabled, label.selected:has(input:disabled) { opacity: 1; background: Canvas; color: GrayText; }
270
+ }
266
271
  }
267
272
  </style>
@@ -155,5 +155,15 @@ useControlRegistration(() => {
155
155
  .switch[data-density='touch'] { --smrt-control-target-min: var(--smrt-touch-target-min); }
156
156
  .switch[data-density='comfortable'] { --smrt-control-target-min: 0px; }
157
157
  .switch { min-height: var(--smrt-control-target-min, 0px); min-width: var(--smrt-control-target-min, 0px); }
158
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
159
+ @media (forced-colors: active) {
160
+ .switch__track { background: Canvas; border-color: CanvasText; }
161
+ .switch__thumb { background: CanvasText; }
162
+ input:checked + .switch__track { background: Highlight; border-color: Highlight; }
163
+ input:checked + .switch__track .switch__thumb { background: HighlightText; }
164
+ .switch--disabled { opacity: 1; color: GrayText; }
165
+ .switch--disabled .switch__track, .switch--disabled input:checked + .switch__track { background: Canvas; border-color: GrayText; }
166
+ .switch--disabled .switch__thumb, .switch--disabled input:checked + .switch__track .switch__thumb { background: GrayText; }
167
+ }
158
168
  }
159
169
  </style>
@@ -223,5 +223,17 @@ const sizeClasses = {
223
223
  transition: none;
224
224
  }
225
225
  }
226
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
227
+ @media (forced-colors: active) {
228
+ .toggle__thumb, .toggle:not(.toggle--disabled):hover .toggle__thumb { background: Canvas; outline: var(--smrt-hairline-strong); outline-offset: 0; }
229
+ .toggle__input:checked + .toggle__thumb, .toggle:not(.toggle--disabled):hover .toggle__input:checked + .toggle__thumb { background: Highlight; outline-color: Highlight; }
230
+ .toggle__thumb::after { background: CanvasText; }
231
+ .toggle__input:checked + .toggle__thumb::after { background: HighlightText; }
232
+ .toggle__input:focus-visible + .toggle__thumb { outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color); outline-offset: var(--smrt-focus-ring-offset); }
233
+ .toggle--disabled { opacity: 1; color: GrayText; }
234
+ .toggle--disabled .toggle__label { color: GrayText; }
235
+ .toggle--disabled .toggle__thumb, .toggle--disabled .toggle__input:checked + .toggle__thumb { background: Canvas; outline-color: GrayText; }
236
+ .toggle--disabled .toggle__thumb::after, .toggle--disabled .toggle__input:checked + .toggle__thumb::after { background: GrayText; }
237
+ }
226
238
  }
227
239
  </style>
@@ -99,5 +99,10 @@ useControlRegistration(() => {
99
99
  .toggle-button:focus-visible { outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color); outline-offset: var(--smrt-focus-ring-offset); }
100
100
  .toggle-button:disabled { opacity: .5; cursor: not-allowed; }
101
101
  .toggle-button[data-smrt-highlighted='true'] { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
102
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
103
+ @media (forced-colors: active) {
104
+ .toggle-button.pressed { background: Highlight; color: HighlightText; border-color: Highlight; }
105
+ .toggle-button:disabled, .toggle-button.pressed:disabled { opacity: 1; background: Canvas; color: GrayText; border-color: GrayText; }
106
+ }
102
107
  }
103
108
  </style>
@@ -158,5 +158,10 @@ function handleClick(value: string) {
158
158
  .filter-chips[data-density='touch'] { --smrt-control-target-min: var(--smrt-touch-target-min); }
159
159
  .filter-chips[data-density='comfortable'] { --smrt-control-target-min: 0px; }
160
160
  .filter-chip { min-height: var(--smrt-control-target-min, 0px); min-width: var(--smrt-control-target-min, 0px); }
161
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
162
+ @media (forced-colors: active) {
163
+ .filter-chip.active { background-color: Highlight; color: HighlightText; border-color: Highlight; }
164
+ .filter-chip:disabled, .filter-chip.active:disabled { opacity: 1; background: Canvas; color: GrayText; border-color: GrayText; }
165
+ }
161
166
  }
162
167
  </style>
@@ -523,5 +523,15 @@ function handleKeydown(event: KeyboardEvent, tabId: string) {
523
523
  clip: rect(0 0 0 0);
524
524
  white-space: nowrap;
525
525
  }
526
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
527
+ @media (forced-colors: active) {
528
+ .active-indicator { background-color: Highlight; }
529
+ .tab-button.active, .tab-link.active { color: Highlight; }
530
+ .tab-link { border-bottom-color: Canvas; }
531
+ .tab-link.active { border-bottom-color: Highlight; }
532
+ .tab-dot { background: CanvasText; }
533
+ .tab-badge { background: Canvas; border: var(--smrt-hairline-strong); }
534
+ .tab-button:disabled, .tab-link[aria-disabled='true'] { opacity: 1; color: GrayText; }
535
+ }
526
536
  }
527
537
  </style>
@@ -73,5 +73,9 @@ const { variant = 'default', size = 'md', children, ...rest }: Props = $props();
73
73
  background: var(--smrt-color-primary-container);
74
74
  color: var(--smrt-color-on-primary-container);
75
75
  }
76
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
77
+ @media (forced-colors: active) {
78
+ .badge { background: Canvas; color: CanvasText; border-color: CanvasText; }
79
+ }
76
80
  }
77
81
  </style>
@@ -190,5 +190,10 @@ const {
190
190
  outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color);
191
191
  outline-offset: var(--smrt-focus-ring-offset);
192
192
  }
193
+ /* Forced colors (#3908): system colours where colour alone carries meaning. */
194
+ @media (forced-colors: active) {
195
+ .chip.selected { border-color: Highlight; outline: var(--smrt-focus-ring-width) solid Highlight; outline-offset: calc(-1 * var(--smrt-focus-ring-width)); }
196
+ .chip.disabled, .chip.selected.disabled { opacity: 1; color: GrayText; border-color: GrayText; outline-color: GrayText; }
197
+ }
193
198
  }
194
199
  </style>
@@ -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>
@@ -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:
@@ -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"}
@@ -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
+ };
@@ -1,5 +1,8 @@
1
1
 
2
- @layer smrt.reset, smrt.structure, smrt.theme;/**
2
+ @layer smrt.reset, smrt.structure, smrt.theme;
3
+ @import url('./forced-colors.css') (forced-colors: active);
4
+
5
+ /**
3
6
  * Application baseline for SMRT interfaces.
4
7
  *
5
8
  * Import this once at an application entry point, alongside the selected
@@ -257,3 +260,79 @@ time,
257
260
  }
258
261
  }
259
262
  }
263
+
264
+ /*
265
+ * Forced colors (#3908). Token level: Windows High Contrast / `forced-colors:
266
+ * active` maps the theme onto the system palette. These two token blocks are
267
+ * deliberately UNLAYERED: preset stylesheets declare their tokens unlayered on
268
+ * `[data-theme][data-color-scheme]`, and an unlayered rule always beats a
269
+ * layered one, so a layered override would never apply. The selectors match
270
+ * whichever element carries the theme attributes (document root, ThemeProvider
271
+ * root or a consumer wrapper) with one more attribute selector than the preset
272
+ * block. The `forced-colors: active` media case is the conditional `@import`
273
+ * at the top of this file (forced-colors.css); the attribute case is the
274
+ * block below. The same values ship as
275
+ * `FORCED_COLORS_TOKENS` (src/themes/forced-colors.ts): ThemeProvider appends
276
+ * them inline so runtime themes cannot win; a unit test keeps both in step.
277
+ * `[data-forced-colors]` is stamped by ThemeProvider and `themeScript()`;
278
+ * tests can set it without media emulation. Components only consume tokens.
279
+ */
280
+
281
+ [data-forced-colors][data-theme][data-color-scheme] {
282
+ --smrt-hairline-color: CanvasText;
283
+ --smrt-hairline-strong-color: CanvasText;
284
+ --smrt-hairline: 1px solid CanvasText;
285
+ --smrt-hairline-strong: 1px solid CanvasText;
286
+ --smrt-inset-highlight: inset 0 0 0 0 transparent;
287
+ --smrt-elevation-0: 0 0 0 0 transparent;
288
+ --smrt-elevation-1: 0 0 0 0 transparent;
289
+ --smrt-elevation-2: 0 0 0 0 transparent;
290
+ --smrt-elevation-3: 0 0 0 0 transparent;
291
+ --smrt-elevation-4: 0 0 0 0 transparent;
292
+ --smrt-elevation-5: 0 0 0 0 transparent;
293
+ --smrt-focus-ring-color: Highlight;
294
+ --smrt-focus-ring-width: 3px;
295
+ --smrt-focus-ring-offset: 2px;
296
+ --smrt-glass-blur: 0px;
297
+ --smrt-glass-saturation: 100%;
298
+ --smrt-glass-surface: Canvas;
299
+ --smrt-glass-surface-strong: Canvas;
300
+ --smrt-glass-border: CanvasText;
301
+ --smrt-surface-overlay: Canvas;
302
+ --smrt-state-hover: 0;
303
+ --smrt-state-pressed: 0;
304
+ --smrt-state-selected: 0;
305
+ --smrt-state-focus: 0;
306
+ --smrt-state-dragged: 0;
307
+ --smrt-accent-color: Highlight;
308
+ --smrt-caret-color: CanvasText;
309
+ --smrt-selection-background: Highlight;
310
+ --smrt-selection-color: HighlightText;
311
+ --smrt-scrollbar-thumb: CanvasText;
312
+ --smrt-scrollbar-track: Canvas;
313
+ }
314
+
315
+ /*
316
+ * A translucent state-layer fill cannot be seen in forced colors, so hover is
317
+ * an outline instead. :where() keeps specificity at zero: every component's own
318
+ * focus, selected or outline rule still wins.
319
+ */
320
+ @layer smrt.theme {
321
+ @media (forced-colors: active) {
322
+ :where(button, [role="button"], [role="tab"], [role="menuitem"], [role="option"], a[href], summary, label):where(:hover):where(:not(:disabled, [aria-disabled="true"])) {
323
+ outline: 1px solid Highlight;
324
+ outline-offset: -1px;
325
+ }
326
+
327
+ :where(:disabled, [aria-disabled="true"]) {
328
+ color: GrayText;
329
+ }
330
+
331
+ /* Elements that rely on the UA focus ring get the token ring instead (a
332
+ solid Highlight ring of a stated width); component rules still win. */
333
+ :where(a[href], button, summary, input, select, textarea, [tabindex], [role="tab"], [role="menuitem"], [role="option"]):where(:focus-visible) {
334
+ outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color);
335
+ outline-offset: var(--smrt-focus-ring-offset);
336
+ }
337
+ }
338
+ }
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Forced colors token layer (#3908), loaded by base.css as
3
+ * `@import url('./forced-colors.css') (forced-colors: active)`. Deliberately
4
+ * UNLAYERED (and a declaration-only block): preset stylesheets declare their
5
+ * tokens unlayered on `[data-theme][data-color-scheme]`, and an unlayered rule
6
+ * always beats a layered one. The repeated `[data-theme]` out-ranks that block
7
+ * by one attribute. Values mirror `FORCED_COLORS_TOKENS`
8
+ * (src/themes/forced-colors.ts); a unit test keeps the copies in step.
9
+ */
10
+ [data-theme][data-theme][data-color-scheme] {
11
+ --smrt-hairline-color: CanvasText;
12
+ --smrt-hairline-strong-color: CanvasText;
13
+ --smrt-hairline: 1px solid CanvasText;
14
+ --smrt-hairline-strong: 1px solid CanvasText;
15
+ --smrt-inset-highlight: inset 0 0 0 0 transparent;
16
+ --smrt-elevation-0: 0 0 0 0 transparent;
17
+ --smrt-elevation-1: 0 0 0 0 transparent;
18
+ --smrt-elevation-2: 0 0 0 0 transparent;
19
+ --smrt-elevation-3: 0 0 0 0 transparent;
20
+ --smrt-elevation-4: 0 0 0 0 transparent;
21
+ --smrt-elevation-5: 0 0 0 0 transparent;
22
+ --smrt-focus-ring-color: Highlight;
23
+ --smrt-focus-ring-width: 3px;
24
+ --smrt-focus-ring-offset: 2px;
25
+ --smrt-glass-blur: 0px;
26
+ --smrt-glass-saturation: 100%;
27
+ --smrt-glass-surface: Canvas;
28
+ --smrt-glass-surface-strong: Canvas;
29
+ --smrt-glass-border: CanvasText;
30
+ --smrt-surface-overlay: Canvas;
31
+ --smrt-state-hover: 0;
32
+ --smrt-state-pressed: 0;
33
+ --smrt-state-selected: 0;
34
+ --smrt-state-focus: 0;
35
+ --smrt-state-dragged: 0;
36
+ --smrt-accent-color: Highlight;
37
+ --smrt-caret-color: CanvasText;
38
+ --smrt-selection-background: Highlight;
39
+ --smrt-selection-color: HighlightText;
40
+ --smrt-scrollbar-thumb: CanvasText;
41
+ --smrt-scrollbar-track: Canvas;
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-ui",
3
- "version": "0.55.20",
3
+ "version": "0.55.21",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
6
6
  "type": "module",
@@ -155,7 +155,7 @@
155
155
  "access": "public"
156
156
  },
157
157
  "dependencies": {
158
- "@happyvertical/smrt-types": "0.55.20",
158
+ "@happyvertical/smrt-types": "0.55.21",
159
159
  "esm-env": "^1.2.2"
160
160
  },
161
161
  "peerDependencies": {