@motion-proto/live-tokens 0.97.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 (184) 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 +272 -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/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  13. package/dist-plugin/index.cjs +148 -3
  14. package/dist-plugin/index.js +250 -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 +3 -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 +41 -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 +101 -0
  56. package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
  57. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  58. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  59. package/src/editor/core/palettes/contrast.ts +44 -0
  60. package/src/editor/core/palettes/contrastPairs.ts +79 -0
  61. package/src/editor/core/palettes/resolveColor.ts +77 -0
  62. package/src/editor/core/store/editorStore.ts +4 -0
  63. package/src/editor/core/store/editorViewStore.ts +2 -2
  64. package/src/editor/core/store/unsavedEdits.ts +10 -0
  65. package/src/editor/core/themes/liveStateStream.ts +7 -1
  66. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  67. package/src/editor/docs/chapters.ts +1 -0
  68. package/src/editor/docs/content/accessibility.md +362 -0
  69. package/src/editor/docs/content/editing-tokens.md +28 -1
  70. package/src/editor/docs/content.generated.ts +2 -1
  71. package/src/editor/pages/ComponentEditorPage.svelte +10 -1
  72. package/src/editor/pages/EditorShell.svelte +96 -10
  73. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  74. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  75. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  76. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  77. package/src/editor/ui/GradientEditor.svelte +9 -1
  78. package/src/editor/ui/ThemePanel.svelte +4 -4
  79. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  80. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  81. package/src/editor/ui/UIPaletteSelector.svelte +9 -0
  82. package/src/editor/ui/UITokenSelector.svelte +26 -2
  83. package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
  84. package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
  85. package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
  86. package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
  87. package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
  88. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  89. package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
  90. package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
  91. package/src/editor/ui/a11y/a11yView.ts +218 -0
  92. package/src/editor/ui/contrastReadout.ts +45 -0
  93. package/src/live-tokens/data/themes/autumn.json +20 -20
  94. package/src/live-tokens/data/themes/halloween.json +20 -20
  95. package/src/live-tokens/data/themes/midnight-study.json +20 -20
  96. package/src/live-tokens/data/themes/ocean.json +20 -20
  97. package/src/live-tokens/data/themes/royal-velvet.json +20 -20
  98. package/src/live-tokens/data/themes/sketchy.json +20 -20
  99. package/src/live-tokens/data/themes/spring-meadow.json +20 -20
  100. package/src/live-tokens/data/themes/sunset.json +20 -20
  101. package/src/system/components/Badge.svelte +3 -3
  102. package/src/system/components/Button.svelte +9 -3
  103. package/src/system/components/CodeSnippet.svelte +8 -1
  104. package/src/system/components/CornerBadge.svelte +3 -3
  105. package/src/system/components/Dialog.svelte +43 -14
  106. package/src/system/components/IconButton.svelte +1 -1
  107. package/src/system/components/Image.svelte +7 -0
  108. package/src/system/components/ImageLightbox.svelte +18 -25
  109. package/src/system/components/Input.svelte +8 -1
  110. package/src/system/components/MenuSelect.svelte +30 -2
  111. package/src/system/components/Notification.svelte +1 -1
  112. package/src/system/components/ProgressBar.svelte +6 -0
  113. package/src/system/components/RadioButton.svelte +36 -3
  114. package/src/system/components/SegmentedControl.svelte +36 -2
  115. package/src/system/components/SlidePager.svelte +2 -2
  116. package/src/system/components/Slider.svelte +13 -9
  117. package/src/system/components/TabBar.svelte +33 -3
  118. package/src/system/components/Toggle.svelte +2 -2
  119. package/src/system/components/VideoFrame.svelte +1 -1
  120. package/src/system/components/VideoLightbox.svelte +2 -2
  121. package/src/system/internal/focusTrap.ts +35 -0
  122. package/src/testing-js/__aria__/badge.yml +40 -0
  123. package/src/testing-js/__aria__/button.yml +88 -0
  124. package/src/testing-js/__aria__/callout.yml +12 -0
  125. package/src/testing-js/__aria__/card.yml +8 -0
  126. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  127. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  128. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  129. package/src/testing-js/__aria__/dialog.yml +42 -0
  130. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  131. package/src/testing-js/__aria__/image.yml +10 -0
  132. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  133. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  134. package/src/testing-js/__aria__/input.yml +61 -0
  135. package/src/testing-js/__aria__/menuselect.yml +30 -0
  136. package/src/testing-js/__aria__/notification.yml +30 -0
  137. package/src/testing-js/__aria__/panel.yml +2 -0
  138. package/src/testing-js/__aria__/progressbar.yml +2 -0
  139. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  140. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  141. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  142. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  143. package/src/testing-js/__aria__/slidepager.yml +72 -0
  144. package/src/testing-js/__aria__/slider.yml +28 -0
  145. package/src/testing-js/__aria__/tabbar.yml +35 -0
  146. package/src/testing-js/__aria__/table.yml +144 -0
  147. package/src/testing-js/__aria__/toggle.yml +12 -0
  148. package/src/testing-js/__aria__/tooltip.yml +2 -0
  149. package/src/testing-js/__aria__/videoframe.yml +6 -0
  150. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  151. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  152. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
  153. package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
  154. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  155. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  156. package/src/testing-js/chunk-EWSGE27V.js +168 -0
  157. package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
  158. package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
  159. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
  160. package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
  161. package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
  162. package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
  163. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  164. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  165. package/src/testing-js/component-a11y.contract.js +283 -0
  166. package/src/testing-js/component-a11y.contract.js.map +1 -0
  167. package/src/testing-js/component-alias.contract.js +2 -2
  168. package/src/testing-js/component-behavior.contract.js +2 -2
  169. package/src/testing-js/component-editor.contract.js +6 -5
  170. package/src/testing-js/component-editor.contract.js.map +1 -1
  171. package/src/testing-js/component-render.contract.js +10 -6
  172. package/src/testing-js/component-render.contract.js.map +1 -1
  173. package/src/testing-js/index.d.ts +44 -16
  174. package/src/testing-js/index.js +9 -9
  175. package/src/testing-js/page-compliance.contract.js +132 -13
  176. package/src/testing-js/page-compliance.contract.js.map +1 -1
  177. package/src/testing-js/vitest.js +3 -3
  178. package/template/package.json +9 -0
  179. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  180. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  181. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  182. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  183. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  184. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
