@motion-proto/live-tokens 0.43.0 → 0.45.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,96 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.45.0 — The Color Story shows the tokens you actually have
4
+
5
+ ### Fixed
6
+
7
+ - **The Color Story's text steps read the live tokens.** The primary, secondary
8
+ and tertiary row rendered the values the *Even neutral steps* pill would have
9
+ written, not the current ones, so the same step could report two different
10
+ ratios in the same frame (the Canvas header said 17.9:1 while the row said
11
+ 14.7:1). Each step now resolves `--text-primary` / `--text-secondary` /
12
+ `--text-tertiary` through the same path the functional chips and tone pills
13
+ already used, renders in its own token, and reports its real contrast against
14
+ the canvas.
15
+
16
+ ### Removed
17
+
18
+ - **The Color Story's action pills.** *Raise all text to AA*, *Even neutral
19
+ steps*, and the *Anchor at pure white/black* toggle, added in 0.44.0, are
20
+ gone. The story is a proportional preview, and authoring text steps from it
21
+ put a second palette editor next to the real one. Text steps are edited in
22
+ the Tokens view.
23
+
24
+ - **The text-contrast solvers.** `solveTextContrast.ts` and `recommendText.ts`
25
+ (plus `applySolvedTextCurves` and `applySuggestedNeutralText`) backed only
26
+ those pills and are deleted. Both were internal to `src/editor/core/palettes/`
27
+ and never reachable through the package exports, so no consumer import
28
+ changes. `BW_GUARD_MIN_L` / `BW_GUARD_MAX_L` are unaffected: they live in
29
+ `paletteDerivation.ts` and still guard derived inverted text.
30
+
31
+ ### Changed
32
+
33
+ - **The story's prose lines and its unlabeled header ratio are gone.** The three
34
+ sentences under Canvas ("The dominant surface. Body text sits here." and its
35
+ two siblings) said nothing the labelled steps don't, and the ratio beside the
36
+ Canvas heading was primary's, measured but never named. The stacked steps
37
+ carry all three ratios now.
38
+
39
+ ## 0.44.0 — Every text step derives to body contrast
40
+
41
+ ### Added
42
+
43
+ - **A full-page Colors route.** `/live-tokens/colors` renders the Colors editor
44
+ as its own page, alongside the existing editor and components routes, and is
45
+ auto-appended to the dev nav rail. New `@motion-proto/live-tokens/colors`
46
+ export for consumers mounting it themselves, and a new `colors` key on
47
+ `editorRoutes` to relocate it or pass `false` to disable it. Entering the
48
+ route flips the overlay off its Colors view so the two surfaces never stack.
49
+
50
+ ### Changed
51
+
52
+ - **Selection reads across every Colors surface.** Picking a family used to
53
+ highlight only its swatch. Its harmony-axis row and chip now light up, and on
54
+ the wheel its rail thickens to full contrast while its axis numeral goes
55
+ white and the others step back. Axis chips are clickable to select.
56
+
57
+ - **The Color Story is a readout, not a control panel.** Its "Use suggested"
58
+ button and "Use black and white" checkbox moved out to three pills above the
59
+ story: *Raise all text to AA* (every family, contrast-first), *Even neutral
60
+ steps* (Neutral only, hierarchy-first), and an *Anchor at pure white/black*
61
+ toggle. The old labels named neither the scope nor the effect, which are what
62
+ separate the two actions; each pill now carries the full explanation on hover.
63
+ The AA-reach line is gone.
64
+
65
+ - **Color swatches hold their position.** Both swatch rows were sorted by
66
+ luminance, so editing a color's lightness reshuffled the row under the
67
+ pointer and stuttered mid-drag. They render in declaration order now.
68
+
69
+ - **`--text-muted` now derives to 4.5:1, not 3:1.** The neutral text ramp
70
+ guaranteed muted at the WCAG AA floor for *large* text, one step below
71
+ `--text-tertiary`, on the assumption it would only ever be set at 24px (or
72
+ 18.66px bold). Nothing in the token's name or its position in a ramp of text
73
+ colors carried that condition, so consumers reached for it as a quiet body
74
+ color and shipped text below AA — in the first site audited, all eleven uses
75
+ were 13–16px, every one of them failing. `--text-muted` joins primary,
76
+ secondary and tertiary on the 4.5:1 body floor, which leaves `--text-disabled`
77
+ as the ramp's only sub-AA step, and WCAG 1.4.3 exempts disabled controls.
78
+ `AA_LARGE` is unchanged and still floors the graded chromatic steps.
79
+
80
+ Derived palettes get a lighter (dark scheme) or darker (light scheme) muted
81
+ step the next time text contrast is solved. **A theme already on disk keeps
82
+ its stored curve** — its `--text-muted` does not move until the palette is
83
+ re-solved, so a consuming site that has shipped muted on small text must
84
+ either re-derive or restate those colors itself.
85
+
86
+ Expect muted and tertiary to derive to nearly the same color: both target the
87
+ default surface, so both settle just past the same floor (on the reference
88
+ dark palette, tertiary `#94999c` 4.58:1 and muted `#96989a` 4.55:1). That is
89
+ arithmetic, not a rough edge — a step quieter than tertiary cannot also clear
90
+ tertiary's floor. Treat `--text-muted` as an alias of `--text-tertiary` for
91
+ now; reach for `--text-disabled` when a genuinely quieter, non-AA step is
92
+ wanted, and expect a decision on deprecating one of the two.
93
+
3
94
  ## 0.43.0 — The editor opens on Tokens
