@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
@@ -18,21 +18,23 @@
18
18
  let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
19
19
  let colorsDisabled = $derived($parentRoute === DEFAULT_COLORS_PATH);
20
20
 
21
- // Editing flow order: Tokens → Components → Sketchstyle → Color Wheel. The condensed
21
+ // Editing flow order: Tokens → Components → Sketchstyle → Color Wheel → Accessibility. The condensed
22
22
  // rail cycles through these; a view is skipped while it's disabled (already
23
23
  // on that page) so the cycle never lands on a dead view.
24
- const CYCLE: readonly EditorView[] = ['tokens', 'components', 'sketch', 'colors'];
24
+ const CYCLE: readonly EditorView[] = ['tokens', 'components', 'sketch', 'colors', 'accessibility'];
25
25
  const ICONS: Record<EditorView, string> = {
26
26
  tokens: 'fa-sliders',
27
27
  colors: 'fa-palette',
28
28
  components: 'fa-cubes',
29
29
  sketch: 'fa-pen-nib',
30
+ accessibility: 'fa-universal-access',
30
31
  };
31
32
  const LABELS: Record<EditorView, string> = {
32
33
  tokens: 'Tokens',
33
34
  colors: 'Color Wheel',
34
35
  components: 'Components',
35
36
  sketch: 'Sketchstyle',
37
+ accessibility: 'Accessibility',
36
38
  };
37
39
 
