@motion-proto/live-tokens 0.97.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
  4. package/CHANGELOG.md +339 -0
  5. package/README.md +2 -2
  6. package/bin/check-component.mjs +8 -0
  7. package/bin/check-page.mjs +6 -0
  8. package/bin/cli.mjs +36 -6
  9. package/bin/contractRunner.mjs +144 -19
  10. package/bin/lib/findings.mjs +20 -4
  11. package/bin/rules/a11y.mjs +53 -0
  12. package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  13. package/dist-plugin/index.cjs +204 -3
  14. package/dist-plugin/index.js +306 -105
  15. package/dist-plugin/setColors/index.js +1 -1
  16. package/package.json +47 -2
  17. package/src/demo/LabeledSelect.svelte +1 -1
  18. package/src/editor/bootstrap.ts +5 -1
  19. package/src/editor/component-editor/BadgeEditor.svelte +13 -0
  20. package/src/editor/component-editor/ButtonEditor.svelte +33 -2
  21. package/src/editor/component-editor/CalloutEditor.svelte +13 -0
  22. package/src/editor/component-editor/CardEditor.svelte +12 -2
  23. package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
  24. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
  25. package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
  26. package/src/editor/component-editor/DialogEditor.svelte +15 -0
  27. package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
  28. package/src/editor/component-editor/ImageEditor.svelte +20 -9
  29. package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
  30. package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
  31. package/src/editor/component-editor/InputEditor.svelte +23 -0
  32. package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
  33. package/src/editor/component-editor/NotificationEditor.svelte +15 -0
  34. package/src/editor/component-editor/PanelEditor.svelte +1 -1
  35. package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
  36. package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
  37. package/src/editor/component-editor/SectionDividerEditor.svelte +40 -13
  38. package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
  39. package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
  40. package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
  41. package/src/editor/component-editor/SliderEditor.svelte +33 -0
  42. package/src/editor/component-editor/TabBarEditor.svelte +23 -2
  43. package/src/editor/component-editor/TableEditor.svelte +14 -0
  44. package/src/editor/component-editor/ToggleEditor.svelte +17 -0
  45. package/src/editor/component-editor/TooltipEditor.svelte +11 -2
  46. package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
  47. package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
  48. package/src/editor/component-editor/registration.ts +2 -1
  49. package/src/editor/component-editor/registry.ts +69 -27
  50. package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
  51. package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
  52. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
  53. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -3
  54. package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
  55. package/src/editor/core/a11y/a11yReport.ts +164 -0
  56. package/src/editor/core/a11y/a11yReportTypes.ts +77 -0
  57. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  58. package/src/editor/core/a11y/contrastFixes.ts +41 -0
  59. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  60. package/src/editor/core/palettes/contrast.ts +44 -0
  61. package/src/editor/core/palettes/contrastPairs.ts +82 -0
  62. package/src/editor/core/palettes/resolveColor.ts +77 -0
  63. package/src/editor/core/store/editorStore.ts +4 -0
  64. package/src/editor/core/store/editorViewStore.ts +2 -2
  65. package/src/editor/core/store/unsavedEdits.ts +10 -0
  66. package/src/editor/core/themes/liveStateStream.ts +7 -1
  67. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  68. package/src/editor/docs/chapters.ts +1 -0
  69. package/src/editor/docs/content/accessibility.md +411 -0
  70. package/src/editor/docs/content/editing-tokens.md +32 -1
  71. package/src/editor/docs/content.generated.ts +2 -1
  72. package/src/editor/pages/ComponentEditorPage.svelte +11 -1
  73. package/src/editor/pages/EditorShell.svelte +98 -10
  74. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  75. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  76. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  77. package/src/editor/styles/ui-editor.css +2 -0
  78. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  79. package/src/editor/ui/GradientEditor.svelte +9 -1
  80. package/src/editor/ui/ThemePanel.svelte +4 -4
  81. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  82. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  83. package/src/editor/ui/UIPaletteSelector.svelte +9 -0
  84. package/src/editor/ui/UITokenSelector.svelte +26 -2
  85. package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
  86. package/src/editor/ui/a11y/A11yComponentIssues.svelte +121 -0
  87. package/src/editor/ui/a11y/A11yFindingRow.svelte +119 -0
  88. package/src/editor/ui/a11y/A11yIssueRow.svelte +152 -0
  89. package/src/editor/ui/a11y/A11yOverview.svelte +246 -0
  90. package/src/editor/ui/a11y/A11yPanel.svelte +153 -0
  91. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  92. package/src/editor/ui/a11y/A11yRailMark.svelte +41 -0
  93. package/src/editor/ui/a11y/AccessibilityTab.svelte +321 -0
  94. package/src/editor/ui/a11y/a11yView.ts +257 -0
  95. package/src/editor/ui/contrastReadout.ts +45 -0
  96. package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
  97. package/src/live-tokens/data/themes/autumn.json +35 -35
  98. package/src/live-tokens/data/themes/halloween.json +36 -36
  99. package/src/live-tokens/data/themes/midnight-study.json +35 -35
  100. package/src/live-tokens/data/themes/ocean.json +35 -35
  101. package/src/live-tokens/data/themes/royal-velvet.json +35 -35
  102. package/src/live-tokens/data/themes/sketchy.json +35 -35
  103. package/src/live-tokens/data/themes/spring-meadow.json +35 -35
  104. package/src/live-tokens/data/themes/sunset.json +35 -35
  105. package/src/system/components/Badge.svelte +3 -3
  106. package/src/system/components/Button.svelte +12 -6
  107. package/src/system/components/CodeSnippet.svelte +8 -1
  108. package/src/system/components/CornerBadge.svelte +3 -3
  109. package/src/system/components/Dialog.svelte +43 -14
  110. package/src/system/components/IconButton.svelte +1 -1
  111. package/src/system/components/Image.svelte +7 -0
  112. package/src/system/components/ImageLightbox.svelte +18 -25
  113. package/src/system/components/InlineEditActions.svelte +1 -1
  114. package/src/system/components/Input.svelte +9 -2
  115. package/src/system/components/MenuSelect.svelte +31 -3
  116. package/src/system/components/Notification.svelte +1 -1
  117. package/src/system/components/ProgressBar.svelte +7 -1
  118. package/src/system/components/RadioButton.svelte +36 -3
  119. package/src/system/components/SegmentedControl.svelte +36 -2
  120. package/src/system/components/SlidePager.svelte +3 -3
  121. package/src/system/components/Slider.svelte +19 -15
  122. package/src/system/components/TabBar.svelte +33 -3
  123. package/src/system/components/Toggle.svelte +3 -3
  124. package/src/system/components/VideoFrame.svelte +1 -1
  125. package/src/system/components/VideoLightbox.svelte +2 -2
  126. package/src/system/internal/focusTrap.ts +35 -0
  127. package/src/testing-js/__aria__/badge.yml +40 -0
  128. package/src/testing-js/__aria__/button.yml +88 -0
  129. package/src/testing-js/__aria__/callout.yml +12 -0
  130. package/src/testing-js/__aria__/card.yml +8 -0
  131. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  132. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  133. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  134. package/src/testing-js/__aria__/dialog.yml +42 -0
  135. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  136. package/src/testing-js/__aria__/image.yml +10 -0
  137. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  138. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  139. package/src/testing-js/__aria__/input.yml +61 -0
  140. package/src/testing-js/__aria__/menuselect.yml +30 -0
  141. package/src/testing-js/__aria__/notification.yml +30 -0
  142. package/src/testing-js/__aria__/panel.yml +2 -0
  143. package/src/testing-js/__aria__/progressbar.yml +2 -0
  144. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  145. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  146. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  147. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  148. package/src/testing-js/__aria__/slidepager.yml +72 -0
  149. package/src/testing-js/__aria__/slider.yml +28 -0
  150. package/src/testing-js/__aria__/tabbar.yml +35 -0
  151. package/src/testing-js/__aria__/table.yml +144 -0
  152. package/src/testing-js/__aria__/toggle.yml +12 -0
  153. package/src/testing-js/__aria__/tooltip.yml +2 -0
  154. package/src/testing-js/__aria__/videoframe.yml +6 -0
  155. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  156. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  157. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  158. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  159. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-7QXUJRSO.js} +97 -29
  160. package/src/testing-js/chunk-7QXUJRSO.js.map +1 -0
  161. package/src/testing-js/chunk-GZKUYLDW.js +193 -0
  162. package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
  163. package/src/testing-js/chunk-RTZQLW3F.js +649 -0
  164. package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
  165. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-RWOT2XDB.js} +212 -15
  166. package/src/testing-js/chunk-RWOT2XDB.js.map +1 -0
  167. package/src/testing-js/{chunk-GKO27NGN.js → chunk-THWGMSRH.js} +2 -2
  168. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  169. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  170. package/src/testing-js/component-a11y.contract.js +313 -0
  171. package/src/testing-js/component-a11y.contract.js.map +1 -0
  172. package/src/testing-js/component-alias.contract.js +2 -2
  173. package/src/testing-js/component-behavior.contract.js +2 -2
  174. package/src/testing-js/component-editor.contract.js +6 -5
  175. package/src/testing-js/component-editor.contract.js.map +1 -1
  176. package/src/testing-js/component-render.contract.js +10 -6
  177. package/src/testing-js/component-render.contract.js.map +1 -1
  178. package/src/testing-js/index.d.ts +44 -16
  179. package/src/testing-js/index.js +11 -11
  180. package/src/testing-js/index.js.map +1 -1
  181. package/src/testing-js/page-compliance.contract.js +132 -13
  182. package/src/testing-js/page-compliance.contract.js.map +1 -1
  183. package/src/testing-js/vitest.js +3 -3
  184. package/template/_gitignore +3 -0
  185. package/template/package.json +9 -0
  186. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  187. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  188. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  189. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  190. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  191. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-THWGMSRH.js.map} +0 -0