4
95
 
5
96
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -65,6 +65,11 @@
65
65
  "svelte": "./src/editor/pages/ComponentEditorPage.svelte",
66
66
  "default": "./src/editor/pages/ComponentEditorPage.svelte"
67
67
  },
68
+ "./colors": {
69
+ "types": "./src/editor/pages/ColorsPage.svelte.d.ts",
70
+ "svelte": "./src/editor/pages/ColorsPage.svelte",
71
+ "default": "./src/editor/pages/ColorsPage.svelte"
72
+ },
68
73
  "./docs": {
69
74
  "types": "./src/editor/docs/Docs.svelte.d.ts",
70
75
  "svelte": "./src/editor/docs/Docs.svelte",
@@ -104,8 +104,7 @@ export const SCALES: readonly Scale[] = [
104
104
  ];
105
105
 
106
106
  /** Guard band for derived near-black/near-white text: values stay between
107
- * ≈ #111 and ≈ #e8e8e8 unless pure black/white is opted into. The AA floor
108
- * beats the guard (see `recommendText.ts`). */
107
+ * ≈ #111 and ≈ #e8e8e8, the range most designers prefer over pure extremes. */
109
108
  export const BW_GUARD_MIN_L = 0.17;
110
109
  export const BW_GUARD_MAX_L = 0.93;
111
110
 
@@ -8,4 +8,5 @@
8
8
  // <LiveTokensRouter>; these are only the defaults.
9
9
  export const DEFAULT_EDITOR_PATH = '/live-tokens/editor';
10
10
  export const DEFAULT_COMPONENTS_PATH = '/live-tokens/components';
11
+ export const DEFAULT_COLORS_PATH = '/live-tokens/colors';
11
12
  export const DEFAULT_DOCS_PATH = '/live-tokens/docs';
@@ -15,7 +15,7 @@
15
15
  import { fade } from 'svelte/transition';
16
16
  import { cubicInOut } from 'svelte/easing';
17
17
  import { route, navigate } from '../core/routing/router';
18
- import { DEFAULT_EDITOR_PATH, DEFAULT_COMPONENTS_PATH } from '../core/routing/ownedRoutes';
18
+ import { DEFAULT_EDITOR_PATH, DEFAULT_COMPONENTS_PATH, DEFAULT_COLORS_PATH } from '../core/routing/ownedRoutes';
19
19
  import { editorView } from '../core/store/editorViewStore';
20
20
  import { columnsVisible, toggleColumns } from './columnsOverlay';
21
21
  import { storageKey } from '../core/store/editorConfig';
@@ -29,6 +29,7 @@
29
29
  open?: boolean | undefined;
30
30
  editorPath?: string;
31
31
  componentsPath?: string;
32
+ colorsPath?: string;
32
33
  navLinks?: NavLink[];
33
34
  pageSources?: Record<string, string>;
34
35
  hidePageSourceOn?: string[];
@@ -39,6 +40,7 @@
39
40
  open = $bindable(undefined),
40
41
  editorPath = DEFAULT_EDITOR_PATH,
41
42
  componentsPath = DEFAULT_COMPONENTS_PATH,
43
+ colorsPath = DEFAULT_COLORS_PATH,
42
44
  navLinks = [],
43
45
  pageSources = {},
44
46
  hidePageSourceOn = [],
@@ -65,9 +67,9 @@
65
67
  overlayOpen.set(!!open);
66
68
  });
