@motion-proto/live-tokens 0.94.1 → 0.94.3

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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.94.3 — Demo bar chrome
4
+
5
+ ### Fixed
6
+
7
+ - **The demo's Theme and Sketch bar stays legible in every theme.** The bar
8
+ painted from the open theme's scrim, and its selects were themed Buttons, so
9
+ a theme or sketchstyle could leave its labels unreadable. The bar now paints
10
+ as editor chrome from the `--ui-*` tokens, like the editor overlay, and
11
+ sketch mode leaves it alone. `ThemeSelect` and `SketchSelect` take a
12
+ `chrome` prop for the bar. Elsewhere they stay themed.
13
+
14
+ ## 0.94.2 — Palette step confirm
15
+
16
+ ### Fixed
17
+
18
+ - **Confirming a palette step after a curve change saves the override.** A
19
+ step left open while a lightness, saturation, or hue curve moved held its
20
+ draft in a deep reactive proxy. Confirm wrote that proxy into the store,
21
+ and the next edit failed to clone the state. The editor now keeps the draft
22
+ as a plain value.
23
+
24
+ ### Changed
25
+
26
+ - **The demo page's closing quote stands on its own.** The illustration beside
27
+ it is gone, and the quote sits centered between Layers and Get Started.
28
+
3
29
  ## 0.94.1 — Adopt after a save
4
30
 