@@ -73,7 +73,7 @@
73
73
  /* Per-variant color overrides. Default = inherit the same family/level the
74
74
  inner Badge uses; author can swap to a different level (e.g. -low) to
75
75
  reduce contrast against the host surface. */
76
- --cornerbadge-brand-surface: var(--surface-brand);
76
+ --cornerbadge-brand-surface: var(--surface-brand-low);
77
77
  --cornerbadge-brand-border: var(--border-brand);
78
78
  --cornerbadge-brand-text: var(--text-brand);
79
79
  --cornerbadge-accent-surface: var(--surface-accent);
@@ -87,7 +87,7 @@
87
87
  --cornerbadge-alternate-text: var(--text-alternate);
88
88
  --cornerbadge-canvas-surface: var(--surface-canvas);
89
89
  --cornerbadge-canvas-border: var(--border-canvas);
90
- --cornerbadge-canvas-text: var(--text-canvas);
90
+ --cornerbadge-canvas-text: var(--text-secondary);
91
91
  --cornerbadge-special-surface: var(--surface-special);
92
92
  --cornerbadge-special-border: var(--border-special);
93
93
  --cornerbadge-special-text: var(--text-special);
@@ -97,7 +97,7 @@
97
97
  --cornerbadge-warning-surface: var(--surface-warning);