67
69
 
68
- // The components route renders the same component-editor surface as the
69
- // overlay's components view. Pair them: on entering it, flip the overlay to
70
- // tokens so the two surfaces don't stack. Fires only on route
70
+ // The components and colors routes render the same surfaces as the overlay's
71
+ // components and colors views. Pair them: on entering one, flip the overlay
72
+ // to tokens so the two surfaces don't stack. Fires only on route
71
73
  // change, not on every editorView change — otherwise cross-window storage
72
74
  // sync re-triggers the rule, which writes editorView, which fires another
73
75
  // storage event, which fires the rule again. The result is heavy re-render
@@ -81,6 +83,9 @@
81
83
  if (r === componentsPath) {
82
84
  editorView.update((v) => (v === 'components' ? 'tokens' : v));
83
85
  }
86
+ if (r === colorsPath) {
87
+ editorView.update((v) => (v === 'colors' ? 'tokens' : v));
88
+ }
84
89
  });
85
90
 
86
91
  // Editor route has its own chrome — hide overlay there.
@@ -30,20 +30,22 @@
30
30
  /**
31
31
  * Override the package's dev-only routes, which default to the reserved
32
32
  * `/live-tokens/*` namespace (`/live-tokens/editor`, `/live-tokens/components`,
33
- * `/live-tokens/docs`) so they never collide with consumer pages. Pass a
34
- * string to relocate a route; pass `false` to disable it entirely (no
35
- * dispatch and, for `components`/`docs`, no auto-injected nav-rail entry).
33
+ * `/live-tokens/colors`, `/live-tokens/docs`) so they never collide with
34
+ * consumer pages. Pass a string to relocate a route; pass `false` to disable
35
+ * it entirely (no dispatch and, for `components`/`colors`/`docs`, no
36
+ * auto-injected nav-rail entry).
36
37
  */
37
38
  export interface EditorRouteOverrides {
38
39
  editor?: string | false;
39
40
  components?: string | false;
41
+ colors?: string | false;
40
42
  docs?: string | false;
41
43
  }
42
44
 
