@motion-proto/live-tokens 0.96.0 → 0.98.2

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 (206) 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 +309 -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 +142 -17
  10. package/bin/lib/findings.mjs +20 -4
  11. package/bin/rules/a11y.mjs +53 -0
  12. package/bin/set-colors.mjs +1 -0
  13. package/bin/setup-claude.mjs +2 -2
  14. package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  15. package/dist-plugin/index.cjs +148 -3
  16. package/dist-plugin/index.js +250 -105
  17. package/dist-plugin/setColors/index.cjs +7 -1
  18. package/dist-plugin/setColors/index.d.cts +9 -4
  19. package/dist-plugin/setColors/index.d.ts +9 -4
  20. package/dist-plugin/setColors/index.js +8 -2
  21. package/dist-plugin/setGeometry/index.d.cts +1 -1
  22. package/dist-plugin/setGeometry/index.d.ts +1 -1
  23. package/dist-plugin/setType/index.d.cts +1 -1
  24. package/dist-plugin/setType/index.d.ts +1 -1
  25. package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
  26. package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
  27. package/package.json +49 -2
  28. package/src/demo/LabeledSelect.svelte +1 -1
  29. package/src/editor/bootstrap.ts +3 -1
  30. package/src/editor/component-editor/BadgeEditor.svelte +13 -0
  31. package/src/editor/component-editor/ButtonEditor.svelte +33 -2
  32. package/src/editor/component-editor/CalloutEditor.svelte +13 -0
  33. package/src/editor/component-editor/CardEditor.svelte +12 -2
  34. package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
  35. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
  36. package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
  37. package/src/editor/component-editor/DialogEditor.svelte +15 -0
  38. package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
  39. package/src/editor/component-editor/ImageEditor.svelte +20 -9
  40. package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
  41. package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
  42. package/src/editor/component-editor/InputEditor.svelte +23 -0
  43. package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
  44. package/src/editor/component-editor/NotificationEditor.svelte +15 -0
  45. package/src/editor/component-editor/PanelEditor.svelte +1 -1
  46. package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
  47. package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
  48. package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
  49. package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
  50. package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
  51. package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
  52. package/src/editor/component-editor/SliderEditor.svelte +33 -0
  53. package/src/editor/component-editor/TabBarEditor.svelte +23 -2
  54. package/src/editor/component-editor/TableEditor.svelte +14 -0
  55. package/src/editor/component-editor/ToggleEditor.svelte +17 -0
  56. package/src/editor/component-editor/TooltipEditor.svelte +11 -2
  57. package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
  58. package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
  59. package/src/editor/component-editor/registration.ts +2 -1
  60. package/src/editor/component-editor/registry.ts +69 -27
  61. package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
  62. package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
  63. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
  64. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
  65. package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
  66. package/src/editor/core/a11y/a11yReport.ts +101 -0
  67. package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
  68. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  69. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  70. package/src/editor/core/palettes/contrast.ts +44 -0
  71. package/src/editor/core/palettes/contrastPairs.ts +79 -0
  72. package/src/editor/core/palettes/resolveColor.ts +77 -0
  73. package/src/editor/core/store/editorPersistence.ts +9 -1
  74. package/src/editor/core/store/editorRenderer.ts +2 -0
  75. package/src/editor/core/store/editorStore.ts +29 -0
  76. package/src/editor/core/store/editorTypes.ts +9 -0
  77. package/src/editor/core/store/editorViewStore.ts +2 -2
  78. package/src/editor/core/store/unsavedEdits.ts +10 -0
  79. package/src/editor/core/themes/buildColors.ts +16 -5
  80. package/src/editor/core/themes/liveStateStream.ts +7 -1
  81. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  82. package/src/editor/core/themes/slices/customColors.ts +226 -0
  83. package/src/editor/core/themes/themeTypes.ts +10 -0
  84. package/src/editor/docs/chapters.ts +1 -0
  85. package/src/editor/docs/content/accessibility.md +362 -0
  86. package/src/editor/docs/content/editing-tokens.md +56 -1
  87. package/src/editor/docs/content.generated.ts +2 -1
  88. package/src/editor/pages/ComponentEditorPage.svelte +10 -1
  89. package/src/editor/pages/EditorShell.svelte +97 -10
  90. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  91. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  92. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  93. package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
  94. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  95. package/src/editor/ui/GradientEditor.svelte +9 -1
  96. package/src/editor/ui/ThemePanel.svelte +4 -4
  97. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  98. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  99. package/src/editor/ui/UIPaletteSelector.svelte +347 -15
  100. package/src/editor/ui/UITokenSelector.svelte +33 -14
  101. package/src/editor/ui/VariablesTab.svelte +3 -0
  102. package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
  103. package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
  104. package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
  105. package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
  106. package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
  107. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  108. package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
  109. package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
  110. package/src/editor/ui/a11y/a11yView.ts +218 -0
  111. package/src/editor/ui/anchorToTrigger.ts +63 -0
  112. package/src/editor/ui/contrastReadout.ts +45 -0
  113. package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
  114. package/src/live-tokens/data/themes/autumn.json +20 -20
  115. package/src/live-tokens/data/themes/halloween.json +20 -20
  116. package/src/live-tokens/data/themes/midnight-study.json +20 -20
  117. package/src/live-tokens/data/themes/ocean.json +20 -20
  118. package/src/live-tokens/data/themes/royal-velvet.json +20 -20
  119. package/src/live-tokens/data/themes/sketchy.json +20 -20
  120. package/src/live-tokens/data/themes/spring-meadow.json +20 -20
  121. package/src/live-tokens/data/themes/sunset.json +20 -20
  122. package/src/system/components/Badge.svelte +3 -3
  123. package/src/system/components/Button.svelte +9 -3
  124. package/src/system/components/CodeSnippet.svelte +8 -1
  125. package/src/system/components/CornerBadge.svelte +3 -3
  126. package/src/system/components/Dialog.svelte +43 -14
  127. package/src/system/components/IconButton.svelte +1 -1
  128. package/src/system/components/Image.svelte +7 -0
  129. package/src/system/components/ImageLightbox.svelte +26 -26
  130. package/src/system/components/Input.svelte +8 -1
  131. package/src/system/components/MenuSelect.svelte +30 -2
  132. package/src/system/components/Notification.svelte +1 -1
  133. package/src/system/components/ProgressBar.svelte +6 -0
  134. package/src/system/components/RadioButton.svelte +36 -3
  135. package/src/system/components/SegmentedControl.svelte +36 -2
  136. package/src/system/components/SlidePager.svelte +2 -2
  137. package/src/system/components/Slider.svelte +13 -9
  138. package/src/system/components/TabBar.svelte +33 -3
  139. package/src/system/components/Toggle.svelte +2 -2
  140. package/src/system/components/VideoFrame.svelte +1 -1
  141. package/src/system/components/VideoLightbox.svelte +2 -2
  142. package/src/system/internal/focusTrap.ts +35 -0
  143. package/src/testing-js/__aria__/badge.yml +40 -0
  144. package/src/testing-js/__aria__/button.yml +88 -0
  145. package/src/testing-js/__aria__/callout.yml +12 -0
  146. package/src/testing-js/__aria__/card.yml +8 -0
  147. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  148. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  149. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  150. package/src/testing-js/__aria__/dialog.yml +42 -0
  151. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  152. package/src/testing-js/__aria__/image.yml +10 -0
  153. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  154. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  155. package/src/testing-js/__aria__/input.yml +61 -0
  156. package/src/testing-js/__aria__/menuselect.yml +30 -0
  157. package/src/testing-js/__aria__/notification.yml +30 -0
  158. package/src/testing-js/__aria__/panel.yml +2 -0
  159. package/src/testing-js/__aria__/progressbar.yml +2 -0
  160. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  161. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  162. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  163. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  164. package/src/testing-js/__aria__/slidepager.yml +72 -0
  165. package/src/testing-js/__aria__/slider.yml +28 -0
  166. package/src/testing-js/__aria__/tabbar.yml +35 -0
  167. package/src/testing-js/__aria__/table.yml +144 -0
  168. package/src/testing-js/__aria__/toggle.yml +12 -0
  169. package/src/testing-js/__aria__/tooltip.yml +2 -0
  170. package/src/testing-js/__aria__/videoframe.yml +6 -0
  171. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  172. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  173. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
  174. package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
  175. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  176. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  177. package/src/testing-js/chunk-EWSGE27V.js +168 -0
  178. package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
  179. package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
  180. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
  181. package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
  182. package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
  183. package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
  184. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  185. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  186. package/src/testing-js/component-a11y.contract.js +283 -0
  187. package/src/testing-js/component-a11y.contract.js.map +1 -0
  188. package/src/testing-js/component-alias.contract.js +2 -2
  189. package/src/testing-js/component-behavior.contract.js +2 -2
  190. package/src/testing-js/component-editor.contract.js +6 -5
  191. package/src/testing-js/component-editor.contract.js.map +1 -1
  192. package/src/testing-js/component-render.contract.js +10 -6
  193. package/src/testing-js/component-render.contract.js.map +1 -1
  194. package/src/testing-js/index.d.ts +44 -16
  195. package/src/testing-js/index.js +9 -9
  196. package/src/testing-js/page-compliance.contract.js +163 -18
  197. package/src/testing-js/page-compliance.contract.js.map +1 -1
  198. package/src/testing-js/vitest.js +3 -3
  199. package/template/package.json +9 -0
  200. package/src/editor/ui/keepInViewport.ts +0 -84
  201. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  202. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  203. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  204. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  205. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  206. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildSiblings } from './scaffolding/siblings';