98
98
  --cornerbadge-warning-border: var(--border-warning);
99
99
  --cornerbadge-warning-text: var(--text-warning);
100
- --cornerbadge-danger-surface: var(--surface-danger);
100
+ --cornerbadge-danger-surface: var(--surface-danger-low);
101
101
  --cornerbadge-danger-border: var(--border-danger);
102
102
  --cornerbadge-danger-text: var(--text-danger);
103
103
  --cornerbadge-info-surface: var(--surface-info);
@@ -15,6 +15,7 @@
15
15
  import { createEventDispatcher, tick } from 'svelte';
16
16
  import type { Snippet } from 'svelte';
17
17
  import { portal } from '../internal/portal';
18
+ import { tabbables, trapTab } from '../internal/focusTrap';
18
19
  import Button from './Button.svelte';
19
20
  import type { ButtonVariant, DialogButtonSpec } from './types';
20
21
 
@@ -64,6 +65,8 @@
64
65
  children,
65
66
  }: Props = $props();
66
67
 
68
+ const titleId = $props.id();
69
+
67
70
  let effectiveConfirmVariant = $derived(confirm?.variant ?? asVariant(confirmVarValue, 'primary'));
68
71
  let effectiveCancelVariant = $derived(cancel?.variant ?? asVariant(cancelVarValue, 'outline'));
69
72
 
@@ -75,6 +78,7 @@
75
78
  let confirmButtonRef: HTMLButtonElement = $state()!;
76
79
  let cancelButtonRef: HTMLButtonElement = $state()!;
77
80
  let closeButtonRef: HTMLButtonElement = $state()!;
81
+ let dialogEl: HTMLDivElement | undefined = $state();
78
82
 
79
83
  $effect(() => {
80
84
  if (typeof document === 'undefined') return;
@@ -86,21 +90,29 @@
86
90
  return () => obs.disconnect();
87
91
  });
88
92
 
89
- // Focus the primary button when dialog opens (skip in inline mode so the editor doesn't steal focus).
93
+ // The inline preview stays out of the trap, so the editor keeps its focus.
90
94
  $effect(() => {
91
- if (open && !inline) {
92
- tick().then(() => {
93
- if (confirm && confirmButtonRef && !confirm.disabled) {
94
- confirmButtonRef.focus();
95
- } else if (cancel && cancelButtonRef) {
96
- cancelButtonRef.focus();
97
- } else if (closeButtonRef) {
98
- closeButtonRef.focus();
99
- }
100
- });
101
- }
95
+ if (!open || inline) return;
96
+ const returnTo = document.activeElement as HTMLElement | null;
97
+ tick().then(() => {
98
+ const target =
99
+ (confirm && !confirm.disabled && confirmButtonRef) ||
100
+ (cancel && cancelButtonRef) ||
101
+ closeButtonRef ||
102
+ (dialogEl && tabbables(dialogEl)[0]) ||
103
+ dialogEl;
104
+ target?.focus();
105
+ });
106
+ return () => returnTo?.focus();
102
107
  });
103
108
 
109
+ function holdTab(event: KeyboardEvent) {
110
+ if (event.key !== 'Tab' || !open || inline || !dialogEl) return;
111
+ if (trapTab(dialogEl, event)) return;
112
+ event.preventDefault();
113
+ dialogEl.focus();
114
+ }
115
+
104
116
  function handleConfirm() {
105
117
  if (confirm && !confirm.disabled) {
106
118
  confirm.onClick();
@@ -116,17 +128,34 @@
116
128
  open = false;
117
129
  }
118
130
  }
131
+
132
+ function closeOnEscape(event: KeyboardEvent) {
133
+ if (event.key !== 'Escape') return;
134
+ event.preventDefault();
135
+ handleCancel();
136
+ }
119
137
  </script>
120
138
 