43
45
  /**
44
46
  * Resolve a path to the single entry that renders it. Precedence, after the
45
- * package-owned `/live-tokens/*` routes (editor, components, docs) have
46
- * already been matched by the component:
47
+ * package-owned `/live-tokens/*` routes (editor, components, colors, docs)
48
+ * have already been matched by the component:
47
49
  *
48
50
  * 1. `pages[route]` — exact static match.
49
51
  * 2. `resolve(route)` — consumer code, where params / prefixes / gating
@@ -68,7 +70,7 @@
68
70
  import LiveEditorOverlay from './LiveEditorOverlay.svelte';
69
71
  import ColumnsOverlay from './ColumnsOverlay.svelte';
70
72
  import { route, navigate } from '../core/routing/router';
71
- import { DEFAULT_EDITOR_PATH, DEFAULT_COMPONENTS_PATH, DEFAULT_DOCS_PATH } from '../core/routing/ownedRoutes';
73
+ import { DEFAULT_EDITOR_PATH, DEFAULT_COMPONENTS_PATH, DEFAULT_COLORS_PATH, DEFAULT_DOCS_PATH } from '../core/routing/ownedRoutes';
72
74
 
73
75
  interface Props {
74
76
  pages: Record<string, RouteEntry>;
@@ -86,32 +88,38 @@
86
88
 
87
89
  let editorEnabled = $derived(editorRoutes.editor !== false);
88
90
  let componentsEnabled = $derived(editorRoutes.components !== false);
91
+ let colorsEnabled = $derived(editorRoutes.colors !== false);
89
92
  let docsEnabled = $derived(editorRoutes.docs !== false);
90
93
  let editorPath = $derived(typeof editorRoutes.editor === 'string' ? editorRoutes.editor : DEFAULT_EDITOR_PATH);
91
94
  let componentsPath = $derived(typeof editorRoutes.components === 'string' ? editorRoutes.components : DEFAULT_COMPONENTS_PATH);
95
+ let colorsPath = $derived(typeof editorRoutes.colors === 'string' ? editorRoutes.colors : DEFAULT_COLORS_PATH);
92
96
  let docsPath = $derived(typeof editorRoutes.docs === 'string' ? editorRoutes.docs : DEFAULT_DOCS_PATH);
93
97
 
94
98
  const isDev = import.meta.env.DEV;
95
99
  let isEditor = $derived(isDev && editorEnabled && $route === editorPath);
96
100
  let isComponentEditor = $derived(isDev && componentsEnabled && $route === componentsPath);
101
+ let isColors = $derived(isDev && colorsEnabled && $route === colorsPath);
97
102
  let isDocs = $derived(isDev && docsEnabled && $route === docsPath);
98
103
 
99
104
  // The single entry that renders the current route. Owned routes are handled
100
- // by the isEditor/isComponentEditor/isDocs branches, so they resolve to null
101
- // here; everything else flows through resolveRoute and drives both dispatch
102
- // (component + props) and page-source from this one value.
105
+ // by the isEditor/isComponentEditor/isColors/isDocs branches, so they resolve
106
+ // to null here; everything else flows through resolveRoute and drives both
107
+ // dispatch (component + props) and page-source from this one value.
103
108
  let resolvedEntry = $derived(
104
- isEditor || isComponentEditor || isDocs ? null : resolveRoute(pages, resolve, $route),
109
+ isEditor || isComponentEditor || isColors || isDocs ? null : resolveRoute(pages, resolve, $route),
105
110
  );
106
111
 
107
112
  // Pages with a label show up in the nav rail, in declaration order. In dev,
108
- // the components-editor and docs routes are auto-appended so they're reachable
109
- // from every page. Docs ship from the package, so consumers reference the
110
- // guide in the editor while building without vendoring a copy.
113
+ // the colors, components-editor, and docs routes are auto-appended so they're
114
+ // reachable from every page. Docs ship from the package, so consumers
115
+ // reference the guide in the editor while building without vendoring a copy.
111
116
  let navLinks = $derived([
112
117
  ...Object.entries(pages)
113
118
  .filter(([, e]) => !!e.label)
114
119
  .map(([path, e]) => ({ path, label: e.label!, icon: e.icon ?? '' })),
120
+ ...(isDev && colorsEnabled
121
+ ? [{ path: colorsPath, label: 'Colors', icon: 'fa-palette' }]
122
+ : []),
115
123
  ...(isDev && componentsEnabled
116
124
  ? [{ path: componentsPath, label: 'Components', icon: 'fa-puzzle-piece' }]
117
125
  : []),
@@ -132,14 +140,15 @@
132
140
  ...(resolvedEntry?.source ? { [$route]: resolvedEntry.source } : {}),
133
141
  });
134
142
 
135
- // The package-owned components and docs routes hide the page-source button
136
- // (no consumer source file backs them).
143
+ // The package-owned components, colors, and docs routes hide the page-source
144
+ // button (no consumer source file backs them).
137
145
  let hidePageSourceOn = $derived([
138
146
  ...Object.entries(pages)
139
147
  .filter(([, e]) => e.hidePageSource)
140
148
  .map(([path]) => path),
141
149
  ...(resolvedEntry?.hidePageSource ? [$route] : []),
142
150
  ...(componentsEnabled ? [componentsPath] : []),
151
+ ...(colorsEnabled ? [colorsPath] : []),
143
152
  ...(docsEnabled ? [docsPath] : []),
144
153
  ]);
145
154
 
@@ -150,6 +159,7 @@
150
159
  let pagePromise = $derived.by(() => {
151
160
  if (isEditor) return import('../pages/Editor.svelte');
152
161
  if (isComponentEditor) return import('../pages/ComponentEditorPage.svelte');
162
+ if (isColors) return import('../pages/ColorsPage.svelte');
153
163
  if (isDocs) return import('../docs/Docs.svelte');
154
164
  const entry = resolvedEntry;
155
165
  if (!entry) return Promise.resolve({ default: null as unknown as Component<any, any, any> });
@@ -177,9 +187,10 @@
177
187
  class="lt-app"
178
188
  class:is-editor={isEditor}
179
189
  class:is-component-editor={isComponentEditor}
190
+ class:is-colors={isColors}
180
191
  onclick={handleClick}
181
192
  >
182
- <LiveEditorOverlay {navLinks} {pageSources} {hidePageSourceOn} {editorPath} {componentsPath} />
193
+ <LiveEditorOverlay {navLinks} {pageSources} {hidePageSourceOn} {editorPath} {componentsPath} {colorsPath} />
183
194
  <ColumnsOverlay />
184
195
 
185
196
  {#await pagePromise then m}
@@ -210,7 +221,8 @@
210
221
  padding-right: var(--lt-overlay-scroll-pad, 0px);
211
222
  }
212
223
 
213
- .lt-app.is-editor {
224
+ .lt-app.is-editor,
225
+ .lt-app.is-colors {
214
226
  padding-bottom: 0;
215
227
  background: black;
216
228
  }
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import ColorsTab from '../ui/colors/ColorsTab.svelte';
4
+ import UICopyPopover from '../ui/UICopyPopover.svelte';
5
+ import { installEditorKeybindings } from '../core/store/editorKeybindings';
6
+ import { initializeEditorStore } from '../core/store/editorStore';
7
+ import { storageKey } from '../core/store/editorConfig';
8
+ import { DEFAULT_COLORS_PATH } from '../core/routing/ownedRoutes';
9
+ // Editor chrome + form controls + icon font must be JS imports (not @import
10
+ // inside the style block) so Vite resolves them via the module graph
11
+ // regardless of how the consumer compiles Svelte CSS (external ?lang.css vs
12
+ // injected); otherwise the @import URLs leak to the browser and 404 against
13
+ // the consumer's root.
14
+ import '../styles/ui-editor.css';
15
+ import '../styles/ui-form-controls.css';
16
+ import '@fortawesome/fontawesome-free/css/all.min.css';
17
+
18
+ // Where "Back to site" sends the user. Prefer the previous non-colors entry
19
+ // from session history; fall back to home.
20
+ const backHref = pickBackHref();
21
+
22
+ function pickBackHref(): string {
23
+ try {
24
+ const prev = sessionStorage.getItem(storageKey('prev-route'));
25
+ if (prev && prev !== DEFAULT_COLORS_PATH) return prev;
26
+ } catch {
27
+ // ignore
28
+ }
29
+ return '/';
30
+ }
31
+
32
+ onMount(() => {
33
+ initializeEditorStore();
34
+ return installEditorKeybindings();
35
+ });
36
+ </script>
37
+
38
+ <!-- .editor-page scopes the --ui-* token definitions from ui-editor.css. -->
39
+ <div class="editor-page colors-page">
40
+ <div class="page-bar">
41
+ <div class="bar-left">
42
+ <a href={backHref} class="back-link">
43
+ <i class="fas fa-arrow-left"></i>
44
+ <span>Back to site</span>
45
+ </a>
46
+ <span class="page-label">Colors</span>
47
+ </div>
48
+ </div>
49
+
50
+ <main class="content">
51
+ <ColorsTab />
52
+ </main>
53
+ <UICopyPopover />
54
+ </div>
55
+
56
+ <style>
57
+ .colors-page {
58
+ min-height: 100vh;
59
+ background: black;
60
+ }
61
+
62
+ .page-bar {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: var(--ui-space-16);
66
+ padding: var(--ui-space-10) var(--ui-space-16);
67
+ background: black;
68
+ border-bottom: 1px solid var(--ui-border-low);
69
+ min-height: 52px;
70
+ }
71
+
72
+ .bar-left {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: var(--ui-space-16);
76
+ min-width: 0;
77
+ }
78
+
79
+ .back-link {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: var(--ui-space-6);
83
+ color: var(--ui-text-tertiary);
84
+ text-decoration: none;
85
+ font-size: var(--ui-font-size-md);
86
+ transition: color var(--ui-transition-fast);
87
+ }
88
+
89
+ .back-link:hover {
90
+ color: var(--ui-text-primary);
91
+ }
92
+
93
+ .page-label {
94
+ font-size: var(--ui-font-size-md);
95
+ font-weight: var(--ui-font-weight-semibold);
96
+ color: var(--ui-text-secondary);
97
+ }
98
+
99
+ .content {
100
+ max-width: 1680px;
101
+ margin: 0 auto;
102
+ padding: var(--ui-space-24) var(--ui-space-32);
103
+ isolation: isolate;
104
+ }
105
+
106
+ @media (max-width: 1280px) {
107
+ .content {
108
+ padding: var(--ui-space-20) var(--ui-space-20);
109
+ }
110
+ }
111
+
112
+ @media (max-width: 1024px) {
113
+ .content {
114
+ padding: var(--ui-space-16) var(--ui-space-12);
115
+ }
116
+ }
117
+ </style>
@@ -0,0 +1,2 @@
1
+ import { SvelteComponent } from 'svelte';
2
+ export default class ColorsPage extends SvelteComponent<Record<string, never>> {}
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { editorView, type EditorView } from '../core/store/editorViewStore';
3
3
  import { parentRoute } from '../core/routing/parentRouteStore';
4
- import { DEFAULT_COMPONENTS_PATH } from '../core/routing/ownedRoutes';
4
+ import { DEFAULT_COMPONENTS_PATH, DEFAULT_COLORS_PATH } from '../core/routing/ownedRoutes';
5
5
 
6
6
  interface Props {
7
7
  condensed?: boolean;
@@ -9,16 +9,17 @@
9
9
 
10
10
  let { condensed = false }: Props = $props();
11
11
 
12
- // On the components route the host page is already the components editor —
13
- // the overlay's components view would just stack on top, so disable the
14
- // switch. The switcher renders inside the editor iframe, so we read the
15
- // *parent* route, not this iframe's own. Compares the default path:
16
- // editorRoutes.components relocation isn't plumbed across the iframe, so a
17
- // relocated route won't disable the switch.
12
+ // On the components or colors route the host page already renders that
13
+ // surface — the overlay's matching view would just stack on top, so disable
14
+ // the switch. The switcher renders inside the editor iframe, so we read the
15
+ // *parent* route, not this iframe's own. Compares the default paths:
16
+ // editorRoutes relocation isn't plumbed across the iframe, so a relocated
17
+ // route won't disable the switch.
18
18
  let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
19
+ let colorsDisabled = $derived($parentRoute === DEFAULT_COLORS_PATH);
19
20
 
20
21
  // Editing flow order: Tokens → Colors → Components. The condensed rail cycles
21
- // through these; Components is skipped while it's disabled (already on that
22
+ // through these; a view is skipped while it's disabled (already on that
22
23
  // page) so the cycle never lands on a dead view.
23
24
  const CYCLE: readonly EditorView[] = ['tokens', 'colors', 'components'];
24
25
  const ICONS: Record<EditorView, string> = {
@@ -43,6 +44,7 @@
43
44
  i = (i + 1) % CYCLE.length;
44
45
  const next = CYCLE[i];
45
46
  if (next === 'components' && componentsDisabled) continue;
47
+ if (next === 'colors' && colorsDisabled) continue;
46
48
  return next;
47
49
  }
48
50
  return current;
@@ -81,6 +83,8 @@
81
83
  class="seg-btn"
82
84
  class:active={$editorView === 'colors'}
83
85
  aria-selected={$editorView === 'colors'}
86
+ disabled={colorsDisabled}
87
+ title={colorsDisabled ? 'Already viewing the Colors page' : undefined}
84
88
  onclick={() => set('colors')}
85
89
  >
86
90
  <span class="radio" aria-hidden="true"></span>
@@ -10,6 +10,8 @@
10
10
  disabled?: boolean;
11
11
  title?: string;
12
12
  type?: 'button' | 'submit' | 'reset';
13
+ /** Marks the pill a toggle; true also lights it as on. */
14
+ ariaPressed?: boolean;
13
15
  onclick?: (e: MouseEvent) => void;