5
31
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.94.1",
3
+ "version": "0.94.3",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import UIMenuButton from '../editor/ui/UIMenuButton.svelte';
3
+ import UIOptionList from '../editor/ui/UIOptionList.svelte';
4
+ import UIOptionItem from '../editor/ui/UIOptionItem.svelte';
5
+
6
+ interface Props {
7
+ label: string;
8
+ items: { value: string; label: string }[];
9
+ value: string;
10
+ /** Trigger text when `value` matches no item. Defaults to the label. */
11
+ placeholder?: string;
12
+ disabled?: boolean;
13
+ error?: string;
14
+ onchange: (value: string) => void;
15
+ }
16
+
17
+ let {
18
+ label: heading,
19
+ items,
20
+ value,
21
+ placeholder = heading,
22
+ disabled = false,
23
+ error = '',
24
+ onchange,
25
+ }: Props = $props();
26
+
27
+ const selectedLabel = $derived(items.find((item) => item.value === value)?.label ?? placeholder);
28
+
29
+ function select(next: string, close: () => void): void {
30
+ close();
31
+ if (next === value) return;
32
+ onchange(next);
33
+ }
34
+ </script>
35
+
36
+ <!-- LabeledSelect's twin in the editor's --ui-* chrome. It needs an
37
+ .editor-page ancestor, which scopes those tokens and hosts the menu. -->
38
+ <div class="chrome-select" inert={disabled || items.length === 0}>
39
+ <span class="chrome-select-label">{heading}</span>
40
+
41
+ <UIMenuButton header={heading} triggerClass="chrome-select-trigger" menuMinWidth="12rem">
42
+ {#snippet trigger()}
43
+ <span>{selectedLabel}</span>
44
+ <i class="fas fa-chevron-down" aria-hidden="true"></i>
45
+ {/snippet}
46
+ {#snippet children({ close })}
47
+ <UIOptionList>
48
+ {#each items as item (item.value)}
49
+ <UIOptionItem active={item.value === value} onclick={() => select(item.value, close)}>
50
+ {#snippet label()}{item.label}{/snippet}
51
+ </UIOptionItem>
52
+ {/each}
53
+ </UIOptionList>
54
+ {/snippet}
55
+ </UIMenuButton>
56
+
57
+ {#if error}
58
+ <span class="error" aria-live="polite">{error}</span>
59
+ {/if}
60
+ </div>
61
+
62
+ <style>
63
+ .chrome-select {
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ align-items: center;
67
+ gap: var(--ui-space-8);
68
+ }
69
+
70
+ .chrome-select[inert] {
71
+ opacity: var(--ui-opacity-disabled);
72
+ }
73
+
74
+ .chrome-select-label {
75
+ font-size: var(--ui-font-size-xs);
76
+ font-weight: var(--ui-font-weight-semibold);
77
+ color: var(--ui-text-secondary);
78
+ }
79
+
80
+ .chrome-select :global(.chrome-select-trigger) {
81
+ min-width: 9rem;
82
+ justify-content: space-between;
83
+ gap: var(--ui-space-12);
84
+ padding: var(--ui-space-6) var(--ui-space-10);
85
+ font-size: var(--ui-font-size-xs);
86
+ color: var(--ui-text-primary);
87
+ background: var(--ui-surface-high);
88
+ border: 1px solid var(--ui-border);
89
+ border-radius: var(--ui-radius-md);
90
+ transition:
91
+ background var(--ui-transition-fast),
92
+ border-color var(--ui-transition-fast);
93
+ }
94
+
95
+ .chrome-select :global(.chrome-select-trigger:hover),
96
+ .chrome-select :global(.chrome-select-trigger[aria-expanded='true']) {
97
+ background: var(--ui-surface-highest);
98
+ border-color: var(--ui-border-higher);
99
+ }
100
+
101
+ .chrome-select :global(.chrome-select-trigger .fa-chevron-down) {
102
+ font-size: 0.625rem;
103
+ color: var(--ui-text-secondary);
104
+ }
105
+
106
+ .error {
107
+ max-width: 20rem;
108
+ font-size: var(--ui-font-size-xs);
109
+ color: var(--ui-link-broken);
110
+ }
111
+ </style>
@@ -64,14 +64,6 @@
64
64
  margin-top: var(--space-32);
65
65
  }
66
66
 
67
- /* Overlap the offering image up into Layers (top) and butt Get Started
68
- against its bottom. -space-64 cancels the grid row-gap; the extra
69
- -space-64 on top is the actual overlap — tune that second term to taste. */
70
- .kit > :global(.offering) {
71
- margin-top: calc(var(--space-64) * -2 + var(--space-8));
72
- margin-bottom: calc(var(--space-64) * -1);
73
- }
74
-
75
67
  /* Inline code styling cascades into every section via :global descendants. */
76
68
  .kit :global(p > code),
77
69
  .kit :global(.callout code) {
@@ -89,9 +81,5 @@
89
81
  padding: var(--space-32) var(--space-16);
90
82
  row-gap: var(--space-48);
91
83
  }
92
- .kit > :global(.offering) {
93
- margin-top: calc(var(--space-48) * -2);
94
- margin-bottom: calc(var(--space-48) * -1);
95
- }
96
84
  }
97
85
  </style>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import '../editor/styles/ui-editor.css';
2
3
  import ThemeSelect from './ThemeSelect.svelte';
3
4
  import SketchSelect from './SketchSelect.svelte';
4
5
 
@@ -11,11 +12,14 @@
11
12
  } = $props();
12
13
  </script>
13
14
 
14
- <div class="demo-bar">
15
+ <!-- Editor chrome, like the overlay: the bar paints from --ui-*, so every
16
+ theme leaves it legible, and data-live-tokens-chrome keeps sketch mode off
17
+ it. .editor-page scopes the --ui-* tokens and hosts the selects' menus. -->
18
+ <div class="demo-bar editor-page" data-live-tokens-chrome>
15
19
  <div class="bar">
16
20
  <div class="pickers">
17
- <ThemeSelect inline size="small" onpick={onThemePick} />
18
- <SketchSelect inline size="small" onpick={onSketchPick} />
21
+ <ThemeSelect chrome onpick={onThemePick} />
22
+ <SketchSelect chrome onpick={onSketchPick} />
19
23
  </div>
20
24
  </div>
21
25
  </div>
@@ -27,9 +31,9 @@
27
31
  left: 0;
28
32
  right: 0;
29
33
  z-index: var(--z-sticky);
30
- background: var(--scrim-high);
31
- backdrop-filter: blur(var(--blur-md));
32
- border-bottom: var(--border-width-1) solid var(--tint-high);
34
+ --sketch-icon-off: none;
35
+ background: var(--ui-surface-lower);
36
+ border-bottom: 1px solid var(--ui-border-low);
33
37
  }
34
38
 
35
39
  /* The bar repeats the demo's .kit grid, so its content lines up with the
@@ -64,9 +68,6 @@
64
68
  @media (max-width: 600px) {
65
69
  .demo-bar {
66
70
  position: static;
67
- background: none;
68
- backdrop-filter: none;
69
- border-bottom: none;
70
71
  }
71
72
  .bar {
72
73
  padding: var(--space-64) var(--space-16) 0;
@@ -1,8 +1,20 @@
1
1
  <script lang="ts">
2
2
  import LabeledSelect from './LabeledSelect.svelte';
3
+ import ChromeSelect from './ChromeSelect.svelte';
3
4
  import { setSketch, sketchPick, sketchStyles, unsavedSketchStyle } from '../editor/core/sketch';
4
5
 
5
- let { inline = false, size = 'default', onpick }: { inline?: boolean; size?: 'default' | 'small'; onpick?: (id: string | null) => void } = $props();
6
+ let {
7
+ inline = false,
8
+ size = 'default',
9
+ chrome = false,
10
+ onpick,
11
+ }: {
12
+ inline?: boolean;
13
+ size?: 'default' | 'small';
14
+ /** Paint in the editor's --ui-* chrome; needs an .editor-page ancestor. */
15
+ chrome?: boolean;
16
+ onpick?: (id: string | null) => void;
17
+ } = $props();
6
18
 