3
3
  import type { Token, IntrinsicSpec } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'iconbutton';
6
7
 
@@ -104,6 +105,32 @@
104
105
  default: { default: 'var(--color-transparent)' },
105
106
  },
106
107
  ];
108
+
109
+ function variantContrastPairs(v: Variant): ContrastPair[] {
110
+ const icon = (s: StateName) => `${statePrefix(v, s)}-icon`;
111
+ const surface = (s: StateName) => `${statePrefix(v, s)}-surface`;
112
+ const hoverStack = ['--iconbutton-hover-tint-enabled', surface('hover')];
113
+ const disabled: ContrastPair = { foreground: icon('disabled'), backdrop: [surface('disabled')], role: 'non-text', inactive: true };
114
+ if (v !== 'outline') {
115
+ return [
116
+ { foreground: icon('default'), backdrop: [surface('default')], role: 'non-text' },
117
+ { foreground: icon('hover'), backdrop: hoverStack, role: 'non-text' },
118
+ disabled,
119
+ ];
120
+ }
121
+ // `:active` repaints the surface without the tint. A pointer press keeps the
122
+ // hover icon color, and a key press keeps the default one.
123
+ const active = ['--iconbutton-outline-active-surface'];
124
+ return [
125
+ { foreground: icon('default'), backdrop: [surface('default')], role: 'non-text', context: 'default' },
126
+ { foreground: icon('default'), backdrop: active, role: 'non-text', context: 'active' },
127
+ { foreground: icon('hover'), backdrop: hoverStack, role: 'non-text', context: 'hover' },
128
+ { foreground: icon('hover'), backdrop: active, role: 'non-text', context: 'active' },
129
+ disabled,
130
+ ];
131
+ }
132
+
133
+ export const contrastPairs: ContrastPair[] = variants.flatMap(variantContrastPairs);
107
134
  </script>