38
40
  function set(v: EditorView) {
@@ -116,6 +118,17 @@
116
118
  <span class="radio" aria-hidden="true"></span>
117
119
  <span>Color Wheel</span>
118
120
  </button>
121
+ <button
122
+ type="button"
123
+ role="tab"
124
+ class="seg-btn"
125
+ class:active={$editorView === 'accessibility'}
126
+ aria-selected={$editorView === 'accessibility'}
127
+ onclick={() => set('accessibility')}
128
+ >
129
+ <span class="radio" aria-hidden="true"></span>
130
+ <span>Accessibility</span>
131
+ </button>
119
132
  </div>
120
133
  </div>
121
134
  {/if}
@@ -18,6 +18,7 @@
18
18
  import RadialShapePad from '../component-editor/scaffolding/RadialShapePad.svelte';
19
19
  import UISegmentedControl from './UISegmentedControl.svelte';
20
20
  import UIPillButton from './UIPillButton.svelte';
21
+ import A11yPropertyMark from './a11y/A11yPropertyMark.svelte';
21
22
  import { snapTokenToFamily } from '../core/palettes/familySwap';
22
23
 
23
24
  interface Props {
@@ -287,7 +288,10 @@
287
288
  >
288
289
  {#if sectionLabel || hasAside}
289
290
  <div class="editor-header editor-section-left">
290
- <span class="editor-section-label">{sectionLabel ?? ''}</span>
291
+ <span class="editor-section-heading">
292
+ <span class="editor-section-label">{sectionLabel ?? ''}</span>
293
+ <A11yPropertyMark variables={[gradientSource.targetVariable]} />
294
+ </span>
291
295
  {#if !isFlat}
292
296
  <div class="stop-actions">
293
297
  <UIPillButton
@@ -496,6 +500,10 @@
496
500
  color: var(--ui-text-primary);
497
501
  line-height: 1;
498
502
  }
503
+ .editor-section-heading {
504
+ display: inline-flex;
505
+ align-items: center;
506
+ }
499
507
  .editor-section-left { grid-column: 1; }
500
508
  .editor-section-right { grid-column: 2; }
501
509
 
@@ -57,6 +57,7 @@
57
57
  markComponentSaved,
58
58
  } from '../core/store/editorStore';
59
59
  import { openThemeSlug } from '../core/store/editorConfigStore';
60
+ import { unsavedEdits } from '../core/store/unsavedEdits';
60
61
  import { sketchOffTheme, themeSketchSettings } from '../core/sketch/sketchStore';
61
62
  import {
62
63
  componentActiveRevision,
@@ -92,7 +93,6 @@
92
93
  let dirtyComponentCount = $derived(
93
94
  Object.values($componentDirty).filter(Boolean).length,
94
95
  );
95
- let unsavedEdits = $derived($colorsAndTypeDirty || dirtyComponentCount > 0 || $sketchOffTheme);
96
96
 
97
97
  async function refreshFiles() {
98
98
  try {
@@ -282,7 +282,7 @@
282
282
  themeProductionState({
283
283
  openTheme: $openThemeSlug,
284
284
  production: $productionTheme,
285
- unsaved: unsavedEdits || $liveUnsaved,
285
+ unsaved: $unsavedEdits || $liveUnsaved,
286
286
  }),
287
287
  );
288
288
 
@@ -421,7 +421,7 @@
421
421
  async function commitWholeTheme() {
422
422
  const row = previewRow;
423
423
  if (!row) return;
424
- if (unsavedEdits) {
424
+ if ($unsavedEdits) {
425
425
  const ok = window.confirm(
426
426
  'Loading a theme will replace the editor’s current state and discard unsaved changes. Continue?',
427
427
  );
@@ -490,7 +490,7 @@
490
490
  const picked = previewLayer ?? previewedTheme?.colorsAndType ?? null;
491
491
  if (!row || !picked) return;
492
492
  const { _fileName, _source, ...buffer } = picked;
493
- if (unsavedEdits) {
493
+ if ($unsavedEdits) {
494
494
  const ok = window.confirm(
495
495
  'Loading colors and type will discard unsaved changes. Continue?',
496
496
  );
@@ -0,0 +1,186 @@
1
+ <script module lang="ts">
2
+ import { createSubscriber } from 'svelte/reactivity';
3
+ import { on } from 'svelte/events';
4
+ import { THEME_APPLIED_EVENT } from '../core/themes/themeDocumentSync';
5
+
6
+ const themeApplied = createSubscriber((update) => on(document, THEME_APPLIED_EVENT, update));
7
+ </script>
8
+
9
+ <script lang="ts">
10
+ import { editorState } from '../core/store/editorStore';
11
+ import { getContrastPairs } from '../core/components/contrastPairRegistry';
12
+ import { CONTRAST_RULES } from '../core/palettes/contrast';
13
+ import { measurePair, type PairMeasure } from '../core/palettes/contrastPairs';
14
+ import { createDomResolver } from '../core/palettes/resolveColor';
15
+ import { formatRatio, measureState, readoutState, ruleSentence } from './contrastReadout';
16
+ import UIInfoPopover from './UIInfoPopover.svelte';
17
+
18
+ interface Props {
19
+ component: string;
20
+ variable: string;
21
+ }
22
+
23
+ let { component, variable }: Props = $props();
24
+
25
+ let pairs = $derived(getContrastPairs(component, variable));
26
+ let measures = $state<PairMeasure[]>([]);
27
+ let readout = $derived(measures.length > 0 ? readoutState(measures) : null);
28
+
29
+ // Measuring reads the DOM, so it waits a frame. The store renderer writes
30
+ // CSS variables synchronously, so by then the probe reads what the store holds.
31
+ $effect(() => {
32
+ void $editorState;
33
+ themeApplied();
34
+ const current = pairs;
35
+ const frame = requestAnimationFrame(() => {
36
+ const resolver = createDomResolver();
37
+ measures = current.map((pair) => measurePair(pair, resolver));
38
+ });
39
+ return () => cancelAnimationFrame(frame);
40
+ });
41
+ </script>
42
+
43
+ {#if readout}
44
+ <span
45
+ class="contrast-readout"
46
+ data-contrast-state={readout.state}
47
+ data-contrast-ratio={readout.measure.ratio}
48
+ >
49
+ <UIInfoPopover title="Contrast" ariaLabel={readout.ariaLabel}>
50
+ {#snippet trigger()}
51
+ <span class="contrast-value {readout.state}">
52
+ {#if readout.state === 'fail'}
53
+ <i class="fa-solid fa-triangle-exclamation contrast-mark" aria-hidden="true"></i>
54
+ {/if}
55
+ {readout.label}
56
+ </span>
57
+ {/snippet}
58
+ <ul class="contrast-pairs">
59
+ {#each measures as m (m.pair)}
60
+ {@const rule = CONTRAST_RULES[m.pair.role]}
61
+ {@const pairState = measureState(m)}
62
+ <li class="contrast-pair">
63
+ {#if m.pair.context}
64
+ <p class="contrast-context">{m.pair.context}</p>
65
+ {/if}
66
+ <p>{ruleSentence(m)}</p>
67
+ <p class="contrast-measured">
68
+ Measured
69
+ <span class="contrast-value {pairState}">
70
+ {#if pairState === 'fail'}
71
+ <i class="fa-solid fa-triangle-exclamation contrast-mark" aria-hidden="true"></i>
72
+ {/if}
73
+ {formatRatio(m.ratio)}
74
+ </span>
75
+ </p>
76
+ <ol class="contrast-stack" aria-label="Layers behind the color, top first">
77
+ {#each m.pair.backdrop as layer, i (i)}
78
+ <li><code>{layer}</code></li>
79
+ {/each}
80
+ <li>page background</li>
81
+ </ol>
82
+ <a class="contrast-link" href={rule.url} target="_blank" rel="noopener">
83
+ Success criterion {rule.criterion}
84
+ </a>
85
+ </li>
86
+ {/each}
87
+ </ul>
88
+ </UIInfoPopover>
89
+ </span>
90
+ {/if}
91
+
92
+ <style>
93
+ .contrast-readout {
94
+ display: inline-flex;
95
+ flex: 0 0 auto;
96
+ align-items: center;
97
+ }
98
+
99
+ .contrast-value {
100
+ display: inline-flex;
101
+ align-items: baseline;
102
+ gap: var(--ui-space-2);
103
+ font-family: var(--ui-font-mono);
104
+ font-size: var(--ui-font-size-sm);
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .contrast-value.pass {
109
+ color: var(--ui-text-secondary);
110
+ }
111
+
112
+ .contrast-value.fail {
113
+ color: var(--ui-highlight);
114
+ }
115
+
116
+ .contrast-value.inactive {
117
+ color: var(--ui-text-tertiary);
118
+ }
119
+
120
+ .contrast-mark {
121
+ margin-right: var(--ui-space-4);
122
+ font-size: var(--ui-font-size-xs);
123
+ }
124
+
125
+ .contrast-pairs {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: var(--ui-space-12);
129
+ margin: 0;
130
+ padding: 0;
131
+ list-style: none;
132
+ }
133
+
134
+ .contrast-pair {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: var(--ui-space-6);
138
+ }
139
+
140
+ .contrast-pair + .contrast-pair {
141
+ padding-top: var(--ui-space-12);
142
+ border-top: 1px solid var(--ui-border-low);
143
+ }
144
+
145
+ /* The popover's paragraph margin would double the flex gap. */
146
+ .contrast-pair p {
147
+ margin: 0;
148
+ }
149
+
150
+ .contrast-context {
151
+ color: var(--ui-text-primary);
152
+ font-weight: var(--ui-font-weight-semibold);
153
+ }
154
+
155
+ .contrast-measured {
156
+ display: flex;
157
+ align-items: baseline;
158
+ gap: var(--ui-space-6);
159
+ }
160
+
161
+ .contrast-measured .contrast-value {
162
+ font-size: inherit;
163
+ }
164
+
165
+ .contrast-stack {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: var(--ui-space-2);
169
+ margin: 0;
170
+ padding: 0;
171
+ list-style: none;
172
+ font-size: var(--ui-font-size-xs);
173
+ color: var(--ui-text-tertiary);
174
+ }
175
+
176
+ .contrast-stack code {
177
+ overflow-wrap: anywhere;
178
+ }
179
+
180
+ .contrast-link {
181
+ align-self: flex-start;
182
+ font-size: var(--ui-font-size-xs);
183
+ color: var(--ui-text-primary);
184
+ text-decoration: underline;
185
+ }
186
+ </style>
@@ -5,10 +5,12 @@
5
5
  interface Props {
6
6
  title: string;
7
7
  ariaLabel?: string;
8
+ /** Replaces the info icon inside the button. */
9
+ trigger?: Snippet;
8
10
  children?: Snippet;
9
11
  }
10
12
 
11
- let { title, ariaLabel, children }: Props = $props();
13
+ let { title, ariaLabel, trigger, children }: Props = $props();
12
14
 
13
15
  let open = $state(false);
14
16
  let btnEl = $state<HTMLButtonElement | undefined>(undefined);
@@ -83,13 +85,18 @@
83
85
  <button
84
86
  type="button"
85
87
  class="info-btn"
88
+ class:has-trigger={!!trigger}
86
89
  aria-label={ariaLabel ?? `About ${title}`}
87
90
  aria-expanded={open}
88
91
  bind:this={btnEl}
89
92
  data-info-popover-root
90
93
  onclick={() => (open = !open)}
91
94
  >
92
- <i class="fas fa-circle-info"></i>
95
+ {#if trigger}
96
+ {@render trigger()}
97
+ {:else}
98
+ <i class="fas fa-circle-info"></i>
99
+ {/if}
93
100
  </button>
94
101
 
95
102
  {#if open}
@@ -140,6 +147,13 @@
140
147
  color: var(--ui-text-primary);
141
148
  }
142
149
 
150
+ .info-btn.has-trigger {
151
+ width: auto;
152
+ height: auto;
153
+ color: inherit;
154
+ font: inherit;
155
+ }
156
+
143
157
  /* Fixed positioning escapes any parent overflow/stacking context. JS
144
158
  anchors it to the right of the trigger; falls back to centered-below
145
159
  then flips up near the viewport bottom. */
@@ -25,6 +25,8 @@
25
25
  import UITokenSelector from './UITokenSelector.svelte';
26
26
  import ColorEditPanel from './ColorEditPanel.svelte';
27
27
  import CustomColorReplaceStep from './CustomColorReplaceStep.svelte';
28
+ import UIContrastReadout from './UIContrastReadout.svelte';
29
+ import { getContrastPairs } from '../core/components/contrastPairRegistry';
28
30
 
29
31
  /** Honor prefers-reduced-motion: `t()` zeroes durations when the OS asks for
30
32
  * less motion, so the detail-row enter/exit slides skip outright. Mirrors
@@ -89,6 +91,8 @@
89
91
  replacing = null,
90
92
  }: Props = $props();
91
93
 
94
+ let hasContrastPairs = $derived(!!component && getContrastPairs(component, variable).length > 0);
95
+
92
96
  const clampOpacity = (n: number) => Math.max(minOpacity, Math.min(100, Math.round(n)));
93
97
  let noneAllowed = $derived(showNone && minOpacity <= 0);
94
98
 
@@ -740,6 +744,10 @@
740
744
  });
741
745
  </script>
742
746
 
747
+ {#snippet contrastReadout()}
748
+ {#if component}<UIContrastReadout {component} {variable} />{/if}
749
+ {/snippet}
750
+
743
751
  <UITokenSelector
744
752
  bind:this={selector}
745
753
  {variable}
@@ -748,6 +756,7 @@
748
756
  {disabled}
749
757
  {selectionsLocked}
750
758
  {onwrite}
759
+ metaAfter={hasContrastPairs ? contrastReadout : undefined}
751
760
  dropdownMinWidth={session ? '22rem' : '14rem'}
752
761
  dropdownMaxWidth="calc(100vw - 2rem)"
753
762
  hideDefaultHeader={!!selectedFamily}
@@ -23,6 +23,7 @@
23
23
  import UILinkToggle from './UILinkToggle.svelte';
24
24
  import UIRelinkConfirmDialog from './UIRelinkConfirmDialog.svelte';
25
25
  import { anchorToTrigger } from './anchorToTrigger';
26
+ import A11yPropertyMark from './a11y/A11yPropertyMark.svelte';
26
27
 
27
28
  type DropdownContext = { close: () => void; handleReset: () => void };
28
29
 
@@ -53,6 +54,8 @@
53
54
  triggerTitle?: Snippet;
54
55
  /** Optional meta text rendered after the trigger. Renamed from `slot="trigger-meta"` in 0.5.0. */
55
56
  triggerMeta?: Snippet;
57
+ /** Optional content after the meta text, in the same column. The meta text truncates first. */
58
+ metaAfter?: Snippet;
56
59
  /** Optional dropdown header replacement. */
57
60
  header?: Snippet;
58
61
  /** Optional dropdown content above the selections. */
@@ -87,6 +90,7 @@
87
90
  triggerText,
88
91
  triggerTitle,
89
92
  triggerMeta,
93
+ metaAfter,
90
94
  header,
91
95
  subheader,
92
96
  children,
@@ -292,6 +296,7 @@
292
296
  bind:this={container}
293
297
  >
294
298
  <div class="ui-ts-trigger-wrap">
299
+ <A11yPropertyMark variables={[variable]} placement="gutter" />
295
300
  <button
296
301
  bind:this={triggerEl}
297
302
  class="ui-ts-trigger"
@@ -362,11 +367,20 @@
362
367
  {/if}
363
368
  </div>
364
369
 
365
- {#if triggerMeta}
370
+ {#snippet metaText(meta: Snippet)}
366
371
  <span
367
372
  class="ui-ts-meta-text"
368
373
  use:mirrorTextToTitle
369
- >{@render triggerMeta()}</span>
374
+ >{@render meta()}</span>
375
+ {/snippet}
376
+
377
+ {#if metaAfter}
378
+ <div class="ui-ts-meta">
379
+ {#if triggerMeta}{@render metaText(triggerMeta)}{/if}
380
+ {@render metaAfter()}
381
+ </div>
382
+ {:else if triggerMeta}
383
+ {@render metaText(triggerMeta)}
370
384
  {/if}
371
385
  </div>
372
386
 
@@ -518,6 +532,16 @@
518
532
  min-width: 0;
519
533
  }
520
534
 
535
+ /* Column 3 when `metaAfter` is set. `min-width: 0` keeps the pair from
536
+ widening the column; the meta text gives up width first. */
537
+ .ui-ts-meta {
538
+ display: flex;
539
+ align-items: center;
540
+ gap: var(--ui-space-6);
541
+ align-self: center;
542
+ min-width: 0;
543
+ }
544
+
521
545
  .ui-ts-chevron {
522
546
  font-size: 0.625rem;
523
547
  color: var(--ui-text-secondary);