@@ -15,7 +15,7 @@ import {
15
15
  scaleToCssVar,
16
16
  setCurveAnchor,
17
17
  syncBaseAnchor
18
- } from "../chunk-A4TBP4MM.js";
18
+ } from "../chunk-SLICH4IH.js";
19
19
  import {
20
20
  CURRENT_COMPONENT_SCHEMA_VERSION
21
21
  } from "../chunk-QQASYPW7.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.97.0",
3
+ "version": "0.98.2",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -205,15 +205,24 @@
205
205
  "check:smoke-install": "bash scripts/smoke-install.sh",
206
206
  "check:smoke-create": "bash scripts/smoke-create.sh",
207
207
  "eval": "bash scripts/eval.sh",
208
- "eval:billing": "bash scripts/eval-billing.sh",
208
+ "eval:billing": "bash scripts/eval-harbor.sh",
209
209
  "build:eval-project": "bash scripts/build-eval-project.sh",
210
210
  "check:smoke-component-tests": "bash scripts/smoke-component-tests.sh",
211
211
  "check:smoke-page-tests": "bash scripts/smoke-page-tests.sh",
212
212
  "prepublishOnly": "npm run check:no-style-imports && npm run check:no-tooling-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:pages && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:preset-themes && npm run check:skills && npm run check:cli-strings && npm run check:skill-sources && npm run check:skill-atlas && npm run check:smoke-install && npm run check:smoke-create && npm run check:smoke-component-tests && npm run check:smoke-page-tests"
213
213
  },
