@motion-proto/live-tokens 0.68.1 → 0.69.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-build-page/SKILL.md +16 -5
- package/.claude/skills/live-tokens-create-component/SKILL.md +43 -8
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +30 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +131 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +2 -1
- package/CHANGELOG.md +207 -0
- package/README.md +11 -4
- package/bin/check-component.mjs +367 -63
- package/bin/check-page.mjs +409 -0
- package/bin/cli.mjs +57 -8
- package/bin/lib/cssValues.mjs +50 -0
- package/bin/lib/findings.mjs +106 -0
- package/bin/lib/tokenVocabulary.mjs +213 -0
- package/dist-plugin/adjust/index.cjs +174 -23
- package/dist-plugin/adjust/index.js +68 -23
- package/dist-plugin/{chunk-2UX6EVVA.js → chunk-2YNERPXY.js} +1 -1
- package/dist-plugin/{chunk-NE6N66EE.js → chunk-GPIBU44G.js} +107 -1
- package/dist-plugin/{chunk-ZHPX7ZYQ.js → chunk-RFVYPNRO.js} +39 -1
- package/dist-plugin/generateColorsAndType/index.cjs +107 -1
- package/dist-plugin/generateColorsAndType/index.js +1 -1
- package/dist-plugin/index.cjs +146 -2
- package/dist-plugin/index.js +3 -3
- package/dist-plugin/migrateData/index.cjs +107 -1
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +3 -2
- package/src/app/site.css +4 -4
- package/src/editor/component-editor/ButtonEditor.svelte +71 -4
- package/src/editor/component-editor/CardEditor.svelte +6 -6
- package/src/editor/component-editor/DialogEditor.svelte +3 -3
- package/src/editor/component-editor/IconButtonEditor.svelte +68 -3
- package/src/editor/component-editor/ImageEditor.svelte +8 -8
- package/src/editor/component-editor/ImageLightboxEditor.svelte +12 -1
- package/src/editor/component-editor/MenuSelectEditor.svelte +63 -3
- package/src/editor/component-editor/SegmentedControlEditor.svelte +63 -3
- package/src/editor/component-editor/SideNavigationEditor.svelte +67 -3
- package/src/editor/component-editor/SliderEditor.svelte +186 -0
- package/src/editor/component-editor/TabBarEditor.svelte +63 -3
- package/src/editor/component-editor/registry.ts +10 -0
- package/src/editor/core/components/aliasKinds.ts +51 -28
- package/src/editor/core/sketch/sketchLayer.ts +16 -0
- package/src/editor/core/store/editorPersistence.ts +44 -1
- package/src/editor/core/store/editorRenderer.ts +2 -2
- package/src/editor/core/store/editorStore.ts +18 -18
- package/src/editor/core/store/editorTypes.ts +7 -6
- package/src/editor/core/themes/migrations/2026-09-01-gate-suffix-enabled.ts +31 -0
- package/src/editor/core/themes/migrations/2026-09-01-scrim-rename.ts +52 -0
- package/src/editor/core/themes/migrations/2026-09-01-tabbar-active-tint.ts +27 -0
- package/src/editor/core/themes/migrations/2026-09-01-tint-rename.ts +42 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/slices/domainVars.ts +2 -2
- package/src/editor/core/themes/slices/washes.ts +107 -0
- package/src/editor/docs/content/editing-tokens.md +5 -3
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/pages/EditorShell.svelte +1 -1
- package/src/editor/ui/SurfacesTab.svelte +3 -3
- package/src/editor/ui/UITokenSelector.svelte +1 -0
- package/src/editor/ui/VariablesTab.svelte +2 -2
- package/src/editor/ui/sections/{OverlaysSection.svelte → WashesSection.svelte} +44 -43
- package/src/live-tokens/data/colors-and-type/autumn.json +6 -6
- package/src/live-tokens/data/colors-and-type/default.json +6 -6
- package/src/live-tokens/data/colors-and-type/halloween.json +6 -6
- package/src/live-tokens/data/colors-and-type/midnight-study.json +6 -6
- package/src/live-tokens/data/colors-and-type/ocean.json +6 -6
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +6 -6
- package/src/live-tokens/data/colors-and-type/sketchy.json +6 -6
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +6 -6
- package/src/live-tokens/data/colors-and-type/sunset.json +6 -6
- package/src/live-tokens/data/themes/autumn.json +81 -13
- package/src/live-tokens/data/themes/halloween.json +81 -13
- package/src/live-tokens/data/themes/midnight-study.json +81 -13
- package/src/live-tokens/data/themes/ocean.json +81 -13
- package/src/live-tokens/data/themes/royal-velvet.json +81 -13
- package/src/live-tokens/data/themes/sketchy.json +81 -13
- package/src/live-tokens/data/themes/spring-meadow.json +81 -13
- package/src/live-tokens/data/themes/sunset.json +81 -13
- package/src/live-tokens/data/tokens.generated.css +6 -6
- package/src/system/components/Button.svelte +28 -10
- package/src/system/components/Card.svelte +6 -6
- package/src/system/components/Dialog.svelte +3 -3
- package/src/system/components/IconButton.svelte +23 -7
- package/src/system/components/Image.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +17 -1
- package/src/system/components/SegmentedControl.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +22 -2
- package/src/system/components/Slider.svelte +348 -0
- package/src/system/components/TabBar.svelte +20 -2
- package/src/system/styles/CONVENTIONS.md +2 -2
- package/src/system/styles/tokens.css +12 -4
- package/template/package.json +3 -2
- package/template/src/pages/Home.svelte +1 -1
- package/src/editor/core/themes/slices/overlays.ts +0 -101
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
|
|
3
|
+
import type { Token, TypeGroupConfig } from './scaffolding/types';
|
|
4
|
+
import { sliderVariants } from '../../system/components/Slider.svelte';
|
|
5
|
+
|
|
6
|
+
export const component = 'slider';
|
|
7
|
+
const variants = sliderVariants;
|
|
8
|
+
type Variant = typeof variants[number];
|
|
9
|
+
|
|
10
|
+
// Geometry and colour are identical across the two variants by default, so
|
|
11
|
+
// every row is linkable: an edit to the single slider's track moves the range
|
|
12
|
+
// slider's track until a user unlinks them on purpose.
|
|
13
|
+
function variantDefaultTokens(v: Variant): Token[] {
|
|
14
|
+
return [
|
|
15
|
+
{ label: 'surface', element: 'track', canBeLinked: true, groupKey: 'track-surface', variable: `--slider-${v}-track-surface` },
|
|
16
|
+
{ label: 'border', element: 'track', canBeLinked: true, groupKey: 'track-border', variable: `--slider-${v}-track-border` },
|
|
17
|
+
{ label: 'border width', element: 'track', canBeLinked: true, groupKey: 'track-border-width', variable: `--slider-${v}-track-border-width` },
|
|
18
|
+
{ label: 'height', element: 'track', canBeLinked: true, groupKey: 'track-height', variable: `--slider-${v}-track-height` },
|
|
19
|
+
{ label: 'corner radius', element: 'track', canBeLinked: true, groupKey: 'track-radius', variable: `--slider-${v}-track-radius` },
|
|
20
|
+
{ label: 'color', element: 'fill', canBeLinked: true, groupKey: 'fill', variable: `--slider-${v}-fill` },
|
|
21
|
+
{ label: 'surface', element: 'thumb', canBeLinked: true, groupKey: 'thumb-surface', variable: `--slider-${v}-thumb-surface` },
|
|
22
|
+
{ label: 'border', element: 'thumb', canBeLinked: true, groupKey: 'thumb-border', variable: `--slider-${v}-thumb-border` },
|
|
23
|
+
{ label: 'border width', element: 'thumb', canBeLinked: true, groupKey: 'thumb-border-width', variable: `--slider-${v}-thumb-border-width` },
|
|
24
|
+
{ label: 'size', element: 'thumb', canBeLinked: true, groupKey: 'thumb-size', variable: `--slider-${v}-thumb-size` },
|
|
25
|
+
{ label: 'corner radius', element: 'thumb', canBeLinked: true, groupKey: 'thumb-radius', variable: `--slider-${v}-thumb-radius` },
|
|
26
|
+
{ label: 'shadow', element: 'thumb', canBeLinked: true, groupKey: 'thumb-shadow', variable: `--slider-${v}-thumb-shadow` },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function variantHoverTokens(v: Variant): Token[] {
|
|
31
|
+
return [
|
|
32
|
+
{ label: 'thumb surface', canBeLinked: true, groupKey: 'hover-thumb-surface', variable: `--slider-${v}-hover-thumb-surface` },
|
|
33
|
+
{ label: 'thumb border', canBeLinked: true, groupKey: 'hover-thumb-border', variable: `--slider-${v}-hover-thumb-border` },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function variantDisabledTokens(v: Variant): Token[] {
|
|
38
|
+
return [
|
|
39
|
+
{ label: 'track surface', canBeLinked: true, groupKey: 'disabled-track-surface', variable: `--slider-${v}-disabled-track-surface` },
|
|
40
|
+
{ label: 'fill color', canBeLinked: true, groupKey: 'disabled-fill', variable: `--slider-${v}-disabled-fill` },
|
|
41
|
+
{ label: 'thumb surface', canBeLinked: true, groupKey: 'disabled-thumb-surface', variable: `--slider-${v}-disabled-thumb-surface` },
|
|
42
|
+
{ label: 'thumb border', canBeLinked: true, groupKey: 'disabled-thumb-border', variable: `--slider-${v}-disabled-thumb-border` },
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The label and its gap are shared by both variants, so their rows render
|
|
47
|
+
// once, under the single variant beside the type groups.
|
|
48
|
+
const labelGapToken: Token = { label: 'label gap', element: 'label', groupKey: 'label-gap', variable: '--slider-label-gap' };
|
|
49
|
+
|
|
50
|
+
function variantStates(v: Variant): Record<string, Token[]> {
|
|
51
|
+
return {
|
|
52
|
+
default: v === 'single' ? [...variantDefaultTokens(v), labelGapToken] : variantDefaultTokens(v),
|
|
53
|
+
hover: variantHoverTokens(v),
|
|
54
|
+
disabled: variantDisabledTokens(v),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The label and value readout are shared by both variants, so they live in
|
|
59
|
+
// one type group outside the per-variant states.
|
|
60
|
+
const typeGroups: Record<string, TypeGroupConfig[]> = {
|
|
61
|
+
default: [
|
|
62
|
+
{
|
|
63
|
+
legend: '',
|
|
64
|
+
element: 'label',
|
|
65
|
+
colorVariable: '--slider-label',
|
|
66
|
+
familyVariable: '--slider-label-font-family',
|
|
67
|
+
sizeVariable: '--slider-label-font-size',
|
|
68
|
+
weightVariable: '--slider-label-font-weight',
|
|
69
|
+
lineHeightVariable: '--slider-label-line-height',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
legend: '',
|
|
73
|
+
element: 'value',
|
|
74
|
+
colorVariable: '--slider-value',
|
|
75
|
+
familyVariable: '--slider-value-font-family',
|
|
76
|
+
sizeVariable: '--slider-value-font-size',
|
|
77
|
+
weightVariable: '--slider-value-font-weight',
|
|
78
|
+
lineHeightVariable: '--slider-value-line-height',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const typeGroupTokens: Token[] = [
|
|
84
|
+
{ label: 'font family', canBeLinked: true, groupKey: 'label-font-family', variable: '--slider-label-font-family' },
|
|
85
|
+
{ label: 'font size', canBeLinked: true, groupKey: 'label-font-size', variable: '--slider-label-font-size' },
|
|
86
|
+
{ label: 'font weight', canBeLinked: true, groupKey: 'label-font-weight', variable: '--slider-label-font-weight' },
|
|
87
|
+
{ label: 'line height', canBeLinked: true, groupKey: 'label-line-height', variable: '--slider-label-line-height' },
|
|
88
|
+
{ label: 'font family', canBeLinked: true, groupKey: 'value-font-family', variable: '--slider-value-font-family' },
|
|
89
|
+
{ label: 'font size', canBeLinked: true, groupKey: 'value-font-size', variable: '--slider-value-font-size' },
|
|
90
|
+
{ label: 'font weight', canBeLinked: true, groupKey: 'value-font-weight', variable: '--slider-value-font-weight' },
|
|
91
|
+
{ label: 'line height', canBeLinked: true, groupKey: 'value-line-height', variable: '--slider-value-line-height' },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export const allTokens: Token[] = [
|
|
95
|
+
...variants.flatMap((v) => Object.values(variantStates(v)).flat()),
|
|
96
|
+
...buildTypeGroupColorTokens(typeGroups, { component }),
|
|
97
|
+
...typeGroupTokens,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const linkableContexts = new Map<string, string>([
|
|
101
|
+
...variants.flatMap((v) =>
|
|
102
|
+
Object.entries(variantStates(v)).flatMap(([state, list]) =>
|
|
103
|
+
list.filter((t) => t.canBeLinked).map((t) => [t.variable, `${v} ${state}`] as [string, string]),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
...typeGroupTokens
|
|
107
|
+
.filter((t) => t.canBeLinked)
|
|
108
|
+
.map((t) => [t.variable, t.variable.includes('-label-') ? 'label' : 'value'] as [string, string]),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const variantOptions = variants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<script lang="ts">
|
|
115
|
+
import Slider from '../../system/components/Slider.svelte';
|
|
116
|
+
import VariantGroup from './scaffolding/VariantGroup.svelte';
|
|
117
|
+
import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
|
|
118
|
+
import { editorState } from '../core/store/editorStore';
|
|
119
|
+
import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
|
|
120
|
+
import { buildSiblings } from './scaffolding/siblings';
|
|
121
|
+
|
|
122
|
+
let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
|
|
123
|
+
let visibleVariantStates = $derived((v: Variant) => Object.fromEntries(
|
|
124
|
+
Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
|
|
125
|
+
) as Record<string, Token[]>);
|
|
126
|
+
|
|
127
|
+
let value = $state(40);
|
|
128
|
+
let low = $state(20);
|
|
129
|
+
let high = $state(70);
|
|
130
|
+
|
|
131
|
+
function previewProps(activeState: string) {
|
|
132
|
+
return {
|
|
133
|
+
disabled: activeState === 'disabled',
|
|
134
|
+
forceClass: activeState === 'hover' ? 'force-hover' : '',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<ComponentEditorBase
|
|
140
|
+
{component}
|
|
141
|
+
title="Slider"
|
|
142
|
+
description="A number chosen by position on a track. Single moves one thumb to a value; range moves two thumbs to a low and a high bound."
|
|
143
|
+
tokens={allTokens}
|
|
144
|
+
{linked}
|
|
145
|
+
variants={variantOptions}
|
|
146
|
+
>
|
|
147
|
+
{#each variants as v}
|
|
148
|
+
<VariantGroup
|
|
149
|
+
name={v}
|
|
150
|
+
title={v.charAt(0).toUpperCase() + v.slice(1)}
|
|
151
|
+
states={visibleVariantStates(v)}
|
|
152
|
+
typeGroups={v === 'single' ? typeGroups : undefined}
|
|
153
|
+
{component}
|
|
154
|
+
siblings={buildSiblings(variants, v, variantStates)}
|
|
155
|
+
>
|
|
156
|
+
{#snippet children({ activeState })}
|
|
157
|
+
{@const p = previewProps(activeState)}
|
|
158
|
+
<div class="slider-demo-stack">
|
|
159
|
+
{#if v === 'range'}
|
|
160
|
+
<Slider
|
|
161
|
+
variant="range"
|
|
162
|
+
label="Price"
|
|
163
|
+
{low}
|
|
164
|
+
{high}
|
|
165
|
+
disabled={p.disabled}
|
|
166
|
+
class={p.forceClass}
|
|
167
|
+
onrangechange={(l, h) => { low = l; high = h; }}
|
|
168
|
+
/>
|
|
169
|
+
{:else}
|
|
170
|
+
<Slider label="Volume" {value} disabled={p.disabled} class={p.forceClass} onchange={(n) => (value = n)} />
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
{/snippet}
|
|
174
|
+
</VariantGroup>
|
|
175
|
+
{/each}
|
|
176
|
+
</ComponentEditorBase>
|
|
177
|
+
|
|
178
|
+
<style>
|
|
179
|
+
.slider-demo-stack {
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
gap: var(--space-12);
|
|
183
|
+
width: 100%;
|
|
184
|
+
max-width: 32rem;
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
|
|
3
|
-
import type { Token, TypeGroupConfig } from './scaffolding/types';
|
|
3
|
+
import type { Token, TypeGroupConfig, IntrinsicSpec } from './scaffolding/types';
|
|
4
4
|
|
|
5
5
|
export const component = 'tabbar';
|
|
6
6
|
|
|
@@ -81,14 +81,55 @@
|
|
|
81
81
|
[`--tabbar-${s}-text-line-height`, `${s} tab`] as const,
|
|
82
82
|
]),
|
|
83
83
|
]);
|
|
84
|
+
|
|
85
|
+
// The hover tint is off unless a project turns it on: the gate holds a
|
|
86
|
+
// transparent layer, a no-op, until it holds the tint itself.
|
|
87
|
+
export const intrinsics: IntrinsicSpec[] = [
|
|
88
|
+
{
|
|
89
|
+
key: 'hover-tint',
|
|
90
|
+
variants: ['default'],
|
|
91
|
+
variable: () => '--tabbar-hover-tint-enabled',
|
|
92
|
+
values: ['var(--color-transparent)', 'var(--tabbar-hover-tint)'],
|
|
93
|
+
default: { default: 'var(--color-transparent)' },
|
|
94
|
+
},
|
|
95
|
+
];
|
|
84
96
|
</script>
|
|
85
97
|
|
|
86
98
|
<script lang="ts">
|
|
87
99
|
import TabBar from '../../system/components/TabBar.svelte';
|
|
88
100
|
import VariantGroup from './scaffolding/VariantGroup.svelte';
|
|
89
101
|
import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
|
|
90
|
-
import { editorState } from '../core/store/editorStore';
|
|
102
|
+
import { editorState, setComponentAlias, clearComponentAlias } from '../core/store/editorStore';
|
|
91
103
|
import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
|
|
104
|
+
import Toggle from '../ui/Toggle.svelte';
|
|
105
|
+
import UIPaletteSelector from '../ui/UIPaletteSelector.svelte';
|
|
106
|
+
|
|
107
|
+
// One switch. Turning the tint on also stands each hover surface down to its
|
|
108
|
+
// own base, so hover is the tint alone rather than a swap wearing a wash.
|
|
109
|
+
// Written as aliases rather than gated in CSS: a component token's value is
|
|
110
|
+
// substituted at :root, so it can never name a per-variant private var.
|
|
111
|
+
// Off clears the overrides, returning each surface to its shipped default.
|
|
112
|
+
const HOVER_SWAPS: readonly (readonly [hover: string, base: string])[] = [
|
|
113
|
+
['--tabbar-hover-surface', '--tabbar-default-surface'],
|
|
114
|
+
];
|
|
115
|
+
const TINT_ON = 'var(--tabbar-hover-tint)';
|
|
116
|
+
const TINT_OFF = 'var(--color-transparent)';
|
|
117
|
+
|
|
118
|
+
let tintOn = $derived.by(() => {
|
|
119
|
+
const ref = $editorState.components.tabbar?.aliases['--tabbar-hover-tint-enabled'];
|
|
120
|
+
return (ref?.kind === 'literal' ? ref.value : TINT_OFF) === TINT_ON;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function setTintOn(checked: boolean) {
|
|
124
|
+
setComponentAlias('tabbar', '--tabbar-hover-tint-enabled', {
|
|
125
|
+
kind: 'literal',
|
|
126
|
+
value: checked ? TINT_ON : TINT_OFF,
|
|
127
|
+
});
|
|
128
|
+
for (const [hover, base] of HOVER_SWAPS) {
|
|
129
|
+
if (checked) setComponentAlias('tabbar', hover, { kind: 'token', name: base });
|
|
130
|
+
else clearComponentAlias('tabbar', hover);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
92
133
|
|
|
93
134
|
let selectedDemoTab = $state('overview');
|
|
94
135
|
const demoTabs = [
|
|
@@ -101,7 +142,12 @@
|
|
|
101
142
|
let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
|
|
102
143
|
|
|
103
144
|
let visibleStates = $derived(Object.fromEntries(
|
|
104
|
-
Object.entries(states).map(([name, list]) => [
|
|
145
|
+
Object.entries(states).map(([name, list]) => [
|
|
146
|
+
name,
|
|
147
|
+
withLinkedDisabled(list, linked.varSet).map((t) =>
|
|
148
|
+
tintOn && t.variable.endsWith('-hover-surface') ? { ...t, disabled: true } : t,
|
|
149
|
+
),
|
|
150
|
+
]),
|
|
105
151
|
) as Record<string, Token[]>);
|
|
106
152
|
</script>
|
|
107
153
|
|
|
@@ -114,6 +160,20 @@
|
|
|
114
160
|
{component}
|
|
115
161
|
|
|
116
162
|
>
|
|
163
|
+
{#snippet extraPropertyRows(stateName)}
|
|
164
|
+
{#if stateName === 'hover tab'}
|
|
165
|
+
<div class="property-row" data-token-variables="--tabbar-hover-tint-enabled">
|
|
166
|
+
<span class="property-label">tint layer</span>
|
|
167
|
+
<Toggle checked={tintOn} onchange={setTintOn} />
|
|
168
|
+
</div>
|
|
169
|
+
{#if tintOn}
|
|
170
|
+
<div class="property-row">
|
|
171
|
+
<span class="property-label">tint color</span>
|
|
172
|
+
<UIPaletteSelector variable="--tabbar-hover-tint" {component} showNone={false} />
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
{/if}
|
|
176
|
+
{/snippet}
|
|
117
177
|
{#snippet children({ activeState })}
|
|
118
178
|
{@const forceClass = activeState === 'hover tab' ? 'force-hover' : ''}
|
|
119
179
|
<TabBar tabs={demoTabs} selectedTab={selectedDemoTab} class={forceClass} on:tabChange={(e) => (selectedDemoTab = e.detail)} />
|
|
@@ -19,6 +19,7 @@ import MenuSelectEditor, { allTokens as menuSelectTokens } from './MenuSelectEdi
|
|
|
19
19
|
import NotificationEditor, { allTokens as notificationTokens } from './NotificationEditor.svelte';
|
|
20
20
|
import PanelEditor, { allTokens as panelTokens } from './PanelEditor.svelte';
|
|
21
21
|
import ProgressBarEditor, { allTokens as progressBarTokens } from './ProgressBarEditor.svelte';
|
|
22
|
+
import SliderEditor, { allTokens as sliderTokens } from './SliderEditor.svelte';
|
|
22
23
|
import RadioButtonEditor, { allTokens as radioButtonTokens } from './RadioButtonEditor.svelte';
|
|
23
24
|
import SectionDividerEditor, { allTokens as sectionDividerTokens, intrinsics as sectionDividerIntrinsics } from './SectionDividerEditor.svelte';
|
|
24
25
|
import SegmentedControlEditor, { allTokens as segmentedControlTokens } from './SegmentedControlEditor.svelte';
|
|
@@ -320,6 +321,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
|
|
|
320
321
|
schema: progressBarTokens,
|
|
321
322
|
origin: 'system',
|
|
322
323
|
},
|
|
324
|
+
slider: {
|
|
325
|
+
id: 'slider',
|
|
326
|
+
label: 'Slider',
|
|
327
|
+
icon: 'fas fa-sliders',
|
|
328
|
+
sourceFile: 'src/system/components/Slider.svelte',
|
|
329
|
+
editorComponent: SliderEditor,
|
|
330
|
+
schema: sliderTokens,
|
|
331
|
+
origin: 'system',
|
|
332
|
+
},
|
|
323
333
|
});
|
|
324
334
|
|
|
325
335
|
/** Mutable map of consumer-registered components, populated by `registerComponent()`. */
|
|
@@ -21,36 +21,59 @@ export type TokenKind =
|
|
|
21
21
|
| 'easing'
|
|
22
22
|
| 'text-color';
|
|
23
23
|
|
|
24
|
-
/** Suffix
|
|
25
|
-
editor's selector layout
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{ kind: '
|
|
40
|
-
{ kind: '
|
|
41
|
-
{ kind: '
|
|
42
|
-
{ kind: '
|
|
43
|
-
{ kind: '
|
|
44
|
-
|
|
45
|
-
{ kind: '
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ kind: '
|
|
49
|
-
{ kind: '
|
|
50
|
-
{ kind: '
|
|
51
|
-
{ kind: '
|
|
24
|
+
/** Suffix and prefix data mapped to kinds — the one source of truth for the
|
|
25
|
+
editor's selector layout, the `adjust` CLI, and `check-component`'s naming
|
|
26
|
+
rule, so the three cannot drift. `bin/check-component.mjs` reads the
|
|
27
|
+
`suffix:` arrays out of this file, which is why they are plain literals.
|
|
28
|
+
|
|
29
|
+
Order matters: `-text` must run before `-border`/`-surface`, and
|
|
30
|
+
`-accent-width` before `-accent`, because the first match wins. A variable
|
|
31
|
+
matching nothing falls through to `text-color` (a palette picker), but that
|
|
32
|
+
fall-through is a smell — `check-component` rejects an unrecognised suffix,
|
|
33
|
+
so add the name here rather than letting it drift. */
|
|
34
|
+
export const KIND_RULES: ReadonlyArray<{
|
|
35
|
+
kind: TokenKind;
|
|
36
|
+
suffix?: readonly string[];
|
|
37
|
+
prefix?: readonly string[];
|
|
38
|
+
}> = [
|
|
39
|
+
{ kind: 'font-family', suffix: ['-font-family'] },
|
|
40
|
+
{ kind: 'font-weight', suffix: ['-font-weight'] },
|
|
41
|
+
{ kind: 'font-size', suffix: ['-font-size', '-icon-size', '-thumb-size'] },
|
|
42
|
+
{ kind: 'line-height', suffix: ['-line-height'] },
|
|
43
|
+
{ kind: 'letter-spacing', suffix: ['-letter-spacing'] },
|
|
44
|
+
// Element-named colour roles: the property *is* the element it paints.
|
|
45
|
+
{ kind: 'text-color', suffix: ['-text', '-label', '-icon', '-title', '-body', '-eyebrow',
|
|
46
|
+
'-description', '-hint', '-error', '-placeholder', '-value'],
|
|
47
|
+
prefix: ['--text-'] },
|
|
48
|
+
{ kind: 'radius', suffix: ['-radius'], prefix: ['--radius-'] },
|
|
49
|
+
{ kind: 'divider-width', suffix: ['-divider-width', '-divider-thickness'] },
|
|
50
|
+
{ kind: 'divider-height', suffix: ['-divider-height', '-track-height'] },
|
|
51
|
+
{ kind: 'divider-inset', suffix: ['-divider-inset', '-inset'] },
|
|
52
|
+
{ kind: 'dot-size', suffix: ['-dot-size'] },
|
|
53
|
+
{ kind: 'blur', suffix: ['-blur'], prefix: ['--blur-'] },
|
|
54
|
+
{ kind: 'scale', suffix: ['-scale'], prefix: ['--scale-'] },
|
|
55
|
+
{ kind: 'shadow', suffix: ['-shadow'], prefix: ['--shadow-'] },
|
|
56
|
+
{ kind: 'padding', suffix: ['-padding', '-margin'] },
|
|
57
|
+
{ kind: 'gap', suffix: ['-gap'] },
|
|
58
|
+
{ kind: 'duration', suffix: ['-duration'], prefix: ['--duration-'] },
|
|
59
|
+
{ kind: 'easing', suffix: ['-easing'], prefix: ['--ease-'] },
|
|
60
|
+
{ kind: 'border-width', suffix: ['-border-width', '-accent-width', '-hairline-thickness', '-thickness'],
|
|
61
|
+
prefix: ['--border-width-'] },
|
|
62
|
+
{ kind: 'border', suffix: ['-border'], prefix: ['--border-'] },
|
|
63
|
+
// Fills. A tint is a wash over a surface, so it takes the surface picker: the
|
|
64
|
+
// full palette with an alpha, not just the tint stops it defaults to.
|
|
65
|
+
{ kind: 'surface', suffix: ['-surface', '-fill', '-divider', '-background', '-indicator',
|
|
66
|
+
'-thumb', '-accent', '-color', '-tint', '-opacity',
|
|
67
|
+
'-width', '-height', '-size'],
|
|
68
|
+
prefix: ['--surface-', '--tint', '--color-'] },
|
|
52
69
|
];
|
|
53
70
|
|
|
71
|
+
export const KIND_PATTERNS: ReadonlyArray<{ kind: TokenKind; matches: (v: string) => boolean }> =
|
|
72
|
+
KIND_RULES.map(({ kind, suffix = [], prefix = [] }) => ({
|
|
73
|
+
kind,
|
|
74
|
+
matches: (v: string) => suffix.some((s) => v.endsWith(s)) || prefix.some((p) => v.startsWith(p)),
|
|
75
|
+
}));
|
|
76
|
+
|
|
54
77
|
export function rawKind(variable: string): TokenKind {
|
|
55
78
|
for (const { kind, matches } of KIND_PATTERNS) {
|
|
56
79
|
if (matches(variable)) return kind;
|
|
@@ -227,6 +227,22 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
227
227
|
// Holds the picture inside the frame; without it a zoom spills out square.
|
|
228
228
|
clips: true,
|
|
229
229
|
},
|
|
230
|
+
...['single', 'range'].map((v) => ({
|
|
231
|
+
sel: `.slider.${v} .slider-track`,
|
|
232
|
+
fill: `var(--slider-${v}-track-surface)`,
|
|
233
|
+
stroke: `var(--slider-${v}-track-border)`,
|
|
234
|
+
radius: `var(--slider-${v}-track-radius, 0px)`,
|
|
235
|
+
})),
|
|
236
|
+
// The fill rides inside the track, so it is stroked by nothing and hatched
|
|
237
|
+
// in the track's ink.
|
|
238
|
+
...['single', 'range'].map((v) => ({
|
|
239
|
+
sel: `.slider.${v} .slider-fill`,
|
|
240
|
+
fill: `var(--slider-${v}-fill)`,
|
|
241
|
+
stroke: 'transparent',
|
|
242
|
+
hatch: `var(--slider-${v}-track-border)`,
|
|
243
|
+
radius: `var(--slider-${v}-track-radius, 0px)`,
|
|
244
|
+
positioned: true,
|
|
245
|
+
})),
|
|
230
246
|
{ sel: '.toggle .track', stem: 'toggle-track' },
|
|
231
247
|
{ sel: '.toggle.on .track', stem: 'toggle-on-track', radius: 'var(--toggle-track-radius, 0px)' },
|
|
232
248
|
|
|
@@ -20,6 +20,7 @@ import { store } from './editorCore';
|
|
|
20
20
|
import { quietGet, quietSet } from '../storage/storage';
|
|
21
21
|
import { isGradientSlot, makeDefaultGradients } from '../themes/slices/gradients';
|
|
22
22
|
import { seedShadowsFromDom } from '../themes/slices/shadows';
|
|
23
|
+
import { makeDefaultScrimTokens, makeDefaultTintTokens } from '../themes/slices/washes';
|
|
23
24
|
import { sanitizeHarmonyAxes } from '../palettes/colorHarmony';
|
|
24
25
|
import {
|
|
25
26
|
renameBackgroundPaletteKey,
|
|
@@ -77,6 +78,48 @@ function migrateGradients(state: EditorState): EditorState {
|
|
|
77
78
|
// calls `Object.entries(slice.config)` unconditionally, so backfill the
|
|
78
79
|
// required fields and drop any non-object slice before the state reaches the
|
|
79
80
|
// renderer. Spread preserves optional fields like `unlinked`.
|
|
81
|
+
// The washes slice was `overlays: { tokens, hoverTokens }` before the scrim and
|
|
82
|
+
// tint renames, and `hydrate` shallow-merges, so a persisted state from an
|
|
83
|
+
// older build replaces `washes` wholesale and arrives without `tints`.
|
|
84
|
+
// `washesToVars` iterates both lists unconditionally, so reshape here: carry the
|
|
85
|
+
// stops across under their new names, and rewrite the variable each one carries.
|
|
86
|
+
const WASH_VAR_RENAMES: Record<string, string> = {
|
|
87
|
+
'--overlay-low': '--scrim-low',
|
|
88
|
+
'--overlay': '--scrim',
|
|
89
|
+
'--overlay-high': '--scrim-high',
|
|
90
|
+
'--hover-low': '--tint-low',
|
|
91
|
+
'--hover': '--tint',
|
|
92
|
+
'--hover-high': '--tint-high',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type LegacyWashes = {
|
|
96
|
+
scrims?: unknown;
|
|
97
|
+
tints?: unknown;
|
|
98
|
+
hoverTokens?: unknown;
|
|
99
|
+
tokens?: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Carry a persisted stop list forward, or fall back to the shipped defaults. */
|
|
103
|
+
function washList(raw: unknown, fallback: () => EditorState['washes']['scrims']) {
|
|
104
|
+
if (!Array.isArray(raw) || raw.length === 0) return fallback();
|
|
105
|
+
const carried = raw
|
|
106
|
+
.filter((t): t is EditorState['washes']['scrims'][number] => !!t && typeof t === 'object' && 'variable' in t)
|
|
107
|
+
.map((t) => ({ ...t, variable: WASH_VAR_RENAMES[t.variable] ?? t.variable }));
|
|
108
|
+
return carried.length > 0 ? carried : fallback();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeWashes(state: EditorState): EditorState {
|
|
112
|
+
const washes = (state.washes ?? {}) as LegacyWashes;
|
|
113
|
+
const legacy = ((state as unknown as { overlays?: LegacyWashes }).overlays ?? {}) as LegacyWashes;
|
|
114
|
+
const next = { ...state } as EditorState & { overlays?: unknown };
|
|
115
|
+
next.washes = {
|
|
116
|
+
scrims: washList(washes.scrims ?? legacy.tokens, makeDefaultScrimTokens),
|
|
117
|
+
tints: washList(washes.tints ?? washes.hoverTokens ?? legacy.hoverTokens, makeDefaultTintTokens),
|
|
118
|
+
};
|
|
119
|
+
delete next.overlays;
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
|
|
80
123
|
export function normalizeComponents(state: EditorState): EditorState {
|
|
81
124
|
const raw = state.components;
|
|
82
125
|
if (!raw || typeof raw !== 'object') return { ...state, components: {} };
|
|
@@ -134,7 +177,7 @@ export function hydrate(): void {
|
|
|
134
177
|
store.set(
|
|
135
178
|
normalizeHarmonyAxes(
|
|
136
179
|
normalizeBaseAnchors(
|
|
137
|
-
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(migrateGradients(merged)))),
|
|
180
|
+
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(normalizeWashes(migrateGradients(merged))))),
|
|
138
181
|
),
|
|
139
182
|
),
|
|
140
183
|
);
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
editorState,
|
|
20
20
|
columnsToVars,
|
|
21
21
|
columnsEqualsDefault,
|
|
22
|
-
|
|
22
|
+
washesToVars,
|
|
23
23
|
shadowsToVars,
|
|
24
24
|
gradientsToVars,
|
|
25
25
|
componentsToVars,
|
|
@@ -30,7 +30,7 @@ export function deriveCssVars(state: EditorState): Record<string, string> {
|
|
|
30
30
|
if (!columnsEqualsDefault(state.columns)) {
|
|
31
31
|
Object.assign(out, columnsToVars(state.columns));
|
|
32
32
|
}
|
|
33
|
-
Object.assign(out,
|
|
33
|
+
Object.assign(out, washesToVars(state.washes));
|
|
34
34
|
if (state.shadows.tokens.length > 0) {
|
|
35
35
|
Object.assign(out, shadowsToVars(state.shadows));
|
|
36
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central editor state store — the single mutation funnel.
|
|
3
3
|
*
|
|
4
|
-
* All editor state (palettes, fonts, shadows,
|
|
4
|
+
* All editor state (palettes, fonts, shadows, washes, columns, ad-hoc CSS
|
|
5
5
|
* vars) lives in one `EditorState` tree. Every change must go through
|
|
6
6
|
* `mutate` (or a transaction). History is captured automatically; the
|
|
7
7
|
* renderer subscribes and writes derived CSS vars to :root via `cssVarSync`
|
|
@@ -62,10 +62,10 @@ import {
|
|
|
62
62
|
loadColumnsFromVars,
|
|
63
63
|
} from '../themes/slices/columns';
|
|
64
64
|
import {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} from '../themes/slices/
|
|
65
|
+
loadWashesFromVars,
|
|
66
|
+
makeDefaultWashesState,
|
|
67
|
+
washesToVars,
|
|
68
|
+
} from '../themes/slices/washes';
|
|
69
69
|
import {
|
|
70
70
|
loadShadowsFromVars,
|
|
71
71
|
shadowsToVars,
|
|
@@ -99,7 +99,7 @@ function emptyState(): EditorState {
|
|
|
99
99
|
tokens: [],
|
|
100
100
|
overrides: {},
|
|
101
101
|
},
|
|
102
|
-
|
|
102
|
+
washes: makeDefaultWashesState(),
|
|
103
103
|
columns: { ...DEFAULT_COLUMNS },
|
|
104
104
|
components: {},
|
|
105
105
|
gradients: { tokens: makeDefaultGradients() },
|
|
@@ -150,15 +150,15 @@ export {
|
|
|
150
150
|
} from '../themes/slices/columns';
|
|
151
151
|
|
|
152
152
|
export {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} from '../themes/slices/
|
|
153
|
+
washesToVars,
|
|
154
|
+
WASH_VAR_NAMES,
|
|
155
|
+
applyWashVarsToState,
|
|
156
|
+
makeDefaultWashesState,
|
|
157
|
+
makeDefaultScrimTokens,
|
|
158
|
+
makeDefaultTintTokens,
|
|
159
|
+
washTokenToCss,
|
|
160
|
+
parseWashCss,
|
|
161
|
+
} from '../themes/slices/washes';
|
|
162
162
|
|
|
163
163
|
export {
|
|
164
164
|
shadowsToVars,
|
|
@@ -364,7 +364,7 @@ type DomainLoader = (next: EditorState, rawVars: Record<string, string>) => void
|
|
|
364
364
|
|
|
365
365
|
const domainLoaders: Record<string, DomainLoader> = {
|
|
366
366
|
columns: loadColumnsFromVars,
|
|
367
|
-
|
|
367
|
+
washes: loadWashesFromVars,
|
|
368
368
|
shadows: loadShadowsFromVars,
|
|
369
369
|
components: loadComponentsFromVars,
|
|
370
370
|
};
|
|
@@ -428,7 +428,7 @@ function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' |
|
|
|
428
428
|
if (!columnsEqualsDefault(state.columns)) {
|
|
429
429
|
Object.assign(cssVariables, columnsToVars(state.columns));
|
|
430
430
|
}
|
|
431
|
-
Object.assign(cssVariables,
|
|
431
|
+
Object.assign(cssVariables, washesToVars(state.washes));
|
|
432
432
|
if (state.shadows.tokens.length > 0) {
|
|
433
433
|
Object.assign(cssVariables, shadowsToVars(state.shadows));
|
|
434
434
|
}
|
|
@@ -450,7 +450,7 @@ function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' |
|
|
|
450
450
|
|
|
451
451
|
/**
|
|
452
452
|
* Serialize current state for saving. Domains with their own typed state
|
|
453
|
-
* (columns,
|
|
453
|
+
* (columns, washes, shadows) fold derived vars into `cssVariables` only
|
|
454
454
|
* when they diverge from defaults; the catch-all `cssVars` bag carries
|
|
455
455
|
* everything not yet migrated to a typed domain.
|
|
456
456
|
*
|
|
@@ -23,9 +23,10 @@ export interface ShadowOverrideFlags {
|
|
|
23
23
|
distance: boolean; blur: boolean; size: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* `color-mix(in srgb, var(<alias>) <opacity%>, transparent)`.
|
|
28
|
-
|
|
26
|
+
/** A wash: an aliased color token + an opacity. Emits as
|
|
27
|
+
* `color-mix(in srgb, var(<alias>) <opacity%>, transparent)`. Scrims dim what
|
|
28
|
+
* is behind them; tints shade the surface they sit on. */
|
|
29
|
+
export interface WashToken {
|
|
29
30
|
variable: string;
|
|
30
31
|
label: string;
|
|
31
32
|
alias: string;
|
|
@@ -111,9 +112,9 @@ export interface EditorState {
|
|
|
111
112
|
tokens: ShadowToken[];
|
|
112
113
|
overrides: Record<string, ShadowOverrideFlags>;
|
|
113
114
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
washes: {
|
|
116
|
+
scrims: WashToken[];
|
|
117
|
+
tints: WashToken[];
|
|
117
118
|
};
|
|
118
119
|
columns: ColumnsState;
|
|
119
120
|
components: Record<string, ComponentSlice>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate suffix, state word to `-enabled` (2026-09-01).
|
|
5
|
+
*
|
|
6
|
+
* An optional interaction is switched by a gate variable. Those gates were named
|
|
7
|
+
* with a state word at the end (`--card-hover-border-active`,
|
|
8
|
+
* `--image-zoom-hover`), which reads as state-after-property and fails
|
|
9
|
+
* `check-component`. A gate is not a state: it says whether the interaction is
|
|
10
|
+
* on at all, so it takes `-enabled`.
|
|
11
|
+
*
|
|
12
|
+
* Values are unchanged; a gate still holds either its off value or its on value.
|
|
13
|
+
*/
|
|
14
|
+
const RENAMED: Record<string, string> = {
|
|
15
|
+
'--card-hover-border-active': '--card-hover-border-enabled',
|
|
16
|
+
'--card-hover-shadow-active': '--card-hover-shadow-enabled',
|
|
17
|
+
'--image-zoom-hover': '--image-zoom-enabled',
|
|
18
|
+
'--image-grow-hover': '--image-grow-enabled',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const componentMigration_2026_09_01_gateSuffixEnabled: Migration = {
|
|
22
|
+
id: '2026-09-01-gate-suffix-enabled',
|
|
23
|
+
fromVersion: 24,
|
|
24
|
+
toVersion: 25,
|
|
25
|
+
appliesTo: 'component-config',
|
|
26
|
+
apply(rawVars) {
|
|
27
|
+
const out: Record<string, string> = {};
|
|
28
|
+
for (const [key, value] of Object.entries(rawVars)) out[RENAMED[key] ?? key] = value;
|
|
29
|
+
return out;
|
|
30
|
+
},
|
|
31
|
+
};
|