108
135
 
109
136
  <script lang="ts">
@@ -113,6 +140,7 @@
113
140
  import { editorState, setComponentAlias, clearComponentAlias } from '../core/store/editorStore';
114
141
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
115
142
  import Toggle from '../ui/Toggle.svelte';
143
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
116
144
  import UIPaletteSelector from '../ui/UIPaletteSelector.svelte';
117
145
 
118
146
  // One switch. Turning the tint on also stands each hover surface down to its
@@ -193,7 +221,7 @@
193
221
  {#snippet extraPropertyRows(stateName)}
194
222
  {#if stateName === 'hover'}
195
223
  <div class="property-row" data-token-variables="--iconbutton-hover-tint-enabled">
196
- <span class="property-label">tint layer</span>
224
+ <span class="property-label">tint layer<A11yPropertyMark variables={['--iconbutton-hover-tint-enabled']} /></span>
197
225
  <Toggle checked={tintOn} onchange={setTintOn} />
198
226
  </div>
199
227
  {#if tintOn}
@@ -50,6 +50,7 @@
50
50
  import VariantGroup from './scaffolding/VariantGroup.svelte';
51
51
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
52
52
  import { buildSiblings } from './scaffolding/siblings';
53
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
53
54
  import demoImageUrl from '../../system/assets/newspaper.webp';
54
55
  import { editorState, setComponentAlias } from '../core/store/editorStore';
55
56
 
@@ -77,17 +78,23 @@
77
78
  <ComponentEditorBase {component} title="Image" description="Picture in the page flow. Default frames it with rounded corners, a border, and a shadow. Bare shows the picture alone." tokens={allTokens} variants={variantOptions}>
78
79
  <VariantGroup name="default" title="Default" states={variantStates('default')} {component} siblings={buildSiblings(variants, 'default', variantStates)}>
79
80
  {#snippet stateActions()}
80
- <label class="zoom-enable">
81
- <input type="checkbox" checked={useZoom} onchange={(e) => setUseZoom(e.currentTarget.checked)} />
82
- <span>Use zoom on hover</span>
83
- </label>
84
- <label class="zoom-enable" class:disabled={!useZoom}>
85
- <input type="checkbox" checked={overflowScaling} disabled={!useZoom} onchange={(e) => setOverflowScaling(e.currentTarget.checked)} />
86
- <span>Overflow scaling (mask zoom to frame)</span>
87
- </label>
81
+ <span class="zoom-control" data-token-variables="--image-zoom-enabled">
82
+ <label class="zoom-enable">
83
+ <input type="checkbox" checked={useZoom} onchange={(e) => setUseZoom(e.currentTarget.checked)} />
84
+ <span>Use zoom on hover</span>
85
+ </label>
86
+ <A11yPropertyMark variables={['--image-zoom-enabled']} />
87
+ </span>
88
+ <span class="zoom-control" data-token-variables="--image-grow-enabled">
89
+ <label class="zoom-enable" class:disabled={!useZoom}>
90
+ <input type="checkbox" checked={overflowScaling} disabled={!useZoom} onchange={(e) => setOverflowScaling(e.currentTarget.checked)} />
91
+ <span>Overflow scaling (mask zoom to frame)</span>
92
+ </label>
93
+ <A11yPropertyMark variables={['--image-grow-enabled']} />
94
+ </span>
88
95
  {/snippet}
89
96
  <Image src={demoImageUrl} alt="Demo" size="banner" zoom={useZoom ? undefined : false} {overflowScaling} forceHover={useZoom} />
90
- <p class="zoom-help">
97
+ <p class="zoom-help" data-preview-demo>
91
98
  Use zoom on hover: every image scales on hover (a page can still force it per image with the
92
99
  <code>zoom</code> prop). Overflow scaling on (default): the content zooms inside the fixed frame,
93
100
  masked by overflow. Off: the whole framed image grows past its box. Override per image with
@@ -100,6 +107,10 @@
100
107
  </ComponentEditorBase>
101
108
 
102
109
  <style>
110
+ .zoom-control {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ }
103
114
  .zoom-enable {
104
115
  display: inline-flex;
105
116
  align-items: center;
@@ -1,5 +1,6 @@
1
1
  <script module lang="ts">
2
2
  import type { Token, IntrinsicSpec } from './scaffolding/types';
3
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
3
4
 
4
5
  export const component = 'imagelightbox';
5
6
 
@@ -39,13 +40,26 @@
39
40
  default: { default: 'contain' },
40
41
  },
41
42
  ];
43
+
44
+ // One color paints the chrome's icons and its extra-small counter text. The
45
+ // chrome floats on the scrim.
46
+ const scrim = '--imagelightbox-scrim-surface';
47
+ export const contrastPairs: ContrastPair[] = [
48
+ { foreground: '--imagelightbox-chrome-icon', backdrop: ['--imagelightbox-chrome-surface', scrim], role: 'non-text', context: 'icons' },
49
+ { foreground: '--imagelightbox-chrome-icon', backdrop: ['--imagelightbox-chrome-hover-surface', scrim], role: 'non-text', context: 'hovered icons' },
50
+ { foreground: '--imagelightbox-chrome-icon', backdrop: ['--imagelightbox-chrome-surface', scrim], role: 'text', fontSize: '--font-size-xs', context: 'counter' },
51
+ ];
42
52
  </script>
43
53
 
44
54
  <script lang="ts">
45
55
  import ImageLightbox from '../../system/components/ImageLightbox.svelte';
46
56
  import VariantGroup from './scaffolding/VariantGroup.svelte';
47
57
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
58
+ import UISegmentedControl from '../ui/UISegmentedControl.svelte';
59
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
48
60
  import newspaperUrl from '../../system/assets/newspaper.webp';
61
+ import { editorState, setComponentAlias } from '../core/store/editorStore';
62
+ import type { CssVarRef } from '../core/store/editorTypes';
49
63
 
50
64
  const demoImages = [
51
65
  // The first entry carries explicit dimensions (the no-reflow path); the second
@@ -56,6 +70,18 @@
56
70
 
57
71
  let multiple = $state(true);
58
72
  let shadow = $state<'box' | 'content' | 'none'>('box');
73
+
74
+ type TileFit = 'contain' | 'cover';
75
+ const TILE_FIT = intrinsics[0].variable('default');
76
+ const fitOptions: { value: TileFit; label: string; title: string }[] = [
77
+ { value: 'contain', label: 'Contain', title: 'Show the whole image inside the tile' },
78
+ { value: 'cover', label: 'Cover', title: 'Crop the image to fill the tile' },
79
+ ];
80
+ let tileFit = $derived.by((): TileFit => {
81
+ const ref = ($editorState.components[component]?.aliases as Record<string, CssVarRef> | undefined)?.[TILE_FIT];
82
+ const value = ref?.kind === 'literal' ? ref.value : intrinsics[0].default.default;
83
+ return value === 'cover' ? 'cover' : 'contain';
84
+ });
59
85
  </script>
60
86
 
61
87
  <ComponentEditorBase
@@ -79,6 +105,21 @@
79
105
  </select>
80
106
  </label>
81
107
  {/snippet}
108
+ {#snippet extraPropertyRowsTop(stateName)}
109
+ {#if stateName === 'tile'}
110
+ <div class="property-row" data-token-variables={TILE_FIT}>
111
+ <span class="property-label">tile fit<A11yPropertyMark variables={[TILE_FIT]} /></span>
112
+ <div class="tile-fit-control">
113
+ <UISegmentedControl
114
+ value={tileFit}
115
+ options={fitOptions}
116
+ ariaLabel="Tile fit"
117
+ onchange={(value) => setComponentAlias(component, TILE_FIT, { kind: 'literal', value })}
118
+ />
119
+ </div>
120
+ </div>
121
+ {/if}
122
+ {/snippet}
82
123
  <div class="preview-frame">
83
124
  <ImageLightbox images={multiple ? demoImages : [demoImages[0]]} {shadow} extended />
84
125
  </div>
@@ -91,6 +132,9 @@
91
132
  max-width: 28rem;
92
133
  margin: 0 auto;
93
134
  }
135
+ .tile-fit-control {
136
+ display: flex;
137
+ }
94
138
  .preview-toggle {
95
139
  display: inline-flex;
96
140
  align-items: center;
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildSiblings } from './scaffolding/siblings';
3
3
  import type { Token } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'inlineeditactions';
6
7
 
@@ -38,6 +39,17 @@
38
39
  [`--inlineeditactions-${btn}-default-padding`, btn] as const,
39
40
  [`--inlineeditactions-${btn}-default-icon-size`, btn] as const,
40
41
  ]));
42
+
43
+ // Each button holds a glyph and no text, so its color is a non-text pair.
44
+ function buttonContrastPairs(btn: Button): ContrastPair[] {
45
+ return (['default', 'hover'] as const).map((state) => ({
46
+ foreground: `--inlineeditactions-${btn}-${state}-text`,
47
+ backdrop: [`--inlineeditactions-${btn}-${state}-surface`],
48
+ role: 'non-text',
49
+ }));
50
+ }
51
+
52
+ export const contrastPairs: ContrastPair[] = buttons.flatMap(buttonContrastPairs);
41
53
  </script>
42
54
 
43
55
  <script lang="ts">
@@ -66,7 +78,7 @@
66
78
  {#snippet children({ activeState })}
67
79
  {@const forceClass = activeState === 'hover' ? 'force-hover' : ''}
68
80
  <div class="inline-edit-demo-row">
69
- <span style="color: var(--text-secondary);">Editing value...</span>
81
+ <span style="color: var(--text-secondary);" data-preview-demo>Editing value...</span>
70
82
  <InlineEditActions
71
83
  onsave={() => {}}
72
84
  oncancel={() => {}}
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildTypeGroupTokens, buildTypeGroupShareableContexts } from './scaffolding/buildTypeGroupTokens';
3
3
  import type { Token, TypeGroupConfig } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'input';
6
7
 
@@ -146,6 +147,28 @@
146
147
  // Silence unused-import lint without changing behavior; buildTypeGroupTokens stays available
147
148
  // for editors that want auto-derived color rows, but we keep ours hand-built for slot-prefixed groupKeys.
148
149
  void buildTypeGroupTokens;
150
+
151
+ function textPair(foreground: string, state: string, backdrop: string[]): ContrastPair {
152
+ const g = typeGroups[state][0];
153
+ return { foreground, backdrop, role: 'text', fontSize: g.sizeVariable, fontWeight: g.weightVariable };
154
+ }
155
+
156
+ // The placeholder paints on each state's surface at that state's text size.
157
+ // The icon button takes the focused icon color on hover and focus while the
158
+ // field itself stays unfocused. Label, hint and error sit on no surface.
159
+ export const contrastPairs: ContrastPair[] = [
160
+ textPair('--input-default-text', 'default', ['--input-default-surface']),
161
+ textPair('--input-focused-text', 'focused', ['--input-focused-surface']),
162
+ { ...textPair('--input-disabled-text', 'disabled', ['--input-disabled-surface']), inactive: true },
163
+ { ...textPair('--input-default-placeholder', 'default', ['--input-default-surface']), context: 'default' },
164
+ { ...textPair('--input-default-placeholder', 'focused', ['--input-focused-surface']), context: 'focused' },
165
+ { ...textPair('--input-default-placeholder', 'disabled', ['--input-disabled-surface']), context: 'disabled', inactive: true },
166
+ { foreground: '--input-default-icon', backdrop: ['--input-default-surface'], role: 'non-text' },
167
+ { foreground: '--input-focused-icon', backdrop: ['--input-focused-surface'], role: 'non-text', context: 'focused' },
168
+ { foreground: '--input-focused-icon', backdrop: ['--input-default-surface'], role: 'non-text', context: 'icon button' },
169
+ { foreground: '--input-disabled-icon', backdrop: ['--input-disabled-surface'], role: 'non-text', inactive: true },
170
+ ...(['label', 'hint', 'error'] as const).map((slot) => textPair(`--input-${slot}`, slot, [])),
171
+ ];
149
172
  </script>
150
173
 
151
174
  <script lang="ts">
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildTypeGroupTokens, buildTypeGroupShareableContexts } from './scaffolding/buildTypeGroupTokens';
3
3
  import type { Token, TypeGroupConfig, IntrinsicSpec } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'menuselect';
6
7
 
@@ -106,6 +107,29 @@
106
107
  default: { default: 'var(--color-transparent)' },
107
108
  },