214
214
  "peerDependencies": {
215
+ "@axe-core/playwright": "^4.11.1",
215
216
  "@playwright/test": "^1.55",
217
+ "@siteimprove/alfa-act": "^0.119.0",
218
+ "@siteimprove/alfa-device": "^0.119.0",
219
+ "@siteimprove/alfa-dom": "^0.119.0",
220
+ "@siteimprove/alfa-http": "^0.119.0",
221
+ "@siteimprove/alfa-rules": "^0.119.0",
222
+ "@siteimprove/alfa-url": "^0.119.0",
223
+ "@siteimprove/alfa-web": "^0.119.0",
216
224
  "@sveltejs/vite-plugin-svelte": "^7.0",
225
+ "axe-core": "^4.11.1",
217
226
  "happy-dom": "^20.0",
218
227
  "sass": "^1.0",
219
228
  "svelte": "^5",
@@ -221,9 +230,36 @@
221
230
  "vitest": "^4.0 || ^5.0"
222
231
  },
223
232
  "peerDependenciesMeta": {
233
+ "@axe-core/playwright": {
234
+ "optional": true
235
+ },
224
236
  "@playwright/test": {
225
237
  "optional": true
226
238
  },
239
+ "@siteimprove/alfa-act": {
240
+ "optional": true
241
+ },
242
+ "@siteimprove/alfa-device": {
243
+ "optional": true
244
+ },
245
+ "@siteimprove/alfa-dom": {
246
+ "optional": true
247
+ },
248
+ "@siteimprove/alfa-http": {
249
+ "optional": true
250
+ },
251
+ "@siteimprove/alfa-rules": {
252
+ "optional": true
253
+ },
254
+ "@siteimprove/alfa-url": {
255
+ "optional": true
256
+ },
257
+ "@siteimprove/alfa-web": {
258
+ "optional": true
259
+ },
260
+ "axe-core": {
261
+ "optional": true
262
+ },
227
263
  "happy-dom": {
228
264
  "optional": true
229
265
  },
@@ -232,9 +268,18 @@
232
268
  }
233
269
  },