139
+ <svelte:document onkeydown={holdTab} />
140
+
121
141
  {#if open}
122
142
  <!-- The fixed backdrop portals to <body> to escape transformed/isolated
123
143
  ancestors; the inline preview variant stays in flow. -->
124
144
  <div class="dialog-backdrop" class:inline use:portal={!inline}>
125
- <div class="dialog" style="width: {width}; max-width: {width};">
145
+ <div
146
+ bind:this={dialogEl}
147
+ class="dialog"
148
+ role="dialog"
149
+ aria-modal={inline ? undefined : true}
150
+ aria-labelledby={title ? titleId : undefined}
151
+ tabindex="-1"
152
+ style="width: {width}; max-width: {width};"
153
+ onkeydown={closeOnEscape}
154
+ >
126
155
  <div class="dialog-content">
127
156
  {#if title}
128
157
  <div class="dialog-header">
129
- <h3 class="dialog-title">{title}</h3>
158
+ <h3 class="dialog-title" id={titleId}>{title}</h3>
130
159
  <button
131
160
  bind:this={closeButtonRef}
132
161
  class="dialog-close"
@@ -5,7 +5,7 @@
5
5
  description: 'An action that an icon alone carries.',
6
6
  whenToUse: 'an action whose meaning is plain from the glyph, such as close, edit, or delete.',
7
7
  whenNotToUse: [
8
- { when: 'the action needs a word to be unambiguous.', use: 'button' },
8
+ { when: 'the trigger needs a word to be unambiguous.', use: 'button' },
9
9
  { when: 'the pair confirms or cancels an inline edit.', use: 'inlineeditactions' },
10
10
  ],
11
11
  props: {
@@ -144,4 +144,11 @@
144
144
  .image.force-hover img {
145
145
  transform: var(--image-zoom-enabled);
146
146
  }
147
+
148
+ @media (prefers-reduced-motion: reduce) {
149
+ .image,
150
+ img {
151
+ transition: none;
152
+ }
153
+ }
147
154
  </style>
@@ -21,6 +21,7 @@
21
21
  <script lang="ts">
22
22
  import { onMount, tick, type Snippet } from 'svelte';
23
23
  import { portal } from '../internal/portal';
24
+ import { trapTab } from '../internal/focusTrap';
24
25
 
25
26
  interface GalleryImage {
26
27
  src: string;
@@ -45,6 +46,8 @@
45
46
  thumbSrc?: string | undefined;
46
47
  /** Closed-thumbnail object-fit. `cover` crops the thumbnail to fill its box;
47
48
  the expanded modal always uses `contain` so the whole image stays visible.
49
+ Unset, it follows the theme's `--imagelightbox-tile-object-fit`, which
50
+ defaults to `contain`.
48
51
  `cover` only crops when the thumbnail box differs from the image's shape
49
52
  (a `thumbAspect`, or a CSS-constrained container). */
50
53
  fit?: 'contain' | 'cover';
@@ -111,7 +114,7 @@
111
114
  maxWidth = undefined,
112
115
  thumbAspect = undefined,
113
116
  thumbSrc = undefined,
114
- fit = 'contain',
117
+ fit = undefined,
115
118
  shadow = 'box',
116
119
  surface = undefined,
117
120
  border = undefined,
@@ -607,29 +610,12 @@
607
610
  if (!isGallery && scale <= MIN_SCALE) closeLightbox();
608
611
  }
609
612
 
610
- // Keep Tab inside the open dialog. The stage (tabindex -1) is excluded, so an
611
- // image-only lightbox holds focus on its close button.
612
- function trapTab(e: KeyboardEvent) {
613
- if (!modalEl) return;
614
- const f = [
615
- ...modalEl.querySelectorAll<HTMLElement>('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'),
616
- ];
617
- if (f.length === 0) {
618
- e.preventDefault();
619
- stageEl?.focus();
620
- return;
621
- }
622
- const first = f[0];
623
- const last = f[f.length - 1];
624
- const active = document.activeElement as HTMLElement | null;
625
- const inside = active && modalEl.contains(active);
626
- if (e.shiftKey && (!inside || active === first)) {
627
- e.preventDefault();
628
- last.focus();
629
- } else if (!e.shiftKey && (!inside || active === last)) {
630
- e.preventDefault();
631
- first.focus();
632
- }
613
+ // The stage (tabindex -1) is no tab stop, so an image-only lightbox holds
614
+ // focus on its close button.
615
+ function onTab(e: KeyboardEvent) {
616
+ if (!modalEl || trapTab(modalEl, e)) return;
617
+ e.preventDefault();
618
+ stageEl?.focus();
633
619
  }
634
620
 
635
621
  onMount(() => {
@@ -638,7 +624,7 @@
638
624
  if (e.key === 'Escape') {
639
625
  closeLightbox();
640
626
  } else if (e.key === 'Tab') {
641
- trapTab(e);
627
+ onTab(e);
642
628
  } else if (isGallery && e.key === 'ArrowRight') {
643
629
  e.preventDefault();
644
630
  next();
@@ -920,6 +906,13 @@
920
906
  transition: none;
921
907
  }
922
908
 
909
+ @media (prefers-reduced-motion: reduce) {
910
+ .image-lightbox-thumb,
911
+ .image-lightbox-attached {
912
+ transition: none;
913
+ }
914
+ }
915
+
923
916
  .image-lightbox-thumb:focus-visible {
924
917
  outline: 2px solid var(--border-brand-medium);
925
918
  outline-offset: 2px;
@@ -92,7 +92,7 @@
92
92
 
93
93
  /* Cancel (hover) */
94
94
  --inlineeditactions-cancel-hover-surface: var(--surface-danger-high);
95
- --inlineeditactions-cancel-hover-text: var(--text-danger);
95
+ --inlineeditactions-cancel-hover-text: var(--text-primary);
96
96
  --inlineeditactions-cancel-hover-border: var(--border-danger);
97
97
  --inlineeditactions-cancel-hover-border-width: var(--border-width-0);
98
98
  --inlineeditactions-cancel-hover-radius: var(--radius-md);
@@ -165,7 +165,7 @@
165
165
 
166
166
  /* Default */
167
167
  --input-default-surface: var(--surface-neutral-lower);
168
- --input-default-border: var(--border-neutral);
168
+ --input-default-border: var(--border-neutral-medium);
169
169
  --input-default-text: var(--text-primary);
170
170
  --input-default-text-font-family: var(--font-sans);
171
171
  --input-default-text-font-size: var(--font-size-sm);
@@ -177,7 +177,7 @@
177
177
 
178
178
  /* Focused — applied as a CSS outline so the layout doesn't shift on focus */
179
179
  --input-focused-surface: var(--surface-neutral-lower);
180
- --input-focused-border: var(--border-brand);
180
+ --input-focused-border: var(--border-brand-strong);
181
181
  --input-focused-border-width: var(--border-width-2);
182
182
  --input-focused-text: var(--text-primary);
183
183
  --input-focused-text-font-family: var(--font-sans);
@@ -305,6 +305,13 @@
305
305
  outline: var(--input-error-border-width) solid var(--input-error-border);
306
306
  }
307
307
 
308
+ /* The red outline stays put on focus, so focus adds a neutral ring outside
309
+ it. The spread covers the outline's width, which paints over the shadow. */
310
+ .input-field.invalid .input-control:focus,
311
+ .input-field.invalid.force-focus .input-control {
312
+ box-shadow: 0 0 0 calc(var(--input-error-border-width) + var(--border-width-2)) var(--border-neutral-strong);
313
+ }
314
+
308
315
  .input-icon {
309
316
  color: var(--input-default-icon);
310
317
  font-size: var(--input-default-icon-size);
@@ -40,6 +40,8 @@
40
40
  minWidth?: string;
41
41
  /** ARIA role on the wrapper. `listbox` for select-like menus; `menu` for command menus. */
42
42
  role?: 'listbox' | 'menu';
43
+ /** The list's accessible name, such as the text of the Button that opens it. */
44
+ ariaLabel?: string;
43
45
  /** Selection callback. */
44
46
  onchange?: (value: string) => void;
45
47
  }
@@ -51,6 +53,7 @@
51
53
  forceHoverValue = null,
52
54
  minWidth = undefined,
53
55
  role = 'listbox',
56
+ ariaLabel = undefined,
54
57
  onchange,
55
58
  }: Props = $props();
56
59
 
@@ -67,16 +70,38 @@
67
70
  onchange?.(v);
68
71
  dispatch('change', v);
69
72
  }
73
+
74
+ const buttons: HTMLButtonElement[] = $state([]);
75
+
76
+ // Roving tab stop: the list takes one Tab press, on the selected item.
77
+ let tabStop = $derived(
78
+ items.find((i) => i.value === value && !i.disabled)?.value ?? items.find((i) => !i.disabled)?.value,
79
+ );
80
+
81
+ function moveFocus(event: KeyboardEvent, from: number) {
82
+ const enabled = items.flatMap((item, i) => (item.disabled ? [] : [i]));
83
+ const at = enabled.indexOf(from);
84
+ const to = {
85
+ ArrowDown: enabled[(at + 1) % enabled.length],
86
+ ArrowUp: enabled[(at - 1 + enabled.length) % enabled.length],
87
+ Home: enabled[0],
88
+ End: enabled[enabled.length - 1],
89
+ }[event.key];
90
+ if (to === undefined) return;
91
+ event.preventDefault();
92
+ buttons[to].focus();
93
+ }
70
94
  </script>
71
95
 
72
96
  <ul
73
97
  style:--menuselect-hover-tint-enabled={hoverTintValue}
74
98
  class="menuselect"
75
99
  {role}
100
+ aria-label={ariaLabel}
76
101
  style:min-width={minWidth}
77
102
  >
78
- {#each items as item (item.value)}
79
- <li>
103
+ {#each items as item, i (item.value)}
104
+ <li role="none">
80
105
  <button
81
106
  type="button"
82
107
  class="menuselect-item"
@@ -85,7 +110,10 @@
85
110
  disabled={item.disabled}
86
111
  role={role === 'listbox' ? 'option' : 'menuitem'}
87
112
  aria-selected={role === 'listbox' ? value === item.value : undefined}
113
+ tabindex={item.value === tabStop ? 0 : -1}
114
+ bind:this={buttons[i]}
88
115
  onclick={() => select(item.value)}
116
+ onkeydown={(event) => moveFocus(event, i)}
89
117
  >
90
118
  {#if item.icon}<i class="menuselect-icon {item.icon}" aria-hidden="true"></i>{/if}
91
119
  <span class="menuselect-label">{item.label}</span>
@@ -130,7 +158,7 @@
130
158
  --menuselect-default-icon-size: var(--icon-size-sm);
131
159
 
132
160
  /* Hover item */
133
- --menuselect-hover-surface: var(--surface-neutral-higher);
161
+ --menuselect-hover-surface: var(--surface-neutral-high);
134
162
  --menuselect-hover-text: var(--text-primary);
135
163
  --menuselect-hover-text-font-family: var(--font-sans);
136
164
  --menuselect-hover-text-font-size: var(--font-size-sm);
@@ -188,7 +188,7 @@
188
188
  --notification-warning-text-line-height: var(--line-height-normal);
189
189
 
190
190
  /* Danger */
191
- --notification-danger-surface: var(--surface-danger);
191
+ --notification-danger-surface: var(--surface-danger-low);
192
192
  --notification-danger-action-surface: var(--surface-neutral-lowest);
193
193
  --notification-danger-border: var(--border-danger);
194
194
  --notification-danger-border-width: var(--border-width-1);
@@ -65,7 +65,7 @@
65
65
  --progressbar-track-border-width: var(--border-width-1);
66
66
  --progressbar-track-height: var(--space-16);
67
67
  --progressbar-radius: var(--radius-full);
68
- --progressbar-fill: var(--surface-special-highest);
68
+ --progressbar-fill: var(--border-special-medium);
69
69
  --progressbar-label-gap: var(--space-6);
70
70
  --progressbar-label: var(--text-secondary);
71
71
  --progressbar-label-font-family: var(--font-sans);
@@ -135,4 +135,10 @@
135
135
  border-radius: var(--progressbar-radius);
136
136
  transition: width 0.4s ease;
137
137
  }
138
+
139
+ @media (prefers-reduced-motion: reduce) {
140
+ .progress-fill {
141
+ transition: none;
142
+ }
143
+ }
138
144
  </style>
@@ -38,13 +38,46 @@
38
38
  onclick?.();
39
39
  dispatch('click');
40
40
  }
41
+
42
+ let button: HTMLButtonElement;
43
+
44
+ // Each option renders alone, so the group is read from the DOM: the nearest
45
+ // radiogroup, else the parent the options share.
46
+ function groupOf(radio: HTMLButtonElement): HTMLButtonElement[] {
47
+ const scope = radio.closest('[role="radiogroup"]') ?? radio.parentElement;
48
+ return scope ? [...scope.querySelectorAll<HTMLButtonElement>('.radio-button')] : [radio];
49
+ }
50
+
51
+ // Roving tab stop: the group takes one Tab press, on the checked option.
52
+ $effect(() => {
53
+ void selected;
54
+ const group = groupOf(button);
55
+ const stop = group.find((radio) => radio.getAttribute('aria-checked') === 'true') ?? group[0];
56
+ for (const radio of group) radio.tabIndex = radio === stop ? 0 : -1;
57
+ });
58
+
59
+ const ARROW_STEPS: Record<string, number> = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
60
+
61
+ function moveSelection(event: KeyboardEvent) {
62
+ const step = ARROW_STEPS[event.key];
63
+ if (step === undefined) return;
64
+ event.preventDefault();
65
+ const group = groupOf(button);
66
+ const next = group[(group.indexOf(button) + step + group.length) % group.length];
67
+ next.focus();
68
+ next.click();
69
+ }
41
70
  </script>
42
71
 
43
72
  <button
44
73
  class="radio-button {className}"
45
74
  class:selected
75
+ role="radio"
76
+ aria-checked={selected}
46
77
  style={color ? `--radiobutton-color: ${color};` : ''}
78
+ bind:this={button}
47
79
  onclick={fireClick}
80
+ onkeydown={moveSelection}
48
81
  >
49
82
  <span class="radio-dot"><span class="radio-dot-fill"></span></span>
50
83
  <span class="radio-label">{label}</span>
@@ -53,7 +86,7 @@
53
86
  <style lang="scss">
54
87
  :global(:root) {
55
88
  /* Default */
56
- --radiobutton-default-dot-border-color: var(--border-neutral);
89
+ --radiobutton-default-dot-border-color: var(--border-neutral-medium);
57
90
  --radiobutton-default-dot-border-width: var(--border-width-2);
58
91
  --radiobutton-default-dot-fill: var(--text-secondary);
59
92
  --radiobutton-default-dot-size: var(--dot-size-0);
@@ -64,7 +97,7 @@
64
97
  --radiobutton-default-label-line-height: var(--line-height-none);
65
98
 
66
99
  /* Hover */
67
- --radiobutton-hover-dot-border-color: var(--border-neutral);
100
+ --radiobutton-hover-dot-border-color: var(--border-neutral-medium);
68
101
  --radiobutton-hover-dot-border-width: var(--border-width-2);
69
102
  --radiobutton-hover-dot-fill: var(--text-secondary);
70
103
  --radiobutton-hover-dot-size: var(--dot-size-50);
@@ -75,7 +108,7 @@
75
108
  --radiobutton-hover-label-line-height: var(--line-height-none);
76
109
 
77
110
  /* Selected */
78
- --radiobutton-selected-dot-border-color: var(--border-neutral);
111
+ --radiobutton-selected-dot-border-color: var(--border-neutral-medium);
79
112
  --radiobutton-selected-dot-border-width: var(--border-width-2);
80
113
  --radiobutton-selected-dot-fill: var(--text-secondary);
81
114
  --radiobutton-selected-dot-size: var(--dot-size-50);
@@ -59,6 +59,29 @@
59
59
  onchange?.(v);
60
60
  dispatch('change', v);
61
61
  }
62
+
63
+ const buttons: HTMLButtonElement[] = $state([]);
64
+
65
+ // Roving tab stop: the group takes one Tab press, on the checked segment.
66
+ let tabStop = $derived(
67
+ segments.find((s) => s.value === value && !s.disabled)?.value ?? segments.find((s) => !s.disabled)?.value,
68
+ );
69
+
70
+ const ARROW_STEPS: Record<string, number> = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
71
+
72
+ function moveSelection(event: KeyboardEvent, from: number) {
73
+ const step = ARROW_STEPS[event.key];
74
+ if (step === undefined) return;
75
+ event.preventDefault();
76
+ const count = segments.length;
77
+ for (let n = 1; n < count; n++) {
78
+ const i = (((from + step * n) % count) + count) % count;
79
+ if (segments[i].disabled) continue;
80
+ buttons[i].focus();
81
+ select(segments[i].value);
82
+ return;
83
+ }
84
+ }
62
85
  </script>
63
86
 
64
87
  <div
@@ -77,7 +100,10 @@
77
100
  disabled={disabled || seg.disabled}
78
101
  role="radio"
79
102
  aria-checked={value === seg.value}
103
+ tabindex={seg.value === tabStop ? 0 : -1}
104
+ bind:this={buttons[i]}
80
105
  onclick={() => select(seg.value)}
106
+ onkeydown={(event) => moveSelection(event, i)}
81
107
  >
82
108
  {#if seg.icon}<i class={seg.icon}></i>{/if}
83
109
  <span>{seg.label}</span>
@@ -96,7 +122,7 @@
96
122
 
97
123
  /* Bar (outer wrapper) */
98
124
  --segmentedcontrol-bar-surface: color-mix(in srgb, var(--surface-neutral-lowest) 75%, transparent);
99
- --segmentedcontrol-bar-border: var(--border-neutral);
125
+ --segmentedcontrol-bar-border: var(--border-neutral-medium);
100
126
  --segmentedcontrol-bar-border-width: var(--border-width-1);
101
127
  --segmentedcontrol-bar-radius: var(--radius-lg);
102
128
  --segmentedcontrol-bar-padding: var(--space-4);
@@ -138,7 +164,7 @@
138
164
  --segmentedcontrol-selected-text-font-weight: var(--font-weight-normal);
139
165
  --segmentedcontrol-selected-text-line-height: var(--line-height-normal);
140
166
  --segmentedcontrol-selected-icon: var(--text-secondary);
141
- --segmentedcontrol-selected-border: var(--border-brand);
167
+ --segmentedcontrol-selected-border: var(--border-brand-medium);
142
168
  --segmentedcontrol-selected-border-width: var(--border-width-1);
143
169
  --segmentedcontrol-selected-radius: var(--radius-md);
144
170
 
@@ -245,6 +271,7 @@
245
271
  @include themed-padding(--segmentedcontrol-option-padding, $h: 2);
246
272
  background: transparent;
247
273
  border: 0;
274
+ outline: none;
248
275
  border-radius: var(--segmentedcontrol-selected-radius);
249
276
  color: var(--_text-color);
250
277
  font-family: var(--_text-family);
@@ -308,6 +335,13 @@
308
335
  outline-offset: calc(var(--segmentedcontrol-selected-border-width) * -1);
309
336
  }
310
337
 
338
+ /* A ring outside the box, in a neutral that reads apart from the selected
339
+ option's brand outline. It is a shadow because the selected option
340
+ already owns the outline. */
341
+ .segment:focus-visible {
342
+ box-shadow: 0 0 0 var(--border-width-2) var(--border-neutral-strong);
343
+ }
344
+
311
345
  .segment:disabled {
312
346
  --_text-color: var(--segmentedcontrol-disabled-text);
313
347
  --_text-family: var(--segmentedcontrol-disabled-text-font-family);
@@ -298,7 +298,7 @@
298
298
 
299
299
  --slidepager-control-hover-surface: var(--surface-brand-high);
300
300
  --slidepager-control-hover-border: var(--border-brand);
301
- --slidepager-control-hover-icon: var(--text-inverted);
301
+ --slidepager-control-hover-icon: var(--text-primary);
302
302
 
303
303
  --slidepager-control-disabled-surface: var(--surface-neutral-lower);
304
304
  --slidepager-control-disabled-border: var(--border-neutral-subtle);
@@ -357,7 +357,7 @@
357
357
 
358
358
  .slidepager-stage:focus-visible {
359
359
  outline: none;
360
- box-shadow: var(--ring-focus-md);
360
+ box-shadow: 0 0 0 var(--border-width-2) var(--border-neutral-strong);
361
361
  }
362
362
 
363
363
  .slidepager-stage img {
@@ -438,7 +438,7 @@
438
438
 
439
439
  .slidepager-control:focus-visible {
440
440
  outline: none;
441
- box-shadow: var(--ring-focus-md);
441
+ box-shadow: 0 0 0 var(--border-width-2) var(--border-neutral-strong);
442
442
  }
443
443
 
444
444
  .slidepager-counter {
@@ -131,18 +131,18 @@
131
131
  to one moves the other until the designer unlinks it. */
132
132
  /* Single: one thumb. */
133
133
  --slider-single-track-surface: var(--surface-neutral-lowest);
134
- --slider-single-track-border: var(--border-neutral-subtle);
134
+ --slider-single-track-border: var(--border-neutral-medium);
135
135
  --slider-single-track-border-width: var(--border-width-1);
136
136
  --slider-single-track-height: var(--space-8);
137
137
  --slider-single-track-radius: var(--radius-full);
138
- --slider-single-fill: var(--surface-brand-high);
139
- --slider-single-thumb-surface: var(--surface-neutral-highest);
140
- --slider-single-thumb-border: var(--border-brand);
138
+ --slider-single-fill: var(--border-brand-medium);
139
+ --slider-single-thumb-surface: var(--text-primary);
140
+ --slider-single-thumb-border: var(--border-brand-medium);
141
141
  --slider-single-thumb-border-width: var(--border-width-2);
142
142
  --slider-single-thumb-size: var(--font-size-lg);
143
143
  --slider-single-thumb-radius: var(--radius-full);
144
144
  --slider-single-thumb-shadow: var(--shadow-sm);
145
- --slider-single-hover-thumb-surface: var(--surface-brand-high);
145
+ --slider-single-hover-thumb-surface: var(--text-primary);
146
146
  --slider-single-hover-thumb-border: var(--border-brand-strong);
147
147
  --slider-single-disabled-track-surface: var(--surface-neutral-lower);
148
148
  --slider-single-disabled-fill: var(--surface-neutral);
@@ -151,18 +151,18 @@
151
151
 
152
152
  /* Range: two thumbs bounding a fill. */
153
153
  --slider-range-track-surface: var(--surface-neutral-lowest);
154
- --slider-range-track-border: var(--border-neutral-subtle);
154
+ --slider-range-track-border: var(--border-neutral-medium);
155
155
  --slider-range-track-border-width: var(--border-width-1);
156
156
  --slider-range-track-height: var(--space-8);
157
157
  --slider-range-track-radius: var(--radius-full);
158
- --slider-range-fill: var(--surface-brand-high);
159
- --slider-range-thumb-surface: var(--surface-neutral-highest);
160
- --slider-range-thumb-border: var(--border-brand);
158
+ --slider-range-fill: var(--border-brand-medium);
159
+ --slider-range-thumb-surface: var(--text-primary);
160
+ --slider-range-thumb-border: var(--border-brand-medium);
161
161
  --slider-range-thumb-border-width: var(--border-width-2);
162
162
  --slider-range-thumb-size: var(--font-size-lg);
163
163
  --slider-range-thumb-radius: var(--radius-full);
164
164
  --slider-range-thumb-shadow: var(--shadow-sm);
165
- --slider-range-hover-thumb-surface: var(--surface-brand-high);
165
+ --slider-range-hover-thumb-surface: var(--text-primary);
166
166
  --slider-range-hover-thumb-border: var(--border-brand-strong);
167
167
  --slider-range-disabled-track-surface: var(--surface-neutral-lower);
168
168
  --slider-range-disabled-fill: var(--surface-neutral);
@@ -224,6 +224,10 @@
224
224
  margin-block: calc((var(--_thumb-size) - var(--_track-height)) / 2);
225
225
  border-style: solid;
226
226
  transition: background var(--duration-150), border-color var(--duration-150);
227
+ /* WCAG 2.2 SC 2.5.8: the native thumb takes the pointer over 24px or
228
+ more. A cap smaller than that sits up to half the difference off the
229
+ native thumb's centre at either end of the travel. */
230
+ --_hit-size: max(var(--_thumb-size), var(--space-24));
227
231
  --_origin: calc(var(--_thumb-size) / 2 - var(--_track-border-width));
228
232
  --_travel: calc(100% + var(--_track-border-width) * 2 - var(--_thumb-size));
229
233
  }
@@ -287,7 +291,7 @@
287
291
  top: 50%;
288
292
  left: calc(-1 * var(--_track-border-width));
289
293
  width: calc(100% + var(--_track-border-width) * 2);
290
- height: var(--_thumb-size);
294
+ height: var(--_hit-size);
291
295
  margin: 0;
292
296
  padding: 0;
293
297
  transform: translateY(-50%);
@@ -299,14 +303,14 @@
299
303
  .thumb:disabled { cursor: not-allowed; }
300
304
  .thumb::-webkit-slider-thumb {
301
305
  appearance: none;
302
- width: var(--_thumb-size);
303
- height: var(--_thumb-size);
306
+ width: var(--_hit-size);
307
+ height: var(--_hit-size);
304
308
  opacity: 0;
305
309
  pointer-events: auto;
306
310
  }
307
311
  .thumb::-moz-range-thumb {
308
- width: var(--_thumb-size);
309
- height: var(--_thumb-size);
312
+ width: var(--_hit-size);
313
+ height: var(--_hit-size);
310
314
  border: 0;
311
315
  opacity: 0;
312
316
  pointer-events: auto;