108
109
  ];
110
+
111
+ // Every item sits on the menu surface. The check mark paints on the selected item alone.
112
+ const menu = '--menuselect-menu-surface';
113
+ function itemContrastPairs(s: 'default' | 'hover' | 'selected' | 'disabled'): ContrastPair[] {
114
+ const backdrop = s === 'hover'
115
+ ? ['--menuselect-hover-tint-enabled', '--menuselect-hover-surface', menu]
116
+ : [`--menuselect-${s}-surface`, menu];
117
+ const inactive = s === 'disabled' ? { inactive: true } : {};
118
+ const marks = s === 'selected' ? ['--menuselect-selected-icon', '--menuselect-selected-indicator'] : [`--menuselect-${s}-icon`];
119
+ return [
120
+ ...typeGroups[`${s} item`].map((g): ContrastPair => ({
121
+ foreground: g.colorVariable,
122
+ backdrop,
123
+ role: 'text',
124
+ fontSize: g.sizeVariable,
125
+ fontWeight: g.weightVariable,
126
+ ...inactive,
127
+ })),
128
+ ...marks.map((foreground): ContrastPair => ({ foreground, backdrop, role: 'non-text', ...inactive })),
129
+ ];
130
+ }
131
+
132
+ export const contrastPairs: ContrastPair[] = (['default', 'hover', 'selected', 'disabled'] as const).flatMap(itemContrastPairs);
109
133
  </script>
