@motion-proto/live-tokens 0.24.1 → 0.25.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/SKILL.md +38 -24
- package/bin/check-component.mjs +52 -4
- package/dist-plugin/index.cjs +7 -0
- package/dist-plugin/index.js +7 -0
- package/package.json +1 -1
- package/src/editor/component-editor/CalloutEditor.svelte +1 -1
- package/src/editor/component-editor/CardEditor.svelte +1 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -1
- package/src/editor/component-editor/DialogEditor.svelte +1 -1
- package/src/editor/component-editor/PanelEditor.svelte +81 -0
- package/src/editor/component-editor/ProgressBarEditor.svelte +1 -1
- package/src/editor/component-editor/RadioButtonEditor.svelte +1 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +2 -2
- package/src/editor/component-editor/SideNavigationEditor.svelte +18 -15
- package/src/editor/component-editor/TabBarEditor.svelte +1 -1
- package/src/editor/component-editor/TableEditor.svelte +8 -17
- package/src/editor/component-editor/TooltipEditor.svelte +1 -1
- package/src/editor/component-editor/index.ts +8 -1
- package/src/editor/component-editor/registry.ts +11 -0
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +120 -17
- package/src/editor/component-editor/scaffolding/types.ts +4 -0
- package/src/editor/overlay/LiveEditorOverlay.svelte +79 -3
- package/src/editor/styles/ui-editor.css +8 -2
- package/src/system/assets/github-mark-white.svg +1 -0
- package/src/system/assets/npm-mark-white.svg +1 -0
- package/src/system/assets/offering.webp +0 -0
- package/src/system/components/Button.svelte +12 -12
- package/src/system/components/CodeSnippet.svelte +4 -1
- package/src/system/components/FloatingTokenTags.css +1 -1
- package/src/system/components/Panel.svelte +44 -0
- package/src/system/components/SectionDivider.svelte +10 -10
- package/src/system/components/SideNavigation.svelte +13 -0
- package/src/system/styles/CONVENTIONS.md +13 -6
|
@@ -25,21 +25,24 @@ For pattern reference, read any shipped component's source directly from the con
|
|
|
25
25
|
|
|
26
26
|
1. **Runtime file** — `src/system/components/MyWidget.svelte`. Declare every editable slot as a CSS custom property inside `:global(:root)`, defaulting to a theme token (never a raw value). The plugin parses `:global(:root)` to seed `component-configs/<id>/default.json`; variables declared anywhere else can't be edited.
|
|
27
27
|
2. **Editor file** — `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block, declare `const component = 'mywidget'`, build a `states: Record<string, Token[]>` for each VariantGroup, and export the flat union as `allTokens: Token[]`. Components with linked siblings also build a `linkableContexts: Map<string, string>` (see the linked-siblings extension below). Components with structural/display controls that aren't token values (alignment, element visibility, layout position) also export an `intrinsics: IntrinsicSpec[]` (see the intrinsics extension below). In the runtime `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.
|
|
28
|
-
3. **Register** — in `src/main.ts`
|
|
28
|
+
3. **Register** — pass the component to `bootLiveTokens` in `src/main.ts`. This is the standard boot the scaffold generates and the README documents; `bootLiveTokens` calls `registerComponent` internally at the right point — after its editor init hooks (`cssVarSync.init`, `editorStore.init`), before it seeds configs and mounts the app:
|
|
29
29
|
```ts
|
|
30
|
-
import {
|
|
30
|
+
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
31
|
+
import App from './App.svelte';
|
|
31
32
|
import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
bootLiveTokens(App, '#app', {
|
|
35
|
+
components: [{
|
|
36
|
+
id: 'mywidget',
|
|
37
|
+
label: 'My Widget',
|
|
38
|
+
icon: 'fas fa-magic',
|
|
39
|
+
sourceFile: 'src/system/components/MyWidget.svelte',
|
|
40
|
+
editorComponent: MyWidgetEditor,
|
|
41
|
+
schema: myWidgetTokens,
|
|
42
|
+
}],
|
|
40
43
|
});
|
|
41
44
|
```
|
|
42
|
-
The schema side-effect happens inside `registerComponent
|
|
45
|
+
The schema side-effect happens inside `registerComponent` (which `bootLiveTokens` calls for you), so you don't call `registerComponentSchema` separately. **Do not place a standalone `registerComponent(...)` *before* `bootLiveTokens`** — that registers before the editor's init hooks run, which is the wrong window and can leave editor changes disconnected from the live page. Only call `registerComponent` directly if your app mounts manually (no `bootLiveTokens`), in which case call it before `mount(App, ...)`.
|
|
43
46
|
4. **Tell the picker** — open `.claude/skills/live-tokens-pick-component/SKILL.md` and add your new component to the **Catalogue** line under the family it belongs to (Action / Input / Selection / Containers / Messaging / Display). If it's confusable with an existing component (a second selection control, a competing container), add a row to that family's decision table explaining the use-case it owns. Without this step, the component exists but [[live-tokens-pick-component]] can't recommend it when a user asks "which component should I use?" — the same rule applies whether the component is first-party (update the picker shipped in this package) or consumer-authored (update the local copy at `.claude/skills/live-tokens-pick-component/SKILL.md` that `setup-claude` placed in your project).
|
|
44
47
|
5. **Verify** — open `/components` and run the verification checklist at the bottom of this file.
|
|
45
48
|
|
|
@@ -107,7 +110,17 @@ The authoritative recognised list lives in `bin/check-component.mjs` (`KNOWN_SUF
|
|
|
107
110
|
- **Defaults reference theme tokens, never raw values.** `var(--surface-primary)` ✓ — `#6a4ce8` ✗.
|
|
108
111
|
- **No abbreviations.** `bg` → `surface`; `fg` → `text`; component ids are never abbreviated.
|
|
109
112
|
- **Text aliases.** Neutral scale is `--text-primary` / `--text-secondary` / `--text-tertiary` / `--text-muted` / `--text-disabled`. Family-tinted is `--text-primary-color`, `--text-accent`, `--text-success`. There is no `--text-neutral`.
|
|
110
|
-
- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` ✓ — bare `groupKey: 'font-family'` silently merges them into one link tree ✗. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears.
|
|
113
|
+
- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` ✓ — bare `groupKey: 'font-family'` silently merges them into one link tree ✗. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears. The same trap applies to type-group **colors** (two slots ending in `-text` collapsing to one `text` key). Let the helpers handle both: see "Deriving groupKeys" below.
|
|
114
|
+
- **Let the type-group helpers derive slot-scoped keys; never rely on the bare last-dash default.** When you build typography tokens with `buildTypeGroupColorTokens` / `buildTypeGroupTokens` / `buildTypeGroupFontTokens`, **pass `{ component, variants }`** so each slot gets a distinct, structural `groupKey`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// variants = the variant/state segment strings as they appear in the variable name
|
|
118
|
+
const VARIANTS = ['default', 'hover'] as const;
|
|
119
|
+
...buildTypeGroupColorTokens(typeGroups, { component, variants: [...VARIANTS] }),
|
|
120
|
+
...buildTypeGroupFontTokens(typeGroups, { component, variants: [...VARIANTS] }),
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The helper strips the `--<component>-` prefix and those segments, keeping the rest: `--mywidget-header-default-text` → `header-text`, `--mywidget-header-default-text-font-family` → `header-text-font-family`. Two parts ending in the same word stay distinct; one slot across variants collapses to one key. To override a single derived key, set `colorGroupKey` on that type-group config — it wins and is never recomputed, so your fix survives. There is no name-based fallback: a bare `buildTypeGroupColorTokens` call emits un-grouped (solo) colors rather than guessing, and a bare *font* helper across multiple slots is a `check-component` warning (its default `font-family`/… keys would merge the slots' fonts).
|
|
111
124
|
|
|
112
125
|
### Linked siblings
|
|
113
126
|
|
|
@@ -158,7 +171,7 @@ import {
|
|
|
158
171
|
import type { Token } from '@motion-proto/live-tokens/component-editor';
|
|
159
172
|
```
|
|
160
173
|
|
|
161
|
-
That covers everything the worked examples use. Additional primitives (`LinkedBlock`, `TypeEditor`, `TokenLayout`, `buildTypeGroupTokens`, more types) are exported from the same paths for advanced cases.
|
|
174
|
+
That covers everything the worked examples use. Additional primitives (`LinkedBlock`, `TypeEditor`, `TokenLayout`, `buildTypeGroupTokens`, `buildTypeGroupColorTokens`, `buildTypeGroupFontTokens`, `buildTypeGroupShareableContexts`, the `TypeGroupConfig` type, more types) are exported from the same paths for advanced cases.
|
|
162
175
|
|
|
163
176
|
**Never deep-import `node_modules/@motion-proto/live-tokens/src/...`.** Reading those files for pattern reference is fine; importing them at runtime is not. If you need something not exported, file an issue rather than reaching in.
|
|
164
177
|
|
|
@@ -376,22 +389,23 @@ What to notice:
|
|
|
376
389
|
### Register: `src/main.ts`
|
|
377
390
|
|
|
378
391
|
```ts
|
|
379
|
-
import {
|
|
392
|
+
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
393
|
+
import App from './App.svelte';
|
|
380
394
|
import ToggleEditor, { allTokens as toggleTokens } from './system/components/ToggleEditor.svelte';
|
|
381
395
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
396
|
+
bootLiveTokens(App, '#app', {
|
|
397
|
+
components: [{
|
|
398
|
+
id: 'mytoggle', // unique id; don't reuse 'toggle'
|
|
399
|
+
label: 'My Toggle',
|
|
400
|
+
icon: 'fas fa-toggle-on',
|
|
401
|
+
sourceFile: 'src/system/components/Toggle.svelte',
|
|
402
|
+
editorComponent: ToggleEditor,
|
|
403
|
+
schema: toggleTokens,
|
|
404
|
+
}],
|
|
389
405
|
});
|
|
390
|
-
|
|
391
|
-
// then mount(App, ...)
|
|
392
406
|
```
|
|
393
407
|
|
|
394
|
-
If
|
|
408
|
+
If your app mounts manually instead of via `bootLiveTokens`, call `registerComponent({ id: 'mytoggle', ... })` yourself before `mount(App, ...)`. If you reuse `id: 'toggle'`, the consumer's component wins over the built-in (with a console warning). The collision rule protects you, but for a fresh component pick an id that doesn't collide.
|
|
395
409
|
|
|
396
410
|
## Extension: linked siblings
|
|
397
411
|
|
|
@@ -530,7 +544,7 @@ npx live-tokens check-component <id>
|
|
|
530
544
|
# or: npx @motion-proto/live-tokens check-component <id>
|
|
531
545
|
```
|
|
532
546
|
|
|
533
|
-
It enforces the file layout, the `:global(:root)` block, token-suffix vocabulary, state-before-property rule, theme-token defaults (no raw colour literals), public-imports rule, and the `registerComponent({ id })` call. Exit code 0 means the static contract is met.
|
|
547
|
+
It enforces the file layout, the `:global(:root)` block, token-suffix vocabulary, state-before-property rule, theme-token defaults (no raw colour literals), public-imports rule, and that the id is registered — via either `bootLiveTokens({ components: [{ id }] })` or a direct `registerComponent({ id })` call. It also *warns* (non-fatal) when a type-group font helper is called bare across multiple slots, which would merge their fonts into one link tree. Exit code 0 means the static contract is met; resolve warnings before shipping.
|
|
534
548
|
|
|
535
549
|
**Then run the registry contract test.** If you're authoring inside the package itself, `src/editor/component-editor/registryContract.test.ts` runs `describe.each(getComponentRegistryEntries())` and verifies, per component:
|
|
536
550
|
|
package/bin/check-component.mjs
CHANGED
|
@@ -88,6 +88,28 @@ function detectStateAfterProperty(token) {
|
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// True if `source` calls `fnName(` at least once with a single argument (no
|
|
92
|
+
// top-level comma before the matching close paren). Brackets/braces are balanced
|
|
93
|
+
// so commas inside nested objects/arrays/calls don't count.
|
|
94
|
+
function hasBareCall(source, fnName) {
|
|
95
|
+
const needle = fnName + '(';
|
|
96
|
+
let idx = 0;
|
|
97
|
+
while ((idx = source.indexOf(needle, idx)) !== -1) {
|
|
98
|
+
let i = idx + needle.length;
|
|
99
|
+
let depth = 1;
|
|
100
|
+
let topComma = false;
|
|
101
|
+
for (; i < source.length && depth > 0; i++) {
|
|
102
|
+
const c = source[i];
|
|
103
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
104
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
105
|
+
else if (c === ',' && depth === 1) topComma = true;
|
|
106
|
+
}
|
|
107
|
+
if (!topComma) return true;
|
|
108
|
+
idx = i;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
function findFilesRecursive(dir, exts) {
|
|
92
114
|
if (!existsSync(dir)) return [];
|
|
93
115
|
const out = [];
|
|
@@ -184,6 +206,27 @@ export function checkComponent(id, root = process.cwd()) {
|
|
|
184
206
|
errors.push(`${relative(root, editorPath)}: missing 'export const allTokens'`);
|
|
185
207
|
}
|
|
186
208
|
|
|
209
|
+
// Editor: phantom-link guard. The font type-group helpers fall back to bare
|
|
210
|
+
// `font-family`/`font-size`/… keys when called with a single argument (no
|
|
211
|
+
// derivation). Across more than one slot that silently links every slot's fonts
|
|
212
|
+
// into one tree. Passing `{ component, variants }` (a second arg) opts into
|
|
213
|
+
// distinct, structural keys and suppresses the check, so this only fires on the
|
|
214
|
+
// silent inference path. (The color helper no longer infers — a bare call there
|
|
215
|
+
// emits solo, un-grouped colors, which can't phantom-link.)
|
|
216
|
+
const colorPatterns = new Set();
|
|
217
|
+
for (const m of editor.matchAll(/colorVariable\s*:\s*[`'"]([^`'"]+)[`'"]/g)) {
|
|
218
|
+
colorPatterns.add(m[1].replace(/\$\{[^}]*\}/g, '*'));
|
|
219
|
+
}
|
|
220
|
+
const slots = colorPatterns.size;
|
|
221
|
+
const fontBare =
|
|
222
|
+
hasBareCall(editor, 'buildTypeGroupFontTokens') || hasBareCall(editor, 'buildTypeGroupTokens');
|
|
223
|
+
if (slots > 1 && fontBare) {
|
|
224
|
+
warnings.push(
|
|
225
|
+
`${relative(root, editorPath)}: a type-group font helper is called across ${slots} slots without a derivation; ` +
|
|
226
|
+
`its bare font-family/font-size/… keys would phantom-link every slot's fonts. Pass { component, variants } to buildTypeGroupTokens/buildTypeGroupFontTokens.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
187
230
|
// Imports across runtime + editor: reject deep imports into the package.
|
|
188
231
|
for (const [path, source] of [[runtimePath, runtime], [editorPath, editor]]) {
|
|
189
232
|
for (const imp of extractImports(source)) {
|
|
@@ -195,13 +238,18 @@ export function checkComponent(id, root = process.cwd()) {
|
|
|
195
238
|
}
|
|
196
239
|
}
|
|
197
240
|
|
|
198
|
-
// Registration: registerComponent({ id
|
|
241
|
+
// Registration: either a direct registerComponent({ id }) call or the id
|
|
242
|
+
// passed through bootLiveTokens({ components: [{ id }] }) — the standard
|
|
243
|
+
// scaffold boot. Accept both, somewhere under src/.
|
|
199
244
|
const srcFiles = findFilesRecursive(join(root, 'src'), ['.ts', '.js', '.svelte', '.mjs']);
|
|
200
|
-
const
|
|
245
|
+
const idLiteral = `id\\s*:\\s*['"]${id}['"]`;
|
|
246
|
+
const directPattern = new RegExp(`registerComponent\\s*\\(\\s*\\{[^}]*${idLiteral}`, 's');
|
|
247
|
+
const bootPattern = new RegExp(`bootLiveTokens\\s*\\([\\s\\S]*?components\\s*:\\s*\\[[\\s\\S]*?${idLiteral}`, 's');
|
|
201
248
|
let registrationFile = null;
|
|
202
249
|
for (const file of srcFiles) {
|
|
203
250
|
try {
|
|
204
|
-
|
|
251
|
+
const src = readFileSync(file, 'utf8');
|
|
252
|
+
if (directPattern.test(src) || bootPattern.test(src)) {
|
|
205
253
|
registrationFile = file;
|
|
206
254
|
break;
|
|
207
255
|
}
|
|
@@ -210,7 +258,7 @@ export function checkComponent(id, root = process.cwd()) {
|
|
|
210
258
|
}
|
|
211
259
|
}
|
|
212
260
|
if (!registrationFile) {
|
|
213
|
-
errors.push(`registerComponent({ id: '${id}', ... })
|
|
261
|
+
errors.push(`no registration for '${id}' under src/ — expected registerComponent({ id: '${id}', ... }) or bootLiveTokens({ components: [{ id: '${id}', ... }] })`);
|
|
214
262
|
} else {
|
|
215
263
|
// Check the registration file's imports too.
|
|
216
264
|
const regSource = readFileSync(registrationFile, 'utf8');
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -1114,6 +1114,13 @@ ${lines.join("\n")}
|
|
|
1114
1114
|
} catch {
|
|
1115
1115
|
}
|
|
1116
1116
|
}
|
|
1117
|
+
if (existingAliases) {
|
|
1118
|
+
for (const [token, val] of Object.entries(existingAliases)) {
|
|
1119
|
+
if (val !== null && typeof val === "object" && !(token in aliases)) {
|
|
1120
|
+
aliases[token] = val;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1117
1124
|
const aliasesUnchanged = existingAliases !== void 0 && JSON.stringify(existingAliases) === JSON.stringify(aliases);
|
|
1118
1125
|
const defaultConfig = {
|
|
1119
1126
|
name: "default",
|
package/dist-plugin/index.js
CHANGED
|
@@ -854,6 +854,13 @@ ${lines.join("\n")}
|
|
|
854
854
|
} catch {
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
|
+
if (existingAliases) {
|
|
858
|
+
for (const [token, val] of Object.entries(existingAliases)) {
|
|
859
|
+
if (val !== null && typeof val === "object" && !(token in aliases)) {
|
|
860
|
+
aliases[token] = val;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
857
864
|
const aliasesUnchanged = existingAliases !== void 0 && JSON.stringify(existingAliases) === JSON.stringify(aliases);
|
|
858
865
|
const defaultConfig = {
|
|
859
866
|
name: "default",
|
package/package.json
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
export const allTokens: Token[] = variants.flatMap((v) => [
|
|
56
56
|
...variantTokens(v),
|
|
57
|
-
...buildTypeGroupColorTokens(variantTypeGroups(v)),
|
|
57
|
+
...buildTypeGroupColorTokens(variantTypeGroups(v), { component, variants: [...variants] }),
|
|
58
58
|
...variantTypeGroupTokens(v),
|
|
59
59
|
]);
|
|
60
60
|
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
|
|
94
94
|
export const allTokens: Token[] = [
|
|
95
95
|
...VARIANTS.flatMap((v) => Object.values(variantStates(v)).flat()),
|
|
96
|
-
...VARIANTS.flatMap((v) => buildTypeGroupColorTokens(variantTypeGroups(v))),
|
|
96
|
+
...VARIANTS.flatMap((v) => buildTypeGroupColorTokens(variantTypeGroups(v), { component, variants: [...VARIANTS, ...HEADER_STATES] })),
|
|
97
97
|
...headerTypeGroupTokens,
|
|
98
98
|
];
|
|
99
99
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Token } from './scaffolding/types';
|
|
3
|
+
export const component = 'panel';
|
|
4
|
+
|
|
5
|
+
// Frame + the non-surface stage props render as ordinary token rows. The
|
|
6
|
+
// stage surface is a structured (tokenized) gradient edited via the
|
|
7
|
+
// GradientEditor below, so it lives outside the row list — `kind: 'gradient'`
|
|
8
|
+
// marks it as a structured payload and folds it back into `allTokens` for the
|
|
9
|
+
// schema without rendering a flat color row for it.
|
|
10
|
+
const states: Record<string, Token[]> = {
|
|
11
|
+
default: [
|
|
12
|
+
{ label: 'border color', variable: '--panel-frame-border', element: 'frame' },
|
|
13
|
+
{ label: 'border width', variable: '--panel-frame-border-width', element: 'frame' },
|
|
14
|
+
{ label: 'corner radius', variable: '--panel-frame-radius', element: 'frame' },
|
|
15
|
+
|
|
16
|
+
{ label: 'vertical padding', variable: '--panel-stage-padding', splittable: false, element: 'stage' },
|
|
17
|
+
{ label: 'side padding', variable: '--panel-stage-inline-padding', splittable: false, element: 'stage' },
|
|
18
|
+
{ label: 'content gap', variable: '--panel-stage-gap', element: 'stage' },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
const surfaceToken: Token = {
|
|
22
|
+
label: 'surface',
|
|
23
|
+
variable: '--panel-stage-surface',
|
|
24
|
+
kind: 'gradient',
|
|
25
|
+
family: 'neutral',
|
|
26
|
+
element: 'stage',
|
|
27
|
+
};
|
|
28
|
+
export const allTokens: Token[] = [...Object.values(states).flat(), surfaceToken];
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script lang="ts">
|
|
32
|
+
import Panel from '../../system/components/Panel.svelte';
|
|
33
|
+
import VariantGroup from './scaffolding/VariantGroup.svelte';
|
|
34
|
+
import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
|
|
35
|
+
import GradientEditor from '../ui/GradientEditor.svelte';
|
|
36
|
+
import { componentGradientSource } from '../core/store/gradientSource';
|
|
37
|
+
|
|
38
|
+
const surfaceGradient = componentGradientSource(component, surfaceToken.variable);
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<ComponentEditorBase
|
|
42
|
+
{component}
|
|
43
|
+
title="Panel"
|
|
44
|
+
description="Framed container with a tokenized border and gradient surface."
|
|
45
|
+
tokens={allTokens}
|
|
46
|
+
>
|
|
47
|
+
<VariantGroup name="panel" title="Panel" {states} {component} elementOrder={['frame', 'stage']}>
|
|
48
|
+
{#snippet compositeControls(_stateName)}
|
|
49
|
+
<div class="panel-surface-section">
|
|
50
|
+
<GradientEditor
|
|
51
|
+
sectionLabel="Surface"
|
|
52
|
+
source={surfaceGradient}
|
|
53
|
+
stopIdPrefix="panel-surface"
|
|
54
|
+
familyFilter="neutral"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
{/snippet}
|
|
58
|
+
{#snippet children()}
|
|
59
|
+
<div class="panel-demo">
|
|
60
|
+
<Panel minHeight="6rem">
|
|
61
|
+
<span class="demo-chip">Stage content</span>
|
|
62
|
+
<span class="demo-chip">Stage content</span>
|
|
63
|
+
</Panel>
|
|
64
|
+
</div>
|
|
65
|
+
{/snippet}
|
|
66
|
+
</VariantGroup>
|
|
67
|
+
</ComponentEditorBase>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.panel-demo {
|
|
71
|
+
width: 100%;
|
|
72
|
+
max-width: 34rem;
|
|
73
|
+
}
|
|
74
|
+
.demo-chip {
|
|
75
|
+
padding: var(--ui-space-4) var(--ui-space-8);
|
|
76
|
+
border: 1px solid var(--ui-border);
|
|
77
|
+
border-radius: var(--ui-radius-sm);
|
|
78
|
+
font-size: var(--ui-font-size-sm);
|
|
79
|
+
color: var(--ui-text-secondary);
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
variants: ['lg', 'md', 'sm'],
|
|
166
166
|
variable: (v) => `--sectiondivider-${v}-align`,
|
|
167
167
|
values: ['start', 'center'],
|
|
168
|
-
default: { lg: 'start', md: 'start', sm: '
|
|
168
|
+
default: { lg: 'start', md: 'start', sm: 'center' },
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
key: 'eyebrow-display',
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
variants: ['lg', 'md', 'sm'],
|
|
194
194
|
variable: (v) => `--sectiondivider-${v}-hairline`,
|
|
195
195
|
values: ['none', ...HAIRLINE_POSITIONS],
|
|
196
|
-
default: { lg: 'below-description', md: 'below-label', sm: '
|
|
196
|
+
default: { lg: 'below-description', md: 'below-label', sm: 'none' },
|
|
197
197
|
// 'above-description' renders identically to 'below-label'; the position
|
|
198
198
|
// dropdown omits it, so coerce on read to keep the control's value valid.
|
|
199
199
|
normalize: (raw) => (raw === 'above-description' ? 'below-label' : raw),
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
|
|
41
41
|
// --- Title --------------------------------------------------------------
|
|
42
42
|
// Title is a flex card containing the label box + toggle box. Per-state
|
|
43
|
-
// tokens drive the card chrome (surface, border, padding); stateless
|
|
44
|
-
//
|
|
45
|
-
// default/hover/active.
|
|
43
|
+
// tokens drive the card chrome (surface, border, padding); the stateless
|
|
44
|
+
// bar + label-box structure lives in `titleBlockTokens` since it doesn't
|
|
45
|
+
// vary across default/hover/active.
|
|
46
46
|
function titleStateTokens(s: StatefulState): Token[] {
|
|
47
47
|
return [
|
|
48
48
|
{ label: 'surface color', groupKey: 'title-surface', variable: `--sidenavigation-title-${s}-surface` },
|
|
@@ -51,9 +51,16 @@
|
|
|
51
51
|
{ label: 'padding', canBeLinked: true, groupKey: 'title-padding', variable: `--sidenavigation-title-${s}-padding` },
|
|
52
52
|
];
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
// Stateless title-bar structure ("Title Block" tab): the bar's own geometry
|
|
55
|
+
// plus the inner label box. Separate from the stateful Title tab because
|
|
56
|
+
// these don't vary by interaction state. Label-box rows are prefixed "label"
|
|
57
|
+
// to disambiguate from the bar's own corner radius.
|
|
58
|
+
const titleBlockTokens: Token[] = [
|
|
55
59
|
{ label: 'gap', canBeLinked: true, groupKey: 'title-gap', variable: '--sidenavigation-title-gap' },
|
|
56
60
|
{ label: 'corner radius', canBeLinked: true, groupKey: 'title-radius', variable: '--sidenavigation-title-radius' },
|
|
61
|
+
{ label: 'label surface color', groupKey: 'title-label-surface', variable: '--sidenavigation-title-label-surface' },
|
|
62
|
+
{ label: 'label corner radius', canBeLinked: true, groupKey: 'title-label-radius', variable: '--sidenavigation-title-label-radius' },
|
|
63
|
+
{ label: 'label padding', canBeLinked: true, groupKey: 'title-label-padding', variable: '--sidenavigation-title-label-padding' },
|
|
57
64
|
];
|
|
58
65
|
function titleStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
|
|
59
66
|
return [{
|
|
@@ -65,14 +72,6 @@
|
|
|
65
72
|
lineHeightVariable: `--sidenavigation-title-${s}-label-line-height`,
|
|
66
73
|
}];
|
|
67
74
|
}
|
|
68
|
-
// Title label — structural inner box (stateless, like Panel). Sits inside
|
|
69
|
-
// the title bar so the header reads as: outer bar → [label box] [toggle box].
|
|
70
|
-
const titleLabelTokens: Token[] = [
|
|
71
|
-
{ label: 'surface color', groupKey: 'title-label-surface', variable: '--sidenavigation-title-label-surface' },
|
|
72
|
-
{ label: 'corner radius', canBeLinked: true, groupKey: 'title-label-radius', variable: '--sidenavigation-title-label-radius' },
|
|
73
|
-
{ label: 'padding', canBeLinked: true, groupKey: 'title-label-padding', variable: '--sidenavigation-title-label-padding' },
|
|
74
|
-
];
|
|
75
|
-
|
|
76
75
|
const titleTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
|
|
77
76
|
{ label: 'font family', canBeLinked: true, groupKey: 'title-label-font-family', variable: `--sidenavigation-title-${s}-label-font-family` },
|
|
78
77
|
{ label: 'font size', canBeLinked: true, groupKey: 'title-label-font-size', variable: `--sidenavigation-title-${s}-label-font-size` },
|
|
@@ -182,8 +181,7 @@
|
|
|
182
181
|
const states: Record<string, Token[]> = {
|
|
183
182
|
'Panel': panelTokens,
|
|
184
183
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Title / ${STATE_LABELS[s]}`, titleStateTokens(s)])),
|
|
185
|
-
'Title
|
|
186
|
-
'Title Label': titleLabelTokens,
|
|
184
|
+
'Title Block': titleBlockTokens,
|
|
187
185
|
...Object.fromEntries(TOGGLE_STATES.map((s) => [`Toggle / ${STATE_LABELS[s]}`, toggleStateTokens(s)])),
|
|
188
186
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Section / ${STATE_LABELS[s]}`, sectionStateTokens(s)])),
|
|
189
187
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Item / ${STATE_LABELS[s]}`, itemStateTokens(s)])),
|
|
@@ -197,9 +195,14 @@
|
|
|
197
195
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Footer / ${STATE_LABELS[s]}`, footerStateTypeGroups(s)])),
|
|
198
196
|
};
|
|
199
197
|
|
|
198
|
+
// Color groupKeys derive structurally from the part+role: stripping the
|
|
199
|
+
// `--sidenavigation-` prefix and the state segment leaves `section-text`,
|
|
200
|
+
// `item-text`, `footer-text`, `title-label` — so the per-part text colors stay
|
|
201
|
+
// distinct instead of collapsing into one inferred `text` link group (which
|
|
202
|
+
// would phantom-link parts the per-part font groupKeys deliberately keep apart).
|
|
200
203
|
export const allTokens: Token[] = [
|
|
201
204
|
...Object.values(states).flat(),
|
|
202
|
-
...buildTypeGroupColorTokens(typeGroups),
|
|
205
|
+
...buildTypeGroupColorTokens(typeGroups, { component, variants: [...STATEFUL_STATES] }),
|
|
203
206
|
...titleTypographyTokens,
|
|
204
207
|
...sectionTypographyTokens,
|
|
205
208
|
...itemTypographyTokens,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
|
+
import { buildTypeGroupColorTokens, buildTypeGroupFontTokens } from './scaffolding/buildTypeGroupTokens';
|
|
2
3
|
import type { Token, TypeGroupConfig } from './scaffolding/types';
|
|
3
4
|
|
|
4
5
|
export const component = 'table';
|
|
@@ -60,26 +61,16 @@
|
|
|
60
61
|
lineHeightVariable: '--table-default-cell-line-height',
|
|
61
62
|
}],
|
|
62
63
|
};
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
];
|
|
68
|
-
const typeGroupTokens: Token[] = [
|
|
69
|
-
{ label: 'font family', groupKey: 'header-font-family', variable: '--table-default-header-font-family' },
|
|
70
|
-
{ label: 'font size', groupKey: 'header-font-size', variable: '--table-default-header-font-size' },
|
|
71
|
-
{ label: 'font weight', groupKey: 'header-font-weight', variable: '--table-default-header-font-weight' },
|
|
72
|
-
{ label: 'line height', groupKey: 'header-line-height', variable: '--table-default-header-line-height' },
|
|
73
|
-
{ label: 'font family', groupKey: 'cell-font-family', variable: '--table-default-cell-font-family' },
|
|
74
|
-
{ label: 'font size', groupKey: 'cell-font-size', variable: '--table-default-cell-font-size' },
|
|
75
|
-
{ label: 'font weight', groupKey: 'cell-font-weight', variable: '--table-default-cell-font-weight' },
|
|
76
|
-
{ label: 'line height', groupKey: 'cell-line-height', variable: '--table-default-cell-line-height' },
|
|
77
|
-
];
|
|
64
|
+
// Structural derivation keeps header / cell text apart: stripping the `--table-`
|
|
65
|
+
// prefix and the `default` state segment leaves `header-text` / `cell-text` (and
|
|
66
|
+
// `header-font-family`, `cell-line-height`, …). A bare last-dash key would
|
|
67
|
+
// phantom-link header-text and cell-text into one `text` group.
|
|
68
|
+
const derivation = { component, variants: ['default'] } as const;
|
|
78
69
|
|
|
79
70
|
export const allTokens: Token[] = [
|
|
80
71
|
...Object.values(states).flat(),
|
|
81
|
-
...
|
|
82
|
-
...
|
|
72
|
+
...buildTypeGroupColorTokens(typeGroups, derivation),
|
|
73
|
+
...buildTypeGroupFontTokens(typeGroups, derivation),
|
|
83
74
|
];
|
|
84
75
|
</script>
|
|
85
76
|
|
|
@@ -14,7 +14,14 @@ export { buildSiblings } from './scaffolding/siblings';
|
|
|
14
14
|
export type { Sibling } from './scaffolding/siblings';
|
|
15
15
|
export { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
|
|
16
16
|
export type { LinkedToken, LinkedGroup, LinkedBlockResult } from './scaffolding/linkedBlock';
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
buildTypeGroupTokens,
|
|
19
|
+
buildTypeGroupColorTokens,
|
|
20
|
+
buildTypeGroupFontTokens,
|
|
21
|
+
buildTypeGroupShareableContexts,
|
|
22
|
+
structuralGroupKey,
|
|
23
|
+
} from './scaffolding/buildTypeGroupTokens';
|
|
24
|
+
export type { TypeGroupConfig } from './scaffolding/types';
|
|
18
25
|
|
|
19
26
|
// Token schema type — the shape of an entry in an editor's `allTokens` array.
|
|
20
27
|
export type { Token } from './scaffolding/types';
|
|
@@ -16,6 +16,7 @@ import InlineEditActionsEditor, { allTokens as inlineEditActionsTokens } from '.
|
|
|
16
16
|
import InputEditor, { allTokens as inputTokens } from './InputEditor.svelte';
|
|
17
17
|
import MenuSelectEditor, { allTokens as menuSelectTokens } from './MenuSelectEditor.svelte';
|
|
18
18
|
import NotificationEditor, { allTokens as notificationTokens } from './NotificationEditor.svelte';
|
|
19
|
+
import PanelEditor, { allTokens as panelTokens } from './PanelEditor.svelte';
|
|
19
20
|
import ProgressBarEditor, { allTokens as progressBarTokens } from './ProgressBarEditor.svelte';
|
|
20
21
|
import RadioButtonEditor, { allTokens as radioButtonTokens } from './RadioButtonEditor.svelte';
|
|
21
22
|
import SectionDividerEditor, { allTokens as sectionDividerTokens, intrinsics as sectionDividerIntrinsics } from './SectionDividerEditor.svelte';
|
|
@@ -43,6 +44,7 @@ type BuiltInComponentId =
|
|
|
43
44
|
| 'inlineeditactions'
|
|
44
45
|
| 'input'
|
|
45
46
|
| 'menuselect'
|
|
47
|
+
| 'panel'
|
|
46
48
|
| 'sectiondivider'
|
|
47
49
|
| 'collapsiblesection'
|
|
48
50
|
| 'sidenavigation'
|
|
@@ -223,6 +225,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
|
|
|
223
225
|
schema: menuSelectTokens,
|
|
224
226
|
origin: 'system',
|
|
225
227
|
},
|
|
228
|
+
panel: {
|
|
229
|
+
id: 'panel',
|
|
230
|
+
label: 'Panel',
|
|
231
|
+
icon: 'fas fa-window-maximize',
|
|
232
|
+
sourceFile: 'src/system/components/Panel.svelte',
|
|
233
|
+
editorComponent: PanelEditor,
|
|
234
|
+
schema: panelTokens,
|
|
235
|
+
origin: 'system',
|
|
236
|
+
},
|
|
226
237
|
sectiondivider: {
|
|
227
238
|
id: 'sectiondivider',
|
|
228
239
|
label: 'Section Divider',
|