@motion-proto/live-tokens 0.3.6 → 0.3.9
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/README.md +1 -1
- package/package.json +8 -5
- package/src/lib/LiveEditorOverlay.svelte +16 -0
- package/src/component-editor/editorTokens.test.ts +0 -93
- package/src/component-editor/groupKeySlots.test.ts +0 -67
- package/src/component-editor/groupKeySnapshot.test.ts +0 -52
- package/src/lib/componentConfig.test.ts +0 -204
- package/src/lib/editorStore.test.ts +0 -328
- package/src/lib/lazyConfig.test.ts +0 -54
- package/src/lib/migrations/migrations.test.ts +0 -341
- package/src/ui/PaletteEditor.test.ts +0 -108
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Live Tokens
|
|
2
2
|
|
|
3
|
-
A foundational design system for quickly styling and building Svelte
|
|
3
|
+
A foundational design system for quickly styling and building Svelte + Vite microsites. **Edit your tokens and components in real time** — colors, typography, spacing, per-component aliases — and see the site update as you drag the slider. Save the result as a portable configuration you can carry from project to project.
|
|
4
4
|
|
|
5
5
|
`npm install @motion-proto/live-tokens` into your app — install once, style fast. The editor is dev-only; production builds get plain CSS variables and your chosen components, nothing else.
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Design token editor with live CSS variable editing. Svelte 4 + Vite.",
|
|
5
|
+
"description": "Design token editor with live CSS variable editing. Svelte 4/5 + Vite 5/6/7.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"svelte",
|
|
8
8
|
"vite",
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
"src/styles/tokens.css",
|
|
36
36
|
"src/data",
|
|
37
37
|
"src/assets",
|
|
38
|
-
"dist-plugin"
|
|
38
|
+
"dist-plugin",
|
|
39
|
+
"!**/*.test.ts",
|
|
40
|
+
"!**/*.spec.ts",
|
|
41
|
+
"!**/__tests__/**"
|
|
39
42
|
],
|
|
40
43
|
"exports": {
|
|
41
44
|
".": {
|
|
@@ -90,9 +93,9 @@
|
|
|
90
93
|
},
|
|
91
94
|
"peerDependencies": {
|
|
92
95
|
"sass": "^1.0",
|
|
93
|
-
"svelte": "^4.2",
|
|
96
|
+
"svelte": "^4.2 || ^5",
|
|
94
97
|
"svelte-preprocess": "^6.0",
|
|
95
|
-
"vite": "^5
|
|
98
|
+
"vite": "^5 || ^6 || ^7"
|
|
96
99
|
},
|
|
97
100
|
"devDependencies": {
|
|
98
101
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
// this component's type-check passes in consumer projects that haven't added
|
|
5
5
|
// the ambient global to their tsconfig.
|
|
6
6
|
declare const __PROJECT_ROOT__: string | undefined;
|
|
7
|
+
declare const __APP_VERSION__: string | undefined;
|
|
7
8
|
const INJECTED_PROJECT_ROOT: string =
|
|
8
9
|
typeof __PROJECT_ROOT__ !== 'undefined' ? (__PROJECT_ROOT__ ?? '') : '';
|
|
10
|
+
const APP_VERSION: string =
|
|
11
|
+
typeof __APP_VERSION__ !== 'undefined' ? (__APP_VERSION__ ?? '') : '';
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<script lang="ts">
|
|
@@ -294,6 +297,10 @@
|
|
|
294
297
|
</button>
|
|
295
298
|
{/if}
|
|
296
299
|
|
|
300
|
+
{#if APP_VERSION}
|
|
301
|
+
<span class="version" title="live-tokens version">v{APP_VERSION}</span>
|
|
302
|
+
{/if}
|
|
303
|
+
|
|
297
304
|
{#if open}
|
|
298
305
|
<div class="spacer" transition:fade={BTN_FADE}></div>
|
|
299
306
|
{/if}
|
|
@@ -454,6 +461,15 @@
|
|
|
454
461
|
|
|
455
462
|
.spacer { flex: 1; }
|
|
456
463
|
|
|
464
|
+
.version {
|
|
465
|
+
font-size: 10px;
|
|
466
|
+
font-weight: 500;
|
|
467
|
+
color: rgba(255, 255, 255, 0.4);
|
|
468
|
+
letter-spacing: 0.02em;
|
|
469
|
+
margin-left: 2px;
|
|
470
|
+
user-select: none;
|
|
471
|
+
}
|
|
472
|
+
|
|
457
473
|
.hdr-btn {
|
|
458
474
|
display: inline-flex;
|
|
459
475
|
align-items: center;
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { join, dirname } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { buildTokenRegistry, extractGlobalRootBody } from '../lib/tokenRegistry';
|
|
7
|
-
import { componentRegistryEntries } from './registry';
|
|
8
|
-
|
|
9
|
-
const TEST_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const TOKENS_CSS = join(TEST_DIR, '..', 'styles', 'tokens.css');
|
|
11
|
-
const COMPONENTS_DIR = join(TEST_DIR, '..', 'components');
|
|
12
|
-
|
|
13
|
-
// Vitest's CSS plugin swallows `?raw` imports for .css files, so we build the
|
|
14
|
-
// registry from an fs-loaded snapshot rather than going through the default
|
|
15
|
-
// module export. Layer-2 tokens now live in each component's <style> block
|
|
16
|
-
// under `:global(:root)`; merge those bodies with tokens.css to match the
|
|
17
|
-
// runtime registry.
|
|
18
|
-
const tokensSource = readFileSync(TOKENS_CSS, 'utf8');
|
|
19
|
-
const componentTokenCss = readdirSync(COMPONENTS_DIR)
|
|
20
|
-
.filter((f) => f.endsWith('.svelte'))
|
|
21
|
-
.map((f) => extractGlobalRootBody(readFileSync(join(COMPONENTS_DIR, f), 'utf8')))
|
|
22
|
-
.join('\n');
|
|
23
|
-
const registry = buildTokenRegistry(tokensSource + '\n' + componentTokenCss);
|
|
24
|
-
|
|
25
|
-
/** Test whether a variable name fits a layer-1 design-token naming pattern. */
|
|
26
|
-
function isLayer1TokenName(name: string): boolean {
|
|
27
|
-
// Color ramps: --color-<palette>-<step> OR semantic color primitives: --color-<name>
|
|
28
|
-
if (/^--color-[a-z]+(-\d{3})?$/.test(name)) return true;
|
|
29
|
-
if (/^--surface-[a-z]+(-[a-z]+)?$/.test(name)) return true;
|
|
30
|
-
if (/^--border-[a-z]+(-[a-z]+)?$/.test(name)) return true;
|
|
31
|
-
if (/^--text-[a-z]+(-[a-z]+)?$/.test(name)) return true;
|
|
32
|
-
if (/^--(radius|space|font|line-height|shadow|ring|transition|overlay|hover|page-bg|border-width|gradient|icon-size|blur|dot-size)(-[a-z0-9]+)*$/.test(name)) return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Drives both describe blocks: one entry per (component, variable) pair pulled
|
|
37
|
-
// from each editor's exported `allTokens`. This is the authoritative surface —
|
|
38
|
-
// includes template-literal-built tokens (Button, SegmentedControl, etc.) and
|
|
39
|
-
// typography colorVariables that the previous source-regex approach silently
|
|
40
|
-
// missed. See registry.ts for how each editor exports its full token list via
|
|
41
|
-
// `<script context="module">`.
|
|
42
|
-
const editorTokenCases: Array<{ editor: string; variable: string }> = [];
|
|
43
|
-
for (const entry of componentRegistryEntries) {
|
|
44
|
-
for (const t of entry.schema) {
|
|
45
|
-
// `hidden: true` tokens are optional override slots (e.g. split-padding's
|
|
46
|
-
// per-side overrides used as `var(--x, fallback)` by the themed-padding
|
|
47
|
-
// mixin) — they don't need a default declaration in tokens.css.
|
|
48
|
-
if ((t as { hidden?: boolean }).hidden) continue;
|
|
49
|
-
editorTokenCases.push({ editor: entry.id, variable: t.variable });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('design-token architecture', () => {
|
|
54
|
-
it('has editor schemas to inspect', () => {
|
|
55
|
-
expect(editorTokenCases.length).toBeGreaterThan(0);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('every editor-exposed variable resolves to a design token', () => {
|
|
59
|
-
for (const { editor, variable } of editorTokenCases) {
|
|
60
|
-
it(`${editor}: ${variable} resolves via alias chain to a layer-1 token`, () => {
|
|
61
|
-
const chain = registry.resolveAliasChain(variable);
|
|
62
|
-
const terminal = chain[chain.length - 1];
|
|
63
|
-
expect(
|
|
64
|
-
isLayer1TokenName(terminal),
|
|
65
|
-
`${variable} → [${chain.join(' → ')}]; terminal "${terminal}" is not a design-token name. ` +
|
|
66
|
-
`Either rename the target or add a var() alias declaration in tokens.css.`,
|
|
67
|
-
).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Every variable an editor binds must be declared in tokens.css (or a
|
|
73
|
-
// component <style> :global(:root) block) as a var() alias — not a literal,
|
|
74
|
-
// not a raw primitive. Component properties bind to component-scoped Layer-2
|
|
75
|
-
// aliases so editing one component never rebinds a shared primitive used
|
|
76
|
-
// elsewhere.
|
|
77
|
-
describe('every editor follows the component-token pattern', () => {
|
|
78
|
-
for (const { editor, variable } of editorTokenCases) {
|
|
79
|
-
it(`${editor}: ${variable} is a layer-2 component token (declared as var() alias)`, () => {
|
|
80
|
-
const declared = registry.getDeclaredValue(variable);
|
|
81
|
-
expect(
|
|
82
|
-
declared,
|
|
83
|
-
`${variable} is referenced by ${editor} but not declared in tokens.css`,
|
|
84
|
-
).not.toBeNull();
|
|
85
|
-
expect(
|
|
86
|
-
declared!.startsWith('var('),
|
|
87
|
-
`${variable} should be a component-scoped alias declared as var(--primitive); got "${declared}". ` +
|
|
88
|
-
`Component properties must not rebind shared primitives directly.`,
|
|
89
|
-
).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
//
|
|
3
|
-
// Slot-collision invariant for editor schemas.
|
|
4
|
-
//
|
|
5
|
-
// A groupKey declares a sibling set: tokens that share a groupKey are linked
|
|
6
|
-
// peers (`getComponentPropertySiblings`, `isComponentPropertyLinked`,
|
|
7
|
-
// `setComponentAliasLinked`). The codebase convention (src/styles/CONVENTIONS.md)
|
|
8
|
-
// requires shared groupKeys to cross only a VARIANT axis — never a slot/part
|
|
9
|
-
// axis. Crossing slots produces "phantom siblings": the schema reports header
|
|
10
|
-
// and cell as linked peers, but the linked block can't surface them because
|
|
11
|
-
// the editor's `linkableContexts` (correctly) treats them as independent.
|
|
12
|
-
//
|
|
13
|
-
// `registerComponentSchema` already warns at runtime when a typography
|
|
14
|
-
// groupKey covers multiple slots (e.g. one groupKey holding both
|
|
15
|
-
// `--card-...-title-font-family` and `--card-...-body-font-family`). This
|
|
16
|
-
// test asserts the invariant instead of relying on a console warning that
|
|
17
|
-
// nothing fails on.
|
|
18
|
-
//
|
|
19
|
-
// Caught the TableEditor regression where `header` and `cell` typography
|
|
20
|
-
// shared `family`/`size`/`weight`/`height` groupKeys (and `surface`/`padding`
|
|
21
|
-
// shared groupKeys across header/stripe and header/cell respectively). See
|
|
22
|
-
// the commit that introduced this test for context.
|
|
23
|
-
|
|
24
|
-
import { describe, it, expect } from 'vitest';
|
|
25
|
-
import { componentRegistryEntries } from './registry';
|
|
26
|
-
|
|
27
|
-
const TYPOGRAPHY_PROP_SUFFIXES = ['font-family', 'font-size', 'font-weight', 'line-height'] as const;
|
|
28
|
-
|
|
29
|
-
/** Mirror of `typographySlotOf` in `lib/slices/components.ts`. */
|
|
30
|
-
function typographySlotOf(varName: string): string | null {
|
|
31
|
-
for (const suffix of TYPOGRAPHY_PROP_SUFFIXES) {
|
|
32
|
-
if (!varName.endsWith('-' + suffix)) continue;
|
|
33
|
-
const head = varName.slice(0, -(suffix.length + 1));
|
|
34
|
-
const lastDash = head.lastIndexOf('-');
|
|
35
|
-
if (lastDash < 0) return null;
|
|
36
|
-
return head.slice(lastDash + 1);
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('editor groupKey slot invariant', () => {
|
|
42
|
-
for (const entry of componentRegistryEntries) {
|
|
43
|
-
it(`${entry.id}: no typography groupKey straddles multiple slots`, () => {
|
|
44
|
-
const byKey = new Map<string, string[]>();
|
|
45
|
-
for (const t of entry.schema as Array<{ variable: string; groupKey?: string }>) {
|
|
46
|
-
if (!t.groupKey) continue;
|
|
47
|
-
const list = byKey.get(t.groupKey) ?? [];
|
|
48
|
-
list.push(t.variable);
|
|
49
|
-
byKey.set(t.groupKey, list);
|
|
50
|
-
}
|
|
51
|
-
const violations: string[] = [];
|
|
52
|
-
for (const [gk, vars] of byKey) {
|
|
53
|
-
const slots = new Set<string>();
|
|
54
|
-
for (const v of vars) {
|
|
55
|
-
const slot = typographySlotOf(v);
|
|
56
|
-
if (slot) slots.add(slot);
|
|
57
|
-
}
|
|
58
|
-
if (slots.size > 1) {
|
|
59
|
-
violations.push(
|
|
60
|
-
`groupKey "${gk}" spans typography slots {${[...slots].join(', ')}}: ${vars.join(', ')}`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
expect(violations, violations.join('\n')).toEqual([]);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Locks the per-component sibling-group topology in place during the migration
|
|
3
|
-
* away from `getGroupKey`'s last-dash fallback.
|
|
4
|
-
*
|
|
5
|
-
* Workflow:
|
|
6
|
-
* 1. Snapshot current behavior: `UPDATE_SNAPSHOT=1 pnpm test groupKeySnapshot`
|
|
7
|
-
* writes `temp/groupkey-snapshot.json` (groupKey → variables, per component).
|
|
8
|
-
* 2. Migrate: add explicit `groupKey` to every token that previously relied on
|
|
9
|
-
* the legacy fallback; delete the fallback branch in `getGroupKey`.
|
|
10
|
-
* 3. Re-run this test (no env var) — must produce the same JSON.
|
|
11
|
-
*
|
|
12
|
-
* Once the migration is complete and verified, delete this file.
|
|
13
|
-
*/
|
|
14
|
-
import { describe, it, expect } from 'vitest';
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import { componentRegistryEntries } from './registry';
|
|
18
|
-
|
|
19
|
-
const SNAPSHOT_PATH = path.resolve(__dirname, '../../temp/groupkey-snapshot.json');
|
|
20
|
-
|
|
21
|
-
/** Mirrors the *post-migration* getGroupKey logic — schema only, no fallback. */
|
|
22
|
-
function effectiveGroupKey(_component: string, _variable: string, explicit: string | undefined): string | null {
|
|
23
|
-
return explicit ?? null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function computeSnapshot(): Record<string, Record<string, string[]>> {
|
|
27
|
-
const out: Record<string, Record<string, string[]>> = {};
|
|
28
|
-
for (const entry of componentRegistryEntries) {
|
|
29
|
-
const groups: Record<string, string[]> = {};
|
|
30
|
-
for (const t of entry.schema as Array<{ variable: string; groupKey?: string }>) {
|
|
31
|
-
const gk = effectiveGroupKey(entry.id, t.variable, t.groupKey);
|
|
32
|
-
if (!gk) continue;
|
|
33
|
-
(groups[gk] ||= []).push(t.variable);
|
|
34
|
-
}
|
|
35
|
-
for (const k of Object.keys(groups)) groups[k].sort();
|
|
36
|
-
out[entry.id] = Object.fromEntries(Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)));
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('groupKey topology snapshot', () => {
|
|
42
|
-
it('matches the committed snapshot', () => {
|
|
43
|
-
const computed = computeSnapshot();
|
|
44
|
-
if (process.env.UPDATE_SNAPSHOT === '1') {
|
|
45
|
-
fs.mkdirSync(path.dirname(SNAPSHOT_PATH), { recursive: true });
|
|
46
|
-
fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(computed, null, 2) + '\n');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const expected = JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf-8'));
|
|
50
|
-
expect(computed).toEqual(expected);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { get } from 'svelte/store';
|
|
4
|
-
import {
|
|
5
|
-
editorState,
|
|
6
|
-
componentDirty,
|
|
7
|
-
setComponentAlias,
|
|
8
|
-
clearComponentAlias,
|
|
9
|
-
setComponentConfig,
|
|
10
|
-
clearComponentConfig,
|
|
11
|
-
loadComponentActive,
|
|
12
|
-
markComponentSaved,
|
|
13
|
-
seedComponentsFromApi,
|
|
14
|
-
undo,
|
|
15
|
-
redo,
|
|
16
|
-
__resetForTests,
|
|
17
|
-
} from './editorStore';
|
|
18
|
-
|
|
19
|
-
const tokenRef = (name: string) => ({ kind: 'token' as const, name });
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
__resetForTests();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('component aliases — editor-state round trip', () => {
|
|
26
|
-
it('setComponentAlias → undo restores previous state; redo reapplies', () => {
|
|
27
|
-
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-success-high'));
|
|
28
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
29
|
-
|
|
30
|
-
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
31
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-error-high'));
|
|
32
|
-
|
|
33
|
-
undo();
|
|
34
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
35
|
-
|
|
36
|
-
redo();
|
|
37
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-error-high'));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('clearComponentAlias removes the entry and is undoable', () => {
|
|
41
|
-
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-success-high'));
|
|
42
|
-
clearComponentAlias('button', '--button-primary-surface');
|
|
43
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toBeUndefined();
|
|
44
|
-
|
|
45
|
-
undo();
|
|
46
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('setComponentAlias implicitly registers the slice with activeFile "default"', () => {
|
|
50
|
-
setComponentAlias('card', '--card-radius', tokenRef('--radius-lg'));
|
|
51
|
-
expect(get(editorState).components.card.activeFile).toBe('default');
|
|
52
|
-
expect(get(editorState).components.card.aliases['--card-radius']).toEqual(tokenRef('--radius-lg'));
|
|
53
|
-
expect(get(editorState).components.card.config).toEqual({});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('component config — literal-valued knobs', () => {
|
|
58
|
-
it('setComponentConfig stores the value and is undoable', () => {
|
|
59
|
-
setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
|
|
60
|
-
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
|
|
61
|
-
|
|
62
|
-
setComponentConfig('dialog', '--dialog-confirm-variant', 'warning');
|
|
63
|
-
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('warning');
|
|
64
|
-
|
|
65
|
-
undo();
|
|
66
|
-
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('clearComponentConfig removes the entry', () => {
|
|
70
|
-
setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
|
|
71
|
-
clearComponentConfig('dialog', '--dialog-confirm-variant');
|
|
72
|
-
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('setComponentConfig implicitly registers the slice with empty aliases', () => {
|
|
76
|
-
setComponentConfig('dialog', '--dialog-cancel-variant', 'outline');
|
|
77
|
-
expect(get(editorState).components.dialog.activeFile).toBe('default');
|
|
78
|
-
expect(get(editorState).components.dialog.aliases).toEqual({});
|
|
79
|
-
expect(get(editorState).components.dialog.config['--dialog-cancel-variant']).toBe('outline');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('componentDirty — per-component scoping', () => {
|
|
84
|
-
it('marks a component dirty only after its aliases diverge from the saved baseline', () => {
|
|
85
|
-
loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
|
|
86
|
-
expect(get(componentDirty).button).toBe(false);
|
|
87
|
-
|
|
88
|
-
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
89
|
-
expect(get(componentDirty).button).toBe(true);
|
|
90
|
-
|
|
91
|
-
markComponentSaved('button');
|
|
92
|
-
expect(get(componentDirty).button).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('marks a component dirty when config diverges from the saved baseline', () => {
|
|
96
|
-
loadComponentActive('dialog', 'default', {}, { '--dialog-confirm-variant': 'primary' });
|
|
97
|
-
expect(get(componentDirty).dialog).toBe(false);
|
|
98
|
-
|
|
99
|
-
setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
|
|
100
|
-
expect(get(componentDirty).dialog).toBe(true);
|
|
101
|
-
|
|
102
|
-
markComponentSaved('dialog');
|
|
103
|
-
expect(get(componentDirty).dialog).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('editing one component does not dirty another', () => {
|
|
107
|
-
loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
|
|
108
|
-
loadComponentActive('card', 'default', { '--card-radius': '--radius-md' });
|
|
109
|
-
expect(get(componentDirty).button).toBe(false);
|
|
110
|
-
expect(get(componentDirty).card).toBe(false);
|
|
111
|
-
|
|
112
|
-
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
113
|
-
expect(get(componentDirty).button).toBe(true);
|
|
114
|
-
expect(get(componentDirty).card).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('undo after a saved state marks dirty again', () => {
|
|
118
|
-
loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
|
|
119
|
-
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
120
|
-
markComponentSaved('button');
|
|
121
|
-
expect(get(componentDirty).button).toBe(false);
|
|
122
|
-
|
|
123
|
-
undo();
|
|
124
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
125
|
-
expect(get(componentDirty).button).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('loadComponentActive — split-on-load migration', () => {
|
|
130
|
-
it('routes legacy config keys from single-bucket aliases into the config bucket', () => {
|
|
131
|
-
loadComponentActive('dialog', 'default', {
|
|
132
|
-
'--dialog-surface': '--surface-neutral-low',
|
|
133
|
-
'--dialog-confirm-variant': 'danger',
|
|
134
|
-
});
|
|
135
|
-
const slice = get(editorState).components.dialog;
|
|
136
|
-
expect(slice.aliases['--dialog-surface']).toEqual(tokenRef('--surface-neutral-low'));
|
|
137
|
-
expect(slice.aliases['--dialog-confirm-variant']).toBeUndefined();
|
|
138
|
-
expect(slice.config['--dialog-confirm-variant']).toBe('danger');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('keeps CSS-var-valued aliases (e.g. --button-shimmer → --shimmer-on) in the aliases bucket', () => {
|
|
142
|
-
loadComponentActive('button', 'default', {
|
|
143
|
-
'--button-primary-surface': '--surface-success-high',
|
|
144
|
-
'--button-shimmer': '--shimmer-on',
|
|
145
|
-
});
|
|
146
|
-
const slice = get(editorState).components.button;
|
|
147
|
-
expect(slice.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
148
|
-
expect(slice.aliases['--button-shimmer']).toEqual(tokenRef('--shimmer-on'));
|
|
149
|
-
expect(slice.config['--button-shimmer']).toBeUndefined();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('classifies literal alias values as kind "literal"', () => {
|
|
153
|
-
loadComponentActive('myComp', 'default', {
|
|
154
|
-
'--my-comp-token': '--some-token',
|
|
155
|
-
'--my-comp-color': 'rebeccapurple',
|
|
156
|
-
});
|
|
157
|
-
const slice = get(editorState).components.myComp;
|
|
158
|
-
expect(slice.aliases['--my-comp-token']).toEqual(tokenRef('--some-token'));
|
|
159
|
-
expect(slice.aliases['--my-comp-color']).toEqual({ kind: 'literal', value: 'rebeccapurple' });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('explicit config field wins over legacy alias-bucketed value', () => {
|
|
163
|
-
loadComponentActive(
|
|
164
|
-
'dialog',
|
|
165
|
-
'default',
|
|
166
|
-
{ '--dialog-confirm-variant': 'primary' },
|
|
167
|
-
{ '--dialog-confirm-variant': 'danger' },
|
|
168
|
-
);
|
|
169
|
-
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('seedComponentsFromApi — boot-time hydration', () => {
|
|
174
|
-
it('populates state and establishes the clean baseline', () => {
|
|
175
|
-
seedComponentsFromApi({
|
|
176
|
-
button: { activeFile: 'myConfig', aliases: { '--button-primary-surface': '--surface-success-high' } },
|
|
177
|
-
});
|
|
178
|
-
expect(get(editorState).components.button.activeFile).toBe('myConfig');
|
|
179
|
-
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
180
|
-
expect(get(componentDirty).button).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('routes config keys when seeding from legacy single-bucket API payload', () => {
|
|
184
|
-
seedComponentsFromApi({
|
|
185
|
-
dialog: {
|
|
186
|
-
activeFile: 'default',
|
|
187
|
-
aliases: { '--dialog-confirm-variant': 'danger', '--dialog-shadow': '--shadow-2xl' },
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
const slice = get(editorState).components.dialog;
|
|
191
|
-
expect(slice.aliases['--dialog-shadow']).toEqual(tokenRef('--shadow-2xl'));
|
|
192
|
-
expect(slice.aliases['--dialog-confirm-variant']).toBeUndefined();
|
|
193
|
-
expect(slice.config['--dialog-confirm-variant']).toBe('danger');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('replaces the full components slice', () => {
|
|
197
|
-
setComponentAlias('card', '--card-radius', tokenRef('--radius-md'));
|
|
198
|
-
seedComponentsFromApi({
|
|
199
|
-
button: { activeFile: 'default', aliases: {} },
|
|
200
|
-
});
|
|
201
|
-
expect(get(editorState).components.card).toBeUndefined();
|
|
202
|
-
expect(get(editorState).components.button).toBeDefined();
|
|
203
|
-
});
|
|
204
|
-
});
|