14
16
  children?: Snippet;
15
17
  }
@@ -23,6 +25,7 @@
23
25
  disabled = false,
24
26
  title,
25
27
  type = 'button',
28
+ ariaPressed,
26
29
  onclick,
27
30
  children,
28
31
  }: Props = $props();
@@ -48,6 +51,7 @@
48
51
  {disabled}
49
52
  {title}
50
53
  {onclick}
54
+ aria-pressed={ariaPressed}
51
55
  >
52
56
  {#if icon}<i class="fas {icon}" aria-hidden="true"></i>{/if}
53
57
  {#if children}<span class="ui-pill-label">{@render children()}</span>{/if}
@@ -89,6 +93,17 @@
89
93
  border-color: rgba(255, 255, 255, 0.75);
90
94
  }
91
95
 
96
+ /* Toggle "on": inset fill so it reads as depressed, not merely hovered. */
97
+ .ui-pill[aria-pressed='true'] {
98
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.30) 0%, rgba(255, 255, 255, 0.16) 100%);
99
+ border-color: rgba(255, 255, 255, 0.9);
100
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.35);
101
+ }
102
+
103
+ .ui-pill[aria-pressed='true'] i {
104
+ color: rgba(255, 255, 255, 0.95);
105
+ }
106
+
92
107
  .ui-pill:focus-visible {
93
108
  outline: none;
94
109
  border-color: rgba(255, 255, 255, 0.9);