7
19
  // The empty string is the placeholder's value, so off needs a name of its own.
8
20
  const OFF = 'none';
@@ -45,14 +57,26 @@
45
57
  }
46
58
  </script>
47
59
 
48
- <LabeledSelect
49
- {inline}
50
- {size}
51
- label="Sketch"
52
- {items}
53
- value={selected}
54
- placeholder="Custom"
55
- disabled={busy}
56
- {error}
57
- onchange={changeSketch}
58
- />
60
+ {#if chrome}
61
+ <ChromeSelect
62
+ label="Sketch"
63
+ {items}
64
+ value={selected}
65
+ placeholder="Custom"
66
+ disabled={busy}
67
+ {error}
68
+ onchange={changeSketch}
69
+ />
70
+ {:else}
71
+ <LabeledSelect
72
+ {inline}
73
+ {size}
74
+ label="Sketch"
75
+ {items}
76
+ value={selected}
77
+ placeholder="Custom"
78
+ disabled={busy}
79
+ {error}
80
+ onchange={changeSketch}
81
+ />
82
+ {/if}
@@ -1,11 +1,23 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
3
  import LabeledSelect from './LabeledSelect.svelte';
4
+ import ChromeSelect from './ChromeSelect.svelte';
4
5
  import { listThemes, applyTheme } from '../editor/core/themes/themeService';
5
6
  import { openThemeSlug } from '../editor/core/store/editorConfigStore';
6
7
  import { carrySketch } from './sketchCarry';
7
8
 
8
- let { inline = false, size = 'default', onpick }: { inline?: boolean; size?: 'default' | 'small'; onpick?: (fileName: string) => void } = $props();
9
+ let {
10
+ inline = false,
11
+ size = 'default',
12
+ chrome = false,
13
+ onpick,
14
+ }: {
15
+ inline?: boolean;
16
+ size?: 'default' | 'small';
17
+ /** Paint in the editor's --ui-* chrome; needs an .editor-page ancestor. */
18
+ chrome?: boolean;
19
+ onpick?: (fileName: string) => void;
20
+ } = $props();
9
21
 
10
22
  let themes = $state<{ value: string; label: string }[]>([]);
11
23
  let busy = $state(false);
@@ -40,13 +52,24 @@
40
52
 
41
53
  <!-- The open theme drives the selection, so applies from the Themes panel or
42
54
  another tab show up here too. -->
43
- <LabeledSelect
44
- {inline}
45
- {size}
46
- label="Theme"
47
- items={themes}
48
- value={$openThemeSlug}
49
- disabled={busy}
50
- {error}
51
- onchange={changeTheme}
52
- />
55
+ {#if chrome}
56
+ <ChromeSelect
57
+ label="Theme"
58
+ items={themes}
59
+ value={$openThemeSlug}
60
+ disabled={busy}
61
+ {error}
62
+ onchange={changeTheme}
63
+ />
64
+ {:else}
65
+ <LabeledSelect
66
+ {inline}
67
+ {size}
68
+ label="Theme"
69
+ items={themes}
70
+ value={$openThemeSlug}
71
+ disabled={busy}
72
+ {error}
73
+ onchange={changeTheme}
74
+ />
75
+ {/if}
@@ -1,10 +1,6 @@
1
- <script lang="ts">
2
- import offeringUrl from '../../system/assets/offering.webp';
3
- </script>
1
+ <script lang="ts"></script>
4
2
 
5
3
  <section class="offering">
6
- <img class="offering-image" src={offeringUrl} alt="" />
7
-
8
4
  <figure class="offering-quote">
9
5
  <blockquote>
10
6
  <p>
@@ -23,37 +19,11 @@
23
19
  display: grid;
24
20
  grid-template-columns: repeat(var(--columns-count), 1fr);
25
21
  column-gap: var(--columns-gutter);
26
- position: relative;
27
- z-index: 1;
28
- }
29
-
30
- /* Roughly a third of the content width, parked on the left edge so it
31
- overlaps the left-hand card above without dominating the section. */
32
- .offering-image {
33
- grid-column: 2 / 7;
34
- grid-row: 1;
35
- display: block;
36
- width: 100%;
37
- height: auto;
38
- border-radius: var(--radius-lg);
39
- z-index: 1;
40
22
  }
41
23
 
42
- /* Sits in the open space to the right of the image. No glass background.
43
- align-self centers it on the image, then translateY drops it by half the
44
- image's upward overlap (the extra -space-64 in Demo.svelte) so it lands in
45
- the middle of the actual gap between the cards above and Get Started below. */
46
24
  .offering-quote {
47
- grid-column: 7 / -2;
48
- grid-row: 1;
49
- align-self: center;
50
- transform: translateY(calc(var(--space-64) / 2));
25
+ grid-column: 3 / -3;
51
26
  margin: 0;
52
- /* Align body text to the card text above. The cards split this same span
53
- in half, so the right card's left edge sits half a gutter right of line 7;
54
- its body text is then inset by the card's --space-20 padding. Match both,
55
- and the open quote dangles left into this padding (text-indent on p). */
56
- padding-left: calc(var(--columns-gutter) / 2 + var(--space-20));
57
27
  }
58
28
 
59
29
  .offering-quote blockquote {
@@ -104,11 +74,8 @@
104
74
  }
105
75
 
106
76
  @media (max-width: 960px) {
107
- .offering-image {
108
- grid-column: 2 / 7;
109
- }
110
77
  .offering-quote {
111
- grid-column: 7 / -2;
78
+ grid-column: 2 / -2;
112
79
  }
113
80
  .offering-quote p {
114
81
  font-size: var(--font-size-xl);
@@ -116,17 +83,8 @@
116
83
  }
117
84
 
118
85
  @media (max-width: 600px) {
119
- .offering-image {
120
- grid-column: 1 / -1;
121
- grid-row: 1;
122
- }
123
86
  .offering-quote {
124
87
  grid-column: 1 / -1;
125
- grid-row: 2;
126
- align-self: auto;
127
- width: auto;
128
- margin: var(--space-16) 0 0;
129
- padding-left: 0;
130
88
  }
131
89
  .offering-quote p {
132
90
  font-size: var(--font-size-4xl);
@@ -174,7 +174,10 @@
174
174
  edit('curveAutoSmooth', { ...curveAutoSmooth, [key]: value });
175
175
  }
176
176
 
177
- let editing: EditingState = $state(idleState);
177
+ // `$state.raw`: confirmEdit writes the draft into the store, which
178
+ // structuredClones on every edit, and a deep `$state` proxy is not cloneable.
179
+ // Only ever reassigned whole.
180
+ let editing: EditingState = $state.raw(idleState);
178
181
 
179
182
  function computePaletteOklch(index: number, base: Oklch): Oklch {
180
183
  return computePaletteOklchPure(index, base, lightnessCurve, saturationCurve, curveOffset, hueCurve);
@@ -9,7 +9,7 @@ Each file plays a distinct role. Don't merge them without understanding why they
9
9
  | File | Scope | Tokens used | Loaded by | Notes |
10
10
  |---|---|---|---|---|
11
11
  | `tokens.css` | Themed pages | Defines `--color-*`, `--surface-*`, `--text-*`, `--space-*`, … | `main.ts` | **Runtime-edited.** The token editor rewrites this file via the `themeFileApi` Vite plugin. Starter content; consumers replace at will. |
12
- | `ui-editor.css` | Editor chrome only | Defines `--ui-*` (opaque grayscale, system fonts) | JS-imported by `Editor.svelte` and `ComponentEditorPage.svelte` | Deliberately isolated from the theme system so editor surfaces stay neutral while live theme edits flow through the components being edited. Never load globally. |
12
+ | `ui-editor.css` | Editor chrome only | Defines `--ui-*` (opaque grayscale, system fonts) | JS-imported by `Editor.svelte`, `ComponentEditorPage.svelte`, `ColorsPage.svelte`, and the demo's `DemoBar.svelte` | Deliberately isolated from the theme system so editor surfaces stay neutral while live theme edits flow through the components being edited. Never load globally. |
13
13
  | `ui-form-controls.css` | Editor chrome only | Consumes `--ui-*` only | JS-imported by `Editor.svelte` and `ComponentEditorPage.svelte` | `.ui-form-select` / `.ui-form-input` / `.ui-form-field-*` classes. Used by `ProjectFontsSection`, `FontStackEditor`, `DialogEditor`, `NotificationEditor`. Theme-immune — must not reference `--font-*`, `--surface-*`, etc. |
14
14
  | `site.css` | Themed pages | Consumes theme tokens | Page-imported by `Home.svelte` | Consumer-facing starter typography for the landing page (unscoped `h1`, `p`, `a`, …). Never load on editor pages. Replaceable starter content — users edit or replace this file to style their own site. Components that slot consumer content (Card body, CollapsibleSection content, SectionKit) defend their body typography against these global rules with `@include slot-prose` (from `styles/_slot-prose.scss`): the mixin pins only the owned type axes (font-family, font-size, font-weight, line-height, color) plus list spacing onto slotted `p`/`ul`/`ol`/`li`, while `a`, `strong`, font-style, letter-spacing and text-transform are left to inherit the page (links stay links, a consumer's italics survive). `prose={false}` opts a slot out entirely, and the `check:slot-prose` guard fails the build on a hand-rolled copy of the pin. The demo page no longer imports it; the demo uses scoped classes and component-owned slot typography instead. |
15
15
  | `fonts.css` + `fonts/` | Themed pages only | n/a (`@font-face` only) | `main.ts` | Build-special-cased: copied directly to `dist/` without processing so Vite doesn't inline woff2 files as base64. Starter content; editor's font invariant means these never affect chrome. |