110
134
 
111
135
  <script lang="ts">
@@ -115,6 +139,7 @@
115
139
  import { editorState, setComponentAlias, clearComponentAlias } from '../core/store/editorStore';
116
140
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
117
141
  import Toggle from '../ui/Toggle.svelte';
142
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
118
143
  import UIPaletteSelector from '../ui/UIPaletteSelector.svelte';
119
144
 
120
145
  // One switch. Turning the tint on also stands each hover surface down to its
@@ -178,7 +203,7 @@
178
203
  {#snippet extraPropertyRows(stateName)}
179
204
  {#if stateName === 'hover item'}
180
205
  <div class="property-row" data-token-variables="--menuselect-hover-tint-enabled">
181
- <span class="property-label">tint layer</span>
206
+ <span class="property-label">tint layer<A11yPropertyMark variables={['--menuselect-hover-tint-enabled']} /></span>
182
207
  <Toggle checked={tintOn} onchange={setTintOn} />
183
208
  </div>
184
209
  {#if tintOn}
@@ -203,6 +228,7 @@
203
228
  items={previewItems}
204
229
  value={previewValue}
205
230
  forceHoverValue={previewForceHover}
231
+ ariaLabel="Options"
206
232
  />
207
233
  {/snippet}
208
234
  </VariantGroup>
@@ -1,5 +1,6 @@
1
1
  <script module lang="ts">
2
2
  import type { Token } from './scaffolding/types';
3
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
3
4
 
4
5
  export const component = 'notification';
5
6
  const variants = ['info', 'success', 'warning', 'danger'] as const;
@@ -67,6 +68,20 @@
67
68
  }
68
69
 
69
70
  const variantOptions = variants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
71
+
72
+ // The body color also paints the dismiss button on the header. The body has
73
+ // no surface, and size="small" sets its description in --font-size-sm.
74
+ function variantContrastPairs(v: Variant): ContrastPair[] {
75
+ const header = [`--notification-${v}-surface`];
76
+ return [
77
+ { foreground: `--notification-${v}-title`, backdrop: header, role: 'text', fontSize: `--notification-${v}-title-font-size`, fontWeight: `--notification-${v}-title-font-weight` },
78
+ { foreground: `--notification-${v}-icon`, backdrop: header, role: 'non-text' },
79
+ { foreground: `--notification-${v}-text`, backdrop: header, role: 'non-text', context: 'dismiss button' },
80
+ { foreground: `--notification-${v}-text`, backdrop: [], role: 'text', fontSize: '--font-size-sm', fontWeight: `--notification-${v}-text-font-weight`, context: 'body' },
81
+ ];
82
+ }
83
+
84
+ export const contrastPairs: ContrastPair[] = variants.flatMap(variantContrastPairs);
70
85
  </script>
71
86
 
72
87
  <script lang="ts">
@@ -58,7 +58,7 @@
58
58
  {#snippet children()}
59
59
  <div class="panel-demo">
60
60
  <Panel minHeight="6rem">
61
- <div class="content-placeholder">Content Placeholder</div>
61
+ <div class="content-placeholder" data-preview-demo>Content Placeholder</div>
62
62
  </Panel>
63
63
  </div>
64
64
  {/snippet}
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
3
3
  import type { Token, TypeGroupConfig } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'progressbar';
6
7
 
@@ -71,6 +72,18 @@
71
72
  ...buildTypeGroupColorTokens(typeGroups, { component }),
72
73
  ...typeGroupTokens,
73
74
  ];
75
+
76
+ // The label and value sit on no surface. The fill paints on the track.
77
+ export const contrastPairs: ContrastPair[] = [
78
+ ...typeGroups.default.map((g): ContrastPair => ({
79
+ foreground: g.colorVariable,
80
+ backdrop: [],
81
+ role: 'text',
82
+ fontSize: g.sizeVariable,
83
+ fontWeight: g.weightVariable,
84
+ })),
85
+ { foreground: '--progressbar-fill', backdrop: ['--progressbar-track-surface'], role: 'non-text' },
86
+ ];
74
87
  </script>
75
88
 
76
89
  <script lang="ts">
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
3
3
  import type { Token, TypeGroupConfig } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'radiobutton';
6
7
 
@@ -79,6 +80,24 @@
79
80
  ...buildTypeGroupColorTokens(typeGroups, { component, variants: Object.keys(states) }),
80
81
  ...typeGroupTokens,
81
82
  ];
83
+
84
+ // The button paints no surface without the `color` prop, and the default dot
85
+ // stays hidden.
86
+ function stateContrastPairs(s: 'default' | 'hover' | 'selected'): ContrastPair[] {
87
+ const marks = s === 'default' ? ['--radiobutton-default-dot-border-color'] : [`--radiobutton-${s}-dot-border-color`, `--radiobutton-${s}-dot-fill`];
88
+ return [
89
+ ...typeGroups[s].map((g): ContrastPair => ({
90
+ foreground: g.colorVariable,
91
+ backdrop: [],
92
+ role: 'text',
93
+ fontSize: g.sizeVariable,
94
+ fontWeight: g.weightVariable,
95
+ })),
96
+ ...marks.map((foreground): ContrastPair => ({ foreground, backdrop: [], role: 'non-text' })),
97
+ ];
98
+ }
99
+
100
+ export const contrastPairs: ContrastPair[] = (['default', 'hover', 'selected'] as const).flatMap(stateContrastPairs);
82
101
  </script>
83
102
 
84
103
  <script lang="ts">
@@ -109,7 +128,7 @@
109
128
  {#snippet children({ activeState })}
110
129
  {@const forceClass = activeState === 'hover' ? 'force-hover' : ''}
111
130
  {@const forceSelected = activeState === 'selected'}
112
- <div class="radio-demo-row">
131
+ <div class="radio-demo-row" role="radiogroup" aria-label="Priority">
113
132
  <RadioButton
114
133
  label="Defense"
115
134
  selected={forceSelected || selectedRadio === 'option-a'}
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildSiblings } from './scaffolding/siblings';
3
3
  import type { Token, TypeGroupConfig, IntrinsicSpec } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'sectiondivider';
6
7
 
@@ -193,6 +194,22 @@
193
194
  },