234
270
  "devDependencies": {
271
+ "@axe-core/playwright": "^4.14.0",
235
272
  "@playwright/test": "^1.62.1",
273
+ "@siteimprove/alfa-act": "^0.119.0",
274
+ "@siteimprove/alfa-device": "^0.119.0",
275
+ "@siteimprove/alfa-dom": "^0.119.0",
276
+ "@siteimprove/alfa-http": "^0.119.0",
277
+ "@siteimprove/alfa-rules": "^0.119.0",
278
+ "@siteimprove/alfa-url": "^0.119.0",
279
+ "@siteimprove/alfa-web": "^0.119.0",
236
280
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
237
281
  "@types/node": "^25.9.1",
282
+ "axe-core": "^4.14.0",
238
283
  "happy-dom": "^20.9.0",
239
284
  "sass": "^1.98.0",
240
285
  "svelte": "^5.55.5",
@@ -164,7 +164,7 @@
164
164
 
165
165
  {#if open}
166
166
  <div class="labeled-select-panel" bind:this={panelEl} use:portal>
167
- <MenuSelect {items} {value} minWidth="12rem" onchange={select} />
167
+ <MenuSelect {items} {value} ariaLabel={label} minWidth="12rem" onchange={select} />
168
168
  </div>
169
169
  {/if}
170
170
 
@@ -50,16 +50,18 @@ export async function bootLiveTokens(
50
50
  if (import.meta.env.DEV) {
51
51
  // Imported here, never statically, so a build drops the editor. The
52
52
  // registry alone imports every shipped component and editor.
53
- const [editorStore, themeDocumentSync, liveStateStream, { initializeTheme }] = await Promise.all([
53
+ const [editorStore, themeDocumentSync, liveStateStream, a11yReport, { initializeTheme }] = await Promise.all([
54
54
  import('./core/store/editorStore'),
55
55
  import('./core/themes/themeDocumentSync'),
56
56
  import('./core/themes/liveStateStream'),
57
+ import('./core/a11y/a11yReport'),
57
58
  import('./core/themes/themeInit'),
58
59
  import('./component-editor/registry'),
59
60
  ]);
60
61
  editorStore.init();
61
62
  themeDocumentSync.init();
62
63
  liveStateStream.init();
64
+ void a11yReport.init();
63
65
  if (opts.components) {
64
66
  for (const entry of opts.components) {
65
67
  registerComponent(entry);
@@ -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
  import { badgeVariants } from '../../system/components/Badge.svelte';
4
5
 
5
6
  export const component = 'badge';
@@ -57,6 +58,18 @@
57
58
  );
58
59
 
59
60
  const variantOptions = variants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
61
+
62
+ function variantContrastPairs(v: Variant): ContrastPair[] {
63
+ return [{
64
+ foreground: `--badge-${v}-text`,
65
+ backdrop: [`--badge-${v}-surface`],
66
+ role: 'text',
67
+ fontSize: `--badge-${v}-text-font-size`,
68
+ fontWeight: `--badge-${v}-text-font-weight`,
69
+ }];
70
+ }
71
+
72
+ export const contrastPairs: ContrastPair[] = variants.flatMap(variantContrastPairs);
60
73
  </script>
61
74
 
62
75
  <script lang="ts">
@@ -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 = 'button';
6
7
 
@@ -120,11 +121,41 @@
120
121
  default: { default: 'var(--color-transparent)' },
121
122
  },
122
123
  ];
124
+
125
+ // The small size keeps each variant's colors, and it is the smaller of the two text sizes.
126
+ const pairFont = { fontSize: '--button-small-text-font-size', fontWeight: '--button-small-text-font-weight' } as const;
127
+
128
+ function variantContrastPairs(v: Variant): ContrastPair[] {
129
+ const text = (s: StateName) => `${statePrefix(v, s)}-text`;
130
+ const surface = (s: StateName) => `${statePrefix(v, s)}-surface`;
131
+ const hoverStack = ['--button-hover-tint-enabled', surface('hover')];
132
+ const disabled: ContrastPair = { foreground: text('disabled'), backdrop: [surface('disabled')], role: 'text', ...pairFont, inactive: true };
133
+ if (v !== 'outline') {
134
+ return [
135
+ { foreground: text('default'), backdrop: [surface('default')], role: 'text', ...pairFont },
136
+ { foreground: text('hover'), backdrop: hoverStack, role: 'text', ...pairFont },
137
+ disabled,
138
+ ];
139
+ }
140
+ // `:active` repaints the surface without the tint. A pointer press keeps the
141
+ // hover text color, and a key press keeps the default one.
142
+ const active = ['--button-outline-active-surface'];
143
+ return [
144
+ { foreground: text('default'), backdrop: [surface('default')], role: 'text', ...pairFont, context: 'default' },
145
+ { foreground: text('default'), backdrop: active, role: 'text', ...pairFont, context: 'active' },
146
+ { foreground: text('hover'), backdrop: hoverStack, role: 'text', ...pairFont, context: 'hover' },
147
+ { foreground: text('hover'), backdrop: active, role: 'text', ...pairFont, context: 'active' },
148
+ disabled,
149
+ ];
150
+ }
151
+
152
+ export const contrastPairs: ContrastPair[] = variants.flatMap(variantContrastPairs);
123
153
  </script>
124
154
 
125
155
  <script lang="ts">
126
156
  import Button from '../../system/components/Button.svelte';
127
157
  import Toggle from '../ui/Toggle.svelte';
158
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
128
159
  import UIPaletteSelector from '../ui/UIPaletteSelector.svelte';
129
160
  import VariantGroup from './scaffolding/VariantGroup.svelte';
130
161
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
@@ -216,7 +247,7 @@
216
247
  {#snippet extraPropertyRows(stateName)}
217
248
  {#if stateName === 'hover'}
218
249
  <div class="property-row" data-token-variables="--button-hover-tint-enabled">
219
- <span class="property-label">tint layer</span>
250
+ <span class="property-label">tint layer<A11yPropertyMark variables={['--button-hover-tint-enabled']} /></span>
220
251
  <Toggle checked={tintOn} onchange={setTintOn} />
221
252
  </div>
222
253
  {#if tintOn}
@@ -226,7 +257,7 @@
226
257
  </div>
227
258
  {/if}
228
259
  <div class="property-row" data-token-variables="--button-shimmer">
229
- <span class="property-label">hover shimmer</span>
260
+ <span class="property-label">hover shimmer<A11yPropertyMark variables={['--button-shimmer']} /></span>
230
261
  <Toggle checked={shimmerEnabled} onchange={handleShimmerChange} />
231
262
  </div>
232
263
  {/if}
@@ -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
  import { calloutVariants } from '../../system/components/Callout.svelte';
5
6
 
6
7
  export const component = 'callout';
@@ -68,6 +69,18 @@
68
69
  );
69
70
 
70
71
  const variantOptions = variants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
72
+
73
+ function variantContrastPairs(v: Variant): ContrastPair[] {
74
+ return variantTypeGroups(v).map((g) => ({
75
+ foreground: g.colorVariable,
76
+ backdrop: [`--callout-${v}-surface`],
77
+ role: 'text',
78
+ fontSize: g.sizeVariable,
79
+ fontWeight: g.weightVariable,
80
+ }));
81
+ }
82
+
83
+ export const contrastPairs: ContrastPair[] = variants.flatMap(variantContrastPairs);
71
84
  </script>
72
85
 
73
86
  <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, IntrinsicSpec } from './scaffolding/types';
4
+ import type { ContrastPair } from '../core/palettes/contrastPairs';
4
5
 
5
6
  export const component = 'card';
6
7
 
@@ -104,12 +105,20 @@
104
105
  default: { default: HOVER_OFF.shadow },
105
106
  },
106
107
  ];
108
+
109
+ // size="small" sets the title in --font-size-md and the body in
110
+ // --font-size-sm, the smaller of each one's two sizes.
111
+ export const contrastPairs: ContrastPair[] = [
112
+ { foreground: '--card-default-title', backdrop: ['--card-default-header-surface', '--card-default-surface'], role: 'text', fontSize: '--font-size-md', fontWeight: '--card-default-title-font-weight' },
113
+ { foreground: '--card-default-body', backdrop: ['--card-default-surface'], role: 'text', fontSize: '--font-size-sm', fontWeight: '--card-default-body-font-weight' },
114
+ ];
107
115
  </script>
108
116
 
109
117
  <script lang="ts">
110
118
  import Card from '../../system/components/Card.svelte';
111
119
  import VariantGroup from './scaffolding/VariantGroup.svelte';
112
120
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
121
+ import A11yPropertyMark from '../ui/a11y/A11yPropertyMark.svelte';
113
122
  import { editorState, setComponentAlias } from '../core/store/editorStore';
114
123
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
115
124
  let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
@@ -152,6 +161,7 @@
152
161
  <input type="checkbox" checked={hoverEnabled} onchange={(e) => setHoverEnabled(e.currentTarget.checked)} />
153
162
  <span>Use hover</span>
154
163
  </label>
164
+ <A11yPropertyMark variables={['--card-hover-border-enabled', '--card-hover-shadow-enabled']} />
155
165
  <p class="hover-help">
156
166
  Checked: every card lifts on hover. Unchecked: hover is off by default and a page turns it on
157
167
  per card with the <code>hover</code> prop.
@@ -163,10 +173,10 @@
163
173
  {@const previewClass = activeState === 'hover' ? 'force-hover' : ''}
164
174
  <div class="card-demo">
165
175
  <Card title="Card title" icon="fas fa-star" class={previewClass}>
166
- <div class="content-placeholder">Content Placeholder</div>
176
+ <div class="content-placeholder" data-preview-demo>Content Placeholder</div>
167
177
  </Card>
168
178
  <Card variant="bare" class={previewClass}>
169
- <div class="content-placeholder">Bare variant</div>
179
+ <div class="content-placeholder" data-preview-demo>Bare variant</div>
170
180
  </Card>
171
181
  </div>
172
182
  {/snippet}
@@ -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 = 'codesnippet';
5
6
 
@@ -33,6 +34,15 @@
33
34
  };
34
35
 
35
36
  export const allTokens: Token[] = Object.values(states).flat();
37
+
38
+ // The copy button and the scrollbar track paint no surface of their own.
39
+ const surface = ['--codesnippet-surface'];
40
+ export const contrastPairs: ContrastPair[] = [
41
+ { foreground: '--codesnippet-code-text', backdrop: surface, role: 'text', fontSize: '--codesnippet-code-font-size', fontWeight: '--codesnippet-code-font-weight' },
42
+ { foreground: '--codesnippet-icon', backdrop: surface, role: 'non-text' },
43
+ { foreground: '--codesnippet-hover-icon', backdrop: surface, role: 'non-text' },
44
+ { foreground: '--codesnippet-scrollbar-thumb', backdrop: surface, role: 'non-text' },
45
+ ];
36
46
  </script>
37
47
 
38
48
  <script lang="ts">
@@ -52,7 +62,7 @@
52
62
  <VariantGroup name="codesnippet" title="Code Snippet" {states} {component}>
53
63
  {#snippet children({ activeState })}
54
64
  <div class="codesnippet-preview">
55
- <label class="preview-text">
65
+ <label class="preview-text" data-preview-demo>
56
66
  <span>Preview text</span>
57
67
  <textarea
58
68
  bind:value={previewCode}
@@ -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 = 'collapsiblesection';
6
7
 
@@ -117,6 +118,22 @@
117
118
  ]);
118
119
 
119
120
  const variantOptions = VARIANTS.map((v) => ({ value: v, label: VARIANT_LABELS[v] }));
121
+
122
+ function headerContrastPairs(v: Variant, s: HeaderState): ContrastPair[] {
123
+ const backdrop = [`--collapsiblesection-${v}-${s}-surface`];
124
+ return [
125
+ ...variantTypeGroups(v)[`Header / ${HEADER_STATE_LABELS[s]}`].map((g): ContrastPair => ({
126
+ foreground: g.colorVariable,
127
+ backdrop,
128
+ role: 'text',
129
+ fontSize: g.sizeVariable,
130
+ fontWeight: g.weightVariable,
131
+ })),
132
+ { foreground: `--collapsiblesection-${v}-${s}-icon`, backdrop, role: 'non-text' },
133
+ ];
134
+ }
135
+
136
+ export const contrastPairs: ContrastPair[] = VARIANTS.flatMap((v) => HEADER_STATES.flatMap((s) => headerContrastPairs(v, s)));
120
137
  </script>
121
138
 
122
139
  <script lang="ts">
@@ -155,7 +172,7 @@
155
172
  open={isBody}
156
173
  class={forceClass}
157
174
  >
158
- <p style="margin: 0; color: var(--text-secondary);">
175
+ <p style="margin: 0; color: var(--text-secondary);" data-preview-demo>
159
176
  {#if isContainerPart}
160
177
  (Container) — outer chrome only; switch to Body to see the content area.
161
178
  {:else}
@@ -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
  import { badgeVariants } from '../../system/components/Badge.svelte';
4
5
 
5
6
  export const component = 'cornerbadge';
@@ -40,6 +41,18 @@
40
41
  ];
41
42
 
42
43
  const variantOptions = badgeVariants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
44
+
45
+ function variantContrastPairs(v: Variant): ContrastPair[] {
46
+ return [{
47
+ foreground: `--cornerbadge-${v}-text`,
48
+ backdrop: [`--cornerbadge-${v}-surface`],
49
+ role: 'text',
50
+ fontSize: '--cornerbadge-text-font-size',
51
+ fontWeight: '--cornerbadge-text-font-weight',
52
+ }];
53
+ }
54
+
55
+ export const contrastPairs: ContrastPair[] = badgeVariants.flatMap(variantContrastPairs);
43
56
  </script>
44
57
 
45
58
  <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 = 'dialog';
6
7
 
@@ -85,6 +86,20 @@
85
86
  function variantLabel(v: ButtonVariant): string {
86
87
  return v.charAt(0).toUpperCase() + v.slice(1);
87
88
  }
89
+
90
+ // The header and body sit on the dialog box, and the box sits on the scrim.
91
+ const boxStack = ['--dialog-surface', '--dialog-scrim-surface'];
92
+ const headerStack = ['--dialog-header-surface', ...boxStack];
93
+ const textPair = (part: string, backdrop: string[]): ContrastPair => {
94
+ const g = frameTypeGroups[part][0];
95
+ return { foreground: g.colorVariable, backdrop, role: 'text', fontSize: g.sizeVariable, fontWeight: g.weightVariable };
96
+ };
97
+
98
+ export const contrastPairs: ContrastPair[] = [
99
+ textPair('header', headerStack),
100
+ { foreground: '--dialog-close-icon', backdrop: headerStack, role: 'non-text' },
101
+ textPair('body', boxStack),
102
+ ];
88
103
  </script>
89
104
 
90
105
  <script lang="ts">
@@ -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={() => {}}