@motion-proto/live-tokens 0.83.0 → 0.85.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/.claude/skills/live-tokens-create-component/references/token-naming.md +0 -1
- package/CHANGELOG.md +72 -0
- package/dist-plugin/{chunk-RFVYPNRO.js → chunk-GWMEYBZ2.js} +43 -1
- package/dist-plugin/{chunk-6JKUYCPL.js → chunk-ONYAIXCZ.js} +104 -1
- package/dist-plugin/{chunk-LBZISJPG.js → chunk-W5VQV32K.js} +1 -1
- package/dist-plugin/index.cjs +152 -7
- package/dist-plugin/index.js +3 -3
- package/dist-plugin/migrateData/index.cjs +108 -5
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +104 -1
- package/dist-plugin/setColors/index.js +1 -1
- package/dist-plugin/setGeometry/index.cjs +105 -3
- package/dist-plugin/setGeometry/index.js +2 -3
- package/dist-plugin/tokensCssMigrations/index.cjs +43 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +1 -1
- package/src/editor/core/components/aliasKinds.ts +1 -1
- package/src/editor/core/store/editorPersistence.ts +46 -23
- package/src/editor/core/store/editorStore.ts +1 -5
- package/src/editor/core/store/editorTypes.ts +11 -7
- package/src/editor/core/themes/migrations/2026-09-20-scrim-color-and-opacity.ts +45 -0
- package/src/editor/core/themes/migrations/2026-09-21-scrim-surface.ts +56 -0
- package/src/editor/core/themes/migrations/2026-09-21-wash-color-and-opacity.ts +51 -0
- package/src/editor/core/themes/migrations/index.ts +6 -0
- package/src/editor/core/themes/slices/washes.ts +59 -77
- package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
- package/src/editor/ui/UIPaletteSelector.svelte +5 -1
- package/src/editor/ui/sections/WashesSection.svelte +100 -99
- package/src/live-tokens/data/colors-and-type/autumn.json +9 -7
- package/src/live-tokens/data/colors-and-type/default.json +9 -7
- package/src/live-tokens/data/colors-and-type/halloween.json +9 -7
- package/src/live-tokens/data/colors-and-type/midnight-study.json +9 -7
- package/src/live-tokens/data/colors-and-type/ocean.json +9 -7
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +9 -7
- package/src/live-tokens/data/colors-and-type/sketchy.json +9 -7
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +9 -7
- package/src/live-tokens/data/colors-and-type/sunset.json +9 -7
- package/src/live-tokens/data/themes/autumn.json +11 -9
- package/src/live-tokens/data/themes/halloween.json +11 -9
- package/src/live-tokens/data/themes/midnight-study.json +11 -9
- package/src/live-tokens/data/themes/ocean.json +11 -9
- package/src/live-tokens/data/themes/royal-velvet.json +11 -9
- package/src/live-tokens/data/themes/sketchy.json +11 -9
- package/src/live-tokens/data/themes/spring-meadow.json +11 -9
- package/src/live-tokens/data/themes/sunset.json +11 -9
- package/src/live-tokens/data/tokens.generated.css +8 -6
- package/src/system/components/ImageLightbox.svelte +5 -19
- package/src/system/styles/tokens.css +22 -9
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A wash family's colour and its strengths, apart (2026-09-21).
|
|
5
|
+
*
|
|
6
|
+
* A theme stored each `--scrim-*` and `--tint-*` stop as a composed fill,
|
|
7
|
+
* `color-mix(in srgb, var(<colour>) N%, transparent)`, so the colour was
|
|
8
|
+
* restated three times and the fill overrode the one tokens.css composes from
|
|
9
|
+
* `--scrim-color` and `--scrim-opacity-*`. That left Dialog and ImageLightbox,
|
|
10
|
+
* which read the pair, deaf to every theme's scrim.
|
|
11
|
+
*
|
|
12
|
+
* Each family now stores `--<family>-color` and `--<family>-opacity-*`. The
|
|
13
|
+
* base stop gives the colour, falling back to the first stop that parses. A
|
|
14
|
+
* stop that does not parse is a hand-written fill: it stays, so the page keeps
|
|
15
|
+
* painting what the theme's author chose.
|
|
16
|
+
*/
|
|
17
|
+
const FAMILIES = ['scrim', 'tint'] as const;
|
|
18
|
+
|
|
19
|
+
const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+(?:\.\d+)?)%,\s*transparent\)$/i;
|
|
20
|
+
const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
21
|
+
|
|
22
|
+
function parseFill(raw: string): { color: string; opacity: number } | null {
|
|
23
|
+
const s = raw.trim();
|
|
24
|
+
const mix = s.match(COLOR_MIX_RE);
|
|
25
|
+
if (mix) return { color: mix[1], opacity: Math.min(100, Number(mix[2])) / 100 };
|
|
26
|
+
const plain = s.match(PLAIN_VAR_RE);
|
|
27
|
+
return plain ? { color: plain[1], opacity: 1 } : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const colorsAndTypeMigration_2026_09_21_washColorAndOpacity: Migration = {
|
|
31
|
+
id: '2026-09-21-wash-color-and-opacity',
|
|
32
|
+
fromVersion: 8,
|
|
33
|
+
toVersion: 9,
|
|
34
|
+
appliesTo: 'colors-and-type',
|
|
35
|
+
apply(rawVars) {
|
|
36
|
+
const out = { ...rawVars };
|
|
37
|
+
for (const family of FAMILIES) {
|
|
38
|
+
let color: string | null = null;
|
|
39
|
+
for (const suffix of ['', '-low', '-high']) {
|
|
40
|
+
const key = `--${family}${suffix}`;
|
|
41
|
+
const parsed = out[key] === undefined ? null : parseFill(out[key]);
|
|
42
|
+
if (!parsed) continue;
|
|
43
|
+
color ??= parsed.color;
|
|
44
|
+
out[`--${family}-opacity${suffix}`] = String(parsed.opacity);
|
|
45
|
+
delete out[key];
|
|
46
|
+
}
|
|
47
|
+
if (color) out[`--${family}-color`] = `var(${color})`;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -80,6 +80,9 @@ import { componentMigration_2026_09_13_hairline } from './2026-09-13-hairline';
|
|
|
80
80
|
import { componentMigration_2026_09_13_indicator } from './2026-09-13-indicator';
|
|
81
81
|
import { componentMigration_2026_09_13_sectiondividerSurface } from './2026-09-13-sectiondivider-surface';
|
|
82
82
|
import { componentMigration_2026_09_20_imagelightboxScrim } from './2026-09-20-imagelightbox-scrim';
|
|
83
|
+
import { componentMigration_2026_09_20_scrimColorAndOpacity } from './2026-09-20-scrim-color-and-opacity';
|
|
84
|
+
import { colorsAndTypeMigration_2026_09_21_washColorAndOpacity } from './2026-09-21-wash-color-and-opacity';
|
|
85
|
+
import { componentMigration_2026_09_21_scrimSurface } from './2026-09-21-scrim-surface';
|
|
83
86
|
import { componentMigration_2026_09_13_cornerbadgePrefix } from './2026-09-13-cornerbadge-prefix';
|
|
84
87
|
import { componentMigration_2026_09_13_toggleLabel } from './2026-09-13-toggle-label';
|
|
85
88
|
import { componentMigration_2026_09_13_collapsiblesectionOpen } from './2026-09-13-collapsiblesection-open';
|
|
@@ -134,6 +137,9 @@ export const MIGRATIONS: Migration[] = [
|
|
|
134
137
|
componentMigration_2026_09_13_collapsiblesectionOpen,
|
|
135
138
|
componentMigration_2026_09_13_badgeBrand,
|
|
136
139
|
componentMigration_2026_09_20_imagelightboxScrim,
|
|
140
|
+
componentMigration_2026_09_20_scrimColorAndOpacity,
|
|
141
|
+
colorsAndTypeMigration_2026_09_21_washColorAndOpacity,
|
|
142
|
+
componentMigration_2026_09_21_scrimSurface,
|
|
137
143
|
];
|
|
138
144
|
|
|
139
145
|
function countFor(kind: 'colors-and-type' | 'component-config'): number {
|
|
@@ -1,107 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Washes slice — a wash is a translucent layer of color
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* every stop).
|
|
2
|
+
* Washes slice — a wash is a translucent layer of color. Each family holds one
|
|
3
|
+
* colour, aliased to a theme token, and three opacity stops. The theme emits
|
|
4
|
+
* the colour and the stops; tokens.css composes the `--scrim-*` and `--tint-*`
|
|
5
|
+
* fills from them, so the strength moves without restating the colour.
|
|
7
6
|
*
|
|
8
7
|
* Two families, opposite jobs. A **scrim** dims what sits behind it, which is
|
|
9
|
-
* what a modal needs. A **tint** shades the surface it
|
|
10
|
-
* a hover needs
|
|
11
|
-
* surface
|
|
12
|
-
* darkens a light one without picking a direction.
|
|
13
|
-
*
|
|
14
|
-
* Defaults diverge from tokens.css by design: the editor starts from a
|
|
15
|
-
* neutral alias and tokens.css continues to win until first edit.
|
|
8
|
+
* what a modal needs, so its stops run dark. A **tint** shades the surface it
|
|
9
|
+
* sits on, which is what a hover needs, so its stops run faint. Scrims alias a
|
|
10
|
+
* near-black surface and tints the text color, so a tint lightens a dark theme
|
|
11
|
+
* and darkens a light one without picking a direction.
|
|
16
12
|
*/
|
|
17
|
-
import type { EditorState,
|
|
13
|
+
import type { EditorState, WashScale } from '../../store/editorTypes';
|
|
18
14
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ variable: '--scrim', label: 'Base', alias: '--surface-neutral-lowest', opacity: 0.51 },
|
|
23
|
-
{ variable: '--scrim-high', label: 'High', alias: '--surface-neutral-lowest', opacity: 0.64 },
|
|
24
|
-
];
|
|
25
|
-
}
|
|
15
|
+
export type WashFamily = 'scrim' | 'tint';
|
|
16
|
+
|
|
17
|
+
export const WASH_FAMILIES: readonly WashFamily[] = ['scrim', 'tint'];
|
|
26
18
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
export function makeDefaultWashScale(family: WashFamily): WashScale {
|
|
20
|
+
if (family === 'scrim') {
|
|
21
|
+
return {
|
|
22
|
+
color: '--surface-neutral-lowest',
|
|
23
|
+
stops: [
|
|
24
|
+
{ variable: '--scrim-opacity-low', label: 'Low', opacity: 0.7 },
|
|
25
|
+
{ variable: '--scrim-opacity', label: 'Base', opacity: 0.8 },
|
|
26
|
+
{ variable: '--scrim-opacity-high', label: 'High', opacity: 0.9 },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
color: '--text-primary',
|
|
32
|
+
stops: [
|
|
33
|
+
{ variable: '--tint-opacity-low', label: 'Low', opacity: 0.05 },
|
|
34
|
+
{ variable: '--tint-opacity', label: 'Base', opacity: 0.1 },
|
|
35
|
+
{ variable: '--tint-opacity-high', label: 'High', opacity: 0.15 },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export function makeDefaultWashesState(): EditorState['washes'] {
|
|
36
41
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
scrim: makeDefaultWashScale('scrim'),
|
|
43
|
+
tint: makeDefaultWashScale('tint'),
|
|
39
44
|
};
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
export const
|
|
43
|
-
'--scrim-low', '--scrim', '--scrim-high',
|
|
44
|
-
'--tint-low', '--tint', '--tint-high',
|
|
45
|
-
] as const;
|
|
47
|
+
export const washColorVar = (family: WashFamily) => `--${family}-color`;
|
|
46
48
|
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
49
|
+
export const WASH_VAR_NAMES: readonly string[] = WASH_FAMILIES.flatMap((family) => [
|
|
50
|
+
washColorVar(family),
|
|
51
|
+
...makeDefaultWashScale(family).stops.map((s) => s.variable),
|
|
52
|
+
]);
|
|
52
53
|
|
|
53
|
-
const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+)%,\s*transparent\)$/i;
|
|
54
54
|
const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
55
55
|
|
|
56
|
-
export function parseWashCss(raw: string): { alias: string; opacity: number } | null {
|
|
57
|
-
const s = raw.trim();
|
|
58
|
-
const mix = s.match(COLOR_MIX_RE);
|
|
59
|
-
if (mix) {
|
|
60
|
-
const pct = parseInt(mix[2], 10);
|
|
61
|
-
if (!Number.isFinite(pct)) return null;
|
|
62
|
-
return { alias: mix[1], opacity: Math.max(0, Math.min(100, pct)) / 100 };
|
|
63
|
-
}
|
|
64
|
-
const plain = s.match(PLAIN_VAR_RE);
|
|
65
|
-
if (plain) return { alias: plain[1], opacity: 1 };
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
56
|
export function washesToVars(w: EditorState['washes']): Record<string, string> {
|
|
70
57
|
const out: Record<string, string> = {};
|
|
71
|
-
for (const
|
|
72
|
-
|
|
58
|
+
for (const family of WASH_FAMILIES) {
|
|
59
|
+
out[washColorVar(family)] = `var(${w[family].color})`;
|
|
60
|
+
for (const s of w[family].stops) out[s.variable] = String(s.opacity);
|
|
61
|
+
}
|
|
73
62
|
return out;
|
|
74
63
|
}
|
|
75
64
|
|
|
76
|
-
export function applyWashVarsToState(washes: EditorState['washes'], vars: Record<string, string>): void {
|
|
77
|
-
const applyTo = (list: WashToken[]) => {
|
|
78
|
-
for (const t of list) {
|
|
79
|
-
const raw = vars[t.variable];
|
|
80
|
-
if (!raw) continue;
|
|
81
|
-
const parsed = parseWashCss(raw);
|
|
82
|
-
if (!parsed) continue;
|
|
83
|
-
t.alias = parsed.alias;
|
|
84
|
-
t.opacity = parsed.opacity;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
applyTo(washes.scrims);
|
|
88
|
-
applyTo(washes.tints);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
65
|
/**
|
|
92
|
-
* Loader: route
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* the cssVars bag so the DOM still paints them; the user's next edit in the
|
|
96
|
-
* picker promotes them to the typed slice.
|
|
66
|
+
* Loader: route a freshly-loaded theme's wash entries into `next.washes` and
|
|
67
|
+
* remove them from the bag. A colour that is not a plain `var()` stays in the
|
|
68
|
+
* bag so the DOM still paints it.
|
|
97
69
|
*/
|
|
98
70
|
export function loadWashesFromVars(
|
|
99
71
|
next: EditorState,
|
|
100
72
|
rawVars: Record<string, string>,
|
|
101
73
|
): void {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
74
|
+
for (const family of WASH_FAMILIES) {
|
|
75
|
+
const scale = next.washes[family];
|
|
76
|
+
const colorVar = washColorVar(family);
|
|
77
|
+
const color = rawVars[colorVar]?.trim().match(PLAIN_VAR_RE);
|
|
78
|
+
if (color) {
|
|
79
|
+
scale.color = color[1];
|
|
80
|
+
delete rawVars[colorVar];
|
|
81
|
+
}
|
|
82
|
+
for (const s of scale.stops) {
|
|
83
|
+
const n = Number(rawVars[s.variable]);
|
|
84
|
+
if (rawVars[s.variable] === undefined || !Number.isFinite(n)) continue;
|
|
85
|
+
s.opacity = Math.max(0, Math.min(1, n));
|
|
86
|
+
delete rawVars[s.variable];
|
|
87
|
+
}
|
|
106
88
|
}
|
|
107
89
|
}
|
|
@@ -14,7 +14,7 @@ export const skillDocs: Record<string, Record<string, string[]>> = {
|
|
|
14
14
|
"references/intrinsics.md": ["# Extension: intrinsics","","Some components expose **structural or display choices** that are not token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a custom `<select>` or checkbox authored in an editor snippet, outside the generic token grid, so they do not belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).","","An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the \"change\" the user thinks they made. `:global(:root)` is the source of truth.","","Declare intrinsics so the editor and the contract test stay honest:","","1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:",""," ```css"," --mywidget-lg-align: start;"," --mywidget-lg-eyebrow-display: block;"," ```","","2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:",""," ```ts"," import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';",""," export const intrinsics: IntrinsicSpec[] = ["," {"," key: 'align',"," variants: ['lg', 'md', 'sm'],"," variable: (v) => `--mywidget-${v}-align`,"," values: ['start', 'center'],"," default: { lg: 'start', md: 'start', sm: 'start' },"," },"," ];"," ```","","3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:",""," ```ts"," const byKey = new Map(intrinsics.map((i) => [i.key, i]));"," function readIntrinsic(key: string, v: string): string {"," const spec = byKey.get(key)!;"," const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default"," return spec.normalize ? spec.normalize(raw) : raw;"," }"," function getAlign(v: string) {"," return readIntrinsic('align', v) === 'center' ? 'center' : 'start';"," }"," ```",""," Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.","","4. **Put `intrinsics` on the registry entry** so the contract test can see it. That is the same entry the recipe passes to `bootLiveTokens`, with one more field:",""," ```ts"," bootLiveTokens(App, '#app', {"," components: [{"," id: 'mywidget',"," // ...label, icon, sourceFile, editorComponent, schema..."," intrinsics: myWidgetIntrinsics,"," }],"," });"," ```","","Use `normalize` only when two raw values render identically and the dropdown lists one (SectionDivider folds `above-description` into `below-label`). Properties that resemble intrinsics but are not: preview-only props with no persistence (a size selector that only changes the demo), `setComponentConfig` editor metadata (Dialog's button variants), and token-valued selects (a control choosing between two tokens). None carry a duplicated runtime default, so none need an `IntrinsicSpec`."],
|
|
15
15
|
"references/linked-siblings.md": ["# Extension: linked siblings","","Read this when the component has more than one variant and those variants share base properties (surface, radius, padding) that should move together. Toggle and SectionDivider have no linked tokens and skip all of it.","","Toggle's tokens are flat per state. Most multi-variant components (Badge, Card, SegmentedControl) share base properties across variants and surface that equality via a *linked block*: one edit propagates to every variant, while per-variant properties stay independent. Five additions to the Toggle pattern; see `BadgeEditor.svelte` in `node_modules` for the full file.","","1. **Mark linkable tokens** with `canBeLinked: true` + a `groupKey`. Peers sharing a `groupKey` form a link set across variants.",""," ```ts"," function variantBaseTokens(v: Variant): Token[] {"," return ["," { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--badge-${v}-padding` },"," { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--badge-${v}-radius` },"," ];"," }"," // Colors omit canBeLinked. Per-variant by design."," function variantColorTokens(v: Variant): Token[] {"," return ["," { label: 'surface color', groupKey: 'surface', variable: `--badge-${v}-surface` },"," { label: 'text color', groupKey: 'text', variable: `--badge-${v}-text` },"," ];"," }"," ```","","2. **Build a `linkableContexts: Map<variable, contextLabel>`** in `<script module>`. The label (e.g. `\"success base\"`) is how the LinkageChart row identifies this variable. Plain literal Map, no helper needed.",""," ```ts"," const linkableContexts = new Map<string, string>("," variants.flatMap((v) =>"," variantBaseTokens(v)"," .filter((t) => t.canBeLinked)"," .map((t) => [t.variable, `${v} base`] as [string, string]),"," ),"," );"," ```","","3. **Compute `linked` and mask currently-linked rows** out of per-state lists, so they render once inside the LinkedBlock instead of twice.",""," ```ts"," import { editorState } from '@motion-proto/live-tokens';"," import { computeLinkedBlock, withLinkedDisabled, buildSiblings }"," from '@motion-proto/live-tokens/component-editor';",""," let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));"," let visibleVariantStates = $derived((v: Variant) => Object.fromEntries("," Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),"," ));"," ```","","4. **Pass `{linked}` to `ComponentEditorBase`** so the LinkedBlock renders above the variant groups.","","5. **Multi-variant editors iterate VariantGroups** with `buildSiblings` so cross-variant link rows resolve to their peers.",""," ```svelte"," <ComponentEditorBase {component} title=\"Badge\" tokens={allTokens} {linked} variants={variantOptions}>"," {#each variants as v}"," <VariantGroup"," name={v}"," title={v}"," states={visibleVariantStates(v)}"," {component}"," siblings={buildSiblings(variants, v, variantStates)}"," >"," ...preview snippet"," </VariantGroup>"," {/each}"," </ComponentEditorBase>"," ```","","Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps; `ComponentEditorBase` renders without a `{linked}` prop."],
|
|
16
16
|
"references/sketch-mode.md": ["# Joining the sketch layer","","Sketch mode blanks each part's real background and border and repaints them onto","`::before`/`::after` through a shared noise field. It draws a fixed set of","selectors: the shipped components, plus four classes reserved for everyone else.","A component is skipped until it opts in, so it stays crisp while the page","around it goes hand-drawn.","","The whole contract is CSS. There is nothing to import and no function to call:","the layer exports no runtime API, and a component joins it by carrying a class","and naming five custom properties.","","## What the layer takes over","","An opted-in element is no longer painting itself. On every drawn part the layer","forces:","","| It forces | So the component must |","|----------------------------------------------|----------------------------------------------------------|","| `background: transparent !important` | Name the fill again as `--sketch-fill` |","| `border-color: transparent !important` | Name the outline again as `--sketch-stroke` |","| `box-shadow: none !important` | Name the shadow again as `--sketch-shadow` |","| `overflow: visible !important` | Never put the class on a box whose clip carries meaning |","| `position: relative` | Never put the class on an absolutely-positioned root |","| `z-index: 0` | Expect a new stacking context on that element |","| `::before` (the fill), `::after` (the stroke) | Never own either pseudo-element on that element |","","`::after` survives on `sketch-rule` alone, which draws no outline. `::before` is","claimed on all four.","","## Opting in","","Put one class on the runtime component's root, chosen by **size, not by kind**.","A card and a modal are both containers; a badge and a pill are both chips.","","| Class | For |","|---------------------|-------------------------------------------------------------|","| `sketch-surface` | A box. The default treatment. |","| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |","| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |","| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |","","The class opts the component in and nothing more. It names no colour, so the layer emits","no rule for it and whatever the component declares survives.","","```svelte","<div class=\"mywidget sketch-container {variant}\">…</div>","","<style>"," .mywidget {"," background: var(--mywidget-surface);"," border: var(--mywidget-border-width) solid var(--mywidget-border);"," border-radius: var(--mywidget-radius);"," box-shadow: var(--mywidget-shadow);",""," /* The layer hides all four above. These are what it draws instead. */"," --sketch-fill: var(--mywidget-surface);"," --sketch-stroke: var(--mywidget-border);"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: var(--mywidget-radius);"," --sketch-shadow: var(--mywidget-shadow);"," }","</style>","```","","State all five. `--sketch-radius` is registered as an inheriting `<length>`, and","the other four inherit as ordinary custom properties, so a part that states","nothing is drawn with its **ancestor's** value: a badge inside a hatched card","stripes itself in the card's ink, and a square header inside a rounded card","picks up the card's corners.","","## Variants, states and inner parts","","Nothing competes for these values, so every case is one more","declaration at the specificity already in use.","","```css",".mywidget.danger { --sketch-fill: var(--mywidget-danger-surface); }",".mywidget:hover,",".mywidget.force-hover { --sketch-stroke: var(--mywidget-hover-border); }","```","","Pair every `:hover` with `.force-hover`, as elsewhere: that is the editor's","preview of the hover state, and a hover the sketch layer cannot paint reads as","no hover at all once the real background is transparent.","","An inner part that carries its own surface (a header strip, a footer) takes its","own class and its own five values. The class is easy to forget, because the part","already has its own values and looks finished without it. A part carrying only","the values is left crisp, and reads as a hard-edged rectangle dropped inside a","drawn box. No checker sees it. Where such a part draws no outline, bind the","hatch ink to the ink its **parent** is outlined in, so the component reads as one","drawing rather than a shaded panel dropped into a box:","","```svelte","<span class=\"mywidget-header sketch-chip\">{label}</span>","```","","```css",".mywidget-header {"," --sketch-fill: var(--mywidget-header-surface);"," --sketch-stroke: transparent;"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: 0px;","}","```","","A part with a visible stroke needs no `--sketch-hatch-color`; it falls back to","the stroke and follows it into hover. A part with no fill wants none, because","there is no surface there to shade.","","A gradient is a valid fill. The `background` shorthand's last layer takes a","colour or an image, so `--sketch-fill` accepts either.","","## Where the class does not go","","- **A positioned root.** The layer forces `position: relative` on parts that sit"," in flow, which drops an absolutely or fixed-positioned element back to its"," flow position. Put the class on an inner box instead.","- **A box that clips something real.** `overflow` is forced visible so the ink"," can travel past the border box, and there is no consumer opt-out. A scroller,"," a fill bar held to its track, or a picture held to its frame keeps its clip by"," keeping the class off that element and carrying it on a wrapper.","- **An element that owns `::before` or `::after`.** The layer claims both. A"," shimmer, a caret or a decorative arrow on the opted-in element is gone.","- **A shipped part's selector** (`.card`, `.panel`). Borrowing one to get drawn"," works, but it hands the component that part's colours and its damping, and it"," is package-internal. The reserved classes are the contract.","","## Rules, which are not boxes","","A `border` cannot be displaced: the effect moves boxes, and a border is not one.","Make the rule an element, give it `sketch-rule`, and name its ink as the fill.","","```svelte","<span class=\"mywidget-rule sketch-rule\" aria-hidden=\"true\"></span>","```","```css",".mywidget-rule {"," height: var(--border-width-2);"," background: var(--mywidget-hairline-color);"," --sketch-fill: var(--mywidget-hairline-color);","}","```","","## Media inside the component","","A drawn part's `overflow` is visible so the fill and outline can travel past the","box. A background that bleeds is the effect working. An image that bleeds is","not, since it keeps square corners while the part around it turns. Media running","to the component's edge has to carry the corners itself:","","```css",".mywidget-cover {"," overflow: hidden;"," border-top-left-radius: var(--sketch-radius, var(--mywidget-radius));"," border-top-right-radius: var(--sketch-radius, var(--mywidget-radius));","}","```","","`--sketch-radius` is the radius the layer drew and it inherits, so the fallback","covers the effect being off. Corner spread is per-corner and per-instance, so at","high spread the crop is the mean rather than an exact trace of the drawn edge.","","## Icons and SVG","","Icons and inline SVG take the wobble directly, since a glyph has no box to","redraw. Body type is left alone deliberately: an icon is a shape and survives a","wobble, a paragraph is not. Nothing opts into this; it applies to every","`[class*=\"fa-\"]` and every `svg` under the scope.","","`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It","inherits, so one declaration covers everything under it:","","```css","/* Crisp. Chrome, a logo, anything that has to stay exact. */",".mywidget-toolbar { --sketch-icon-off: none; }","","/* Drawn back rather than off, at a third of the travel. Small artwork, and"," type set as an SVG, which the layer reads as one large glyph. */",".mywidget-mark { --sketch-icon-off: var(--sketch-icon-soft); }","```","","Travel is stated in px against a glyph whose size the layer cannot know, so the","dial that suits a card's worth of artwork tears a 16px icon apart. Reach for the","soft bank before reaching for `none`.","","## First-party components","","A component authored inside the package does not use the reserved classes. Add a","`PartSpec` row to `PART_SPECS` in `src/editor/core/sketch/sketchLayer.ts`","instead, which is keyed to the component's own token stem and gets the shipped","damping. `sketchPartTokens.test.ts` then holds the component to it: every colour the layer","paints must be one the component itself assigns to that same element, checked","against the compiled `<style>` block.","","## Verify","","Switch Sketch mode on from the editor's **Sketchstyle** view, then check the","component in place:","","- [ ] Drawn, not crisp, in every variant.","- [ ] Wearing its own colours, not its parent's, including inner parts.","- [ ] Hover repaints. The wobble holds still while it does.","- [ ] Hatched fill uses ink that belongs to the component.","- [ ] Media at the component's edge turns with the drawn corners.","- [ ] Nothing that has to stay exact is torn: icons, clipped content, overlays.","- [ ] Switch it off. Every trace is gone and the component is unchanged."],
|
|
17
|
-
"references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-color` | A hairline rule's colour, or a color no role word above names |","| `-shadow` | Box-shadow |","| `-
|
|
17
|
+
"references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-color` | A hairline rule's colour, or a color no role word above names |","| `-shadow` | Box-shadow |","| `-blur` | Backdrop or filter blur radius |","| `-tint` | A wash over the surface, aliasing a `--tint-*` stop |","| `-indicator` | The colour of the bar or stripe that marks an item |","| `-thumb` | A scrollbar or slider thumb's colour |","| `-title` | Title text colour |","| `-body` | Body text colour |","| `-eyebrow` | Eyebrow text colour |","| `-description` | Description text colour |","| `-hint` | Hint text colour |","| `-error` | Error text colour |","| `-placeholder` | Placeholder text colour |","| `-value` | A displayed value's colour |","","## Geometry","","| Suffix | Meaning |","|-----------------|---------------------------------------------------------------|","| `-radius` | Corner radius |","| `-border-width` | Stroke thickness (used even when CSS uses `outline:`) |","| `-indicator-width` | An indicator's thickness |","| `-hairline-width` | A hairline rule's thickness |","| `-dot-size` | A dot indicator's diameter |","| `-hairline-inset` | Inset trimmed from a stretched hairline |","| `-track-height` | A track's height (progress bar, slider) |","| `-icon-size` | An icon's rendered size |","| `-thumb-size` | A thumb's rendered size |","| `-height` | A measured height (a track, a panel) |","| `-margin` | Outer spacing, moved on the same scale as `-padding` |","| `-duration` | Motion duration |","| `-easing` | Motion easing curve |","| `-scale` | A transform scale factor |","| `-width` | Width dimension |","| `-size` | Square / uniform dimension |","| `-padding` | Internal spacing |","| `-gap` | Spacing between sibling elements |","","`-width`, `-height` and `-size` are the fall-through: any dimension with no","more specific name behind it. They read the `--space-*` scale through the same","picker `-gap` uses, and they match last, so `-border-width`, `-hairline-width`,","`-icon-size` and the rest claim their token first. Reach for the specific name","when one fits; a stroke is `-border-width` even where the CSS says `outline:`.","","## Typography","","| Suffix | Meaning |","|--------------------|--------------------------|","| `-font-family` | Font family reference |","| `-font-weight` | Font weight reference |","| `-font-size` | Font size reference |","| `-line-height` | Line height |","| `-letter-spacing` | Letter spacing |","","A suffix that is not here is either a rename away from one that is, or","an issue against `@motion-proto/live-tokens`. Inventing one costs the token its","picker: the editor falls back to a plain text input."],
|
|
18
18
|
},
|
|
19
19
|
"create-page": {
|
|
20
20
|
"SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints a component's declared props, the values each union accepts, and its catalogue entry, including its constraints and when not to use it. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `--body-sm-*` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. A `danger` Button sits apart from the group it could be mistaken for. A vertical stack of Buttons sets `fullWidth` on each Button; a row omits it.","- For a `MenuSelect` picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Card title | the Card `title` prop |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Command or value | `code` in `--code-*` |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h4`, `p`, `code`, `pre`, and list items from these styles, so the tag carries the style. Type an element only when the table gives its tag a different style. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
* arbitrary page content (a dropdown panel) and must stay legible there.
|
|
46
46
|
* A floor also retires "None", which would defeat it. */
|
|
47
47
|
minOpacity?: number;
|
|
48
|
+
/** When false, the picker writes the colour at full strength and hides its
|
|
49
|
+
* opacity control. For a colour whose strength another token holds. */
|
|
50
|
+
showOpacity?: boolean;
|
|
48
51
|
onchange?: () => void;
|
|
49
52
|
/** Forwarded to UITokenSelector — when set, writes route through this
|
|
50
53
|
* callback instead of the DOM. See UITokenSelector.onwrite. */
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
selectionsLocked = false,
|
|
60
63
|
familyFilter = null,
|
|
61
64
|
showNone = true,
|
|
65
|
+
showOpacity = true,
|
|
62
66
|
minOpacity = 0,
|
|
63
67
|
onchange,
|
|
64
68
|
onwrite,
|
|
@@ -599,7 +603,7 @@
|
|
|
599
603
|
</div>
|
|
600
604
|
{/snippet}
|
|
601
605
|
{#snippet subheader()}
|
|
602
|
-
<div class="opacity-control" class:hidden={chosenGradient !== null}>
|
|
606
|
+
<div class="opacity-control" class:hidden={chosenGradient !== null || !showOpacity}>
|
|
603
607
|
<span class="opacity-label">opacity</span>
|
|
604
608
|
<input type="range" min={minOpacity} max="100" bind:value={opacity} class="opacity-slider" oninput={applyOpacity} />
|
|
605
609
|
<input type="number" min={minOpacity} max="100" bind:value={opacity} class="opacity-input" onchange={applyOpacity} />
|