194
195
  ];
195
196
  const INTRINSIC_BY_KEY = new Map(intrinsics.map((i) => [i.key, i]));
197
+
198
+ function variantContrastPairs(v: Variant): ContrastPair[] {
199
+ const backdrop = [`--sectiondivider-${v}-surface`];
200
+ return [
201
+ ...variantTypeGroups(v).map((g): ContrastPair => ({
202
+ foreground: g.colorVariable,
203
+ backdrop,
204
+ role: 'text',
205
+ fontSize: g.sizeVariable,
206
+ fontWeight: g.weightVariable,
207
+ })),
208
+ { foreground: `--sectiondivider-${v}-hairline-color`, backdrop, role: 'non-text' },
209
+ ];
210
+ }
211
+
212
+ export const contrastPairs: ContrastPair[] = variants.flatMap((v) => variantContrastPairs(v.key));
196
213
  </script>
197
214
 
198
215
  <script lang="ts">
@@ -200,6 +217,7 @@
200
217
  import VariantGroup from './scaffolding/VariantGroup.svelte';
201
218
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
202
219
  import GradientEditor from '../ui/GradientEditor.svelte';
220
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
203
221
  import { editorState, mutate, setComponentAlias, setComponentConfig } from '../core/store/editorStore';
204
222
  import { componentGradientSource } from '../core/store/gradientSource';
205
223
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
@@ -376,16 +394,19 @@
376
394
  description: {
377
395
  checked: getShowDescription(v),
378
396
  label: 'Show description',
397
+ variables: [`--sectiondivider-${v}-description-display`],
379
398
  onchange: (c: boolean) => setIntrinsic(v, 'description-display', c ? 'flex' : 'none'),
380
399
  },
381
400
  eyebrow: {
382
401
  checked: getShowEyebrow(v),
383
402
  label: 'Show eyebrow',
403
+ variables: [`--sectiondivider-${v}-eyebrow-display`],
384
404
  onchange: (c: boolean) => setIntrinsic(v, 'eyebrow-display', c ? 'block' : 'none'),
385
405
  },
386
406
  hairline: {
387
407
  checked: getShowHairline(v),
388
408
  label: 'Show',
409
+ variables: [`--sectiondivider-${v}-hairline`],
389
410
  onchange: (c: boolean) => setIntrinsic(v, 'hairline', c ? getHairlinePosition(v) : 'none'),
390
411
  },
391
412
  };
@@ -431,8 +452,8 @@
431
452
  </div>
432
453
  {/snippet}
433
454
  {#snippet extraPropertyRowsTop(_stateName)}
434
- <div class="property-row sd-intrinsic-row">
435
- <span class="property-label">alignment</span>
455
+ <div class="property-row sd-intrinsic-row" data-token-variables={`--sectiondivider-${v.key}-align`}>
456
+ <span class="property-label">alignment<A11yPropertyMark variables={[`--sectiondivider-${v.key}-align`]} /></span>
436
457
  <select
437
458
  class="sd-intrinsic-select"
438
459
  value={getAlign(v.key)}
@@ -462,18 +483,21 @@
462
483
  {/snippet}
463
484
  {#snippet elementExtras(elementName)}
464
485
  {#if elementName === 'eyebrow'}
465
- <label class="sd-element-check">
466
- <input
467
- type="checkbox"
468
- checked={getEyebrowUppercase(v.key)}
469
- onchange={(e) => setIntrinsic(v.key, 'eyebrow-text-transform', (e.currentTarget as HTMLInputElement).checked ? 'uppercase' : 'none')}
470
- />
471
- <span>All caps</span>
472
- </label>
486
+ <span class="sd-element-check-host" data-token-variables={`--sectiondivider-${v.key}-eyebrow-text-transform`}>
487
+ <label class="sd-element-check">
488
+ <input
489
+ type="checkbox"
490
+ checked={getEyebrowUppercase(v.key)}
491
+ onchange={(e) => setIntrinsic(v.key, 'eyebrow-text-transform', (e.currentTarget as HTMLInputElement).checked ? 'uppercase' : 'none')}
492
+ />
493
+ <span>All caps</span>
494
+ </label>
495
+ <A11yPropertyMark variables={[`--sectiondivider-${v.key}-eyebrow-text-transform`]} />
496
+ </span>
473
497
  {/if}
474
498
  {#if elementName === 'hairline'}
475
- <div class="property-row sd-intrinsic-row sd-hairline-position-row">
476
- <span class="property-label">position</span>
499
+ <div class="property-row sd-intrinsic-row sd-hairline-position-row" data-token-variables={`--sectiondivider-${v.key}-hairline`}>
500
+ <span class="property-label">position<A11yPropertyMark variables={[`--sectiondivider-${v.key}-hairline`]} /></span>
477
501
  <select
478
502
  class="sd-intrinsic-select"
479
503
  value={getHairlinePosition(v.key)}
@@ -551,6 +575,11 @@
551
575
  /* Inline boolean control inside an element-section (e.g. "All caps" under
552
576
  the eyebrow heading). Sits flush-left so it reads as a section property
553
577
  rather than aligning with the token grid below. */
578
+ :global(.sd-element-check-host) {
579
+ display: inline-flex;
580
+ align-items: center;
581
+ margin-bottom: var(--ui-space-4);
582
+ }
554
583
  :global(.sd-element-check) {
555
584
  display: inline-flex;
556
585
  align-items: center;
@@ -559,7 +588,6 @@
559
588
  color: var(--ui-text-secondary);
560
589
  cursor: pointer;
561
590
  user-select: none;
562
- margin-bottom: var(--ui-space-4);
563
591
  }
564
592
  :global(.sd-element-check:hover) { color: var(--ui-text-primary); }
565
593
  :global(.sd-element-check input) { margin: 0; cursor: pointer; }
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { buildTypeGroupTokens, buildTypeGroupShareableContexts } from './scaffolding/buildTypeGroupTokens';
3
3
  import type { Token, TypeGroupConfig, IntrinsicSpec } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'segmentedcontrol';
6
7
 
@@ -140,6 +141,32 @@
140
141
  default: { default: 'var(--color-transparent)' },
141
142
  },
142
143
  ];
144
+
145
+ // Every option sits on the bar, and the default option paints no surface of
146
+ // its own. The small size sets every option's text in
147
+ // --segmentedcontrol-option-small-text-font-size, the smaller of its two sizes.
148
+ function optionContrastPairs(state: string, icon: string, backdrop: string[]): ContrastPair[] {
149
+ const inactive = state === 'disabled option' ? { inactive: true } : {};
150
+ return [
151
+ ...typeGroups[state].map((g): ContrastPair => ({
152
+ foreground: g.colorVariable,
153
+ backdrop,
154
+ role: 'text',
155
+ fontSize: '--segmentedcontrol-option-small-text-font-size',
156
+ fontWeight: g.weightVariable,
157
+ ...inactive,
158
+ })),
159
+ { foreground: icon, backdrop, role: 'non-text', ...inactive },
160
+ ];
161
+ }
162
+
163
+ const bar = '--segmentedcontrol-bar-surface';
164
+ export const contrastPairs: ContrastPair[] = [
165
+ ...optionContrastPairs('default option', '--segmentedcontrol-option-icon', [bar]),
166
+ ...optionContrastPairs('hover option', '--segmentedcontrol-option-hover-icon', ['--segmentedcontrol-hover-tint-enabled', '--segmentedcontrol-option-hover-surface', bar]),
167
+ ...optionContrastPairs('selected option', '--segmentedcontrol-selected-icon', ['--segmentedcontrol-selected-surface', bar]),
168
+ ...optionContrastPairs('disabled option', '--segmentedcontrol-disabled-icon', ['--segmentedcontrol-disabled-surface', bar]),
169
+ ];
143
170
  </script>
144
171
 
145
172
  <script lang="ts">
@@ -149,6 +176,7 @@
149
176
  import { editorState, setComponentAlias, clearComponentAlias } from '../core/store/editorStore';
150
177
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
151
178
  import Toggle from '../ui/Toggle.svelte';
179
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
152
180
  import UIPaletteSelector from '../ui/UIPaletteSelector.svelte';
153
181
 
154
182
  // One switch. Turning the tint on also stands each hover surface down to its
@@ -214,7 +242,7 @@
214
242
  {#snippet extraPropertyRows(stateName)}
215
243
  {#if stateName === 'hover option'}
216
244
  <div class="property-row" data-token-variables="--segmentedcontrol-hover-tint-enabled">
217
- <span class="property-label">tint layer</span>
245
+ <span class="property-label">tint layer<A11yPropertyMark variables={['--segmentedcontrol-hover-tint-enabled']} /></span>
218
246
  <Toggle checked={tintOn} onchange={setTintOn} />
219
247
  </div>
220
248
  {#if tintOn}