@motion-proto/live-tokens 0.70.0 → 0.72.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-check-compliance/SKILL.md +78 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -0
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -2
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -0
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +5 -2
- package/CHANGELOG.md +100 -0
- package/README.md +9 -2
- package/bin/check-component.mjs +2 -11
- package/bin/check-page.mjs +4 -3
- package/bin/cli.mjs +29 -1
- package/bin/lib/catalogue.mjs +6 -2
- package/bin/lib/findings.mjs +17 -0
- package/bin/lib/report.mjs +158 -0
- package/bin/lib/tokenVocabulary.mjs +20 -0
- package/dist-plugin/adjust/index.cjs +33 -6
- package/dist-plugin/adjust/index.d.cts +2 -2
- package/dist-plugin/adjust/index.d.ts +2 -2
- package/dist-plugin/adjust/index.js +7 -6
- package/dist-plugin/{chunk-2YNERPXY.js → chunk-RIXO2E55.js} +1 -1
- package/dist-plugin/{chunk-RVE3MNKM.js → chunk-W6Y4BWFB.js} +3 -1
- package/dist-plugin/{chunk-GPIBU44G.js → chunk-YLCOIGQC.js} +25 -1
- package/dist-plugin/{dataPaths-bJTCEO4H.d.ts → dataPaths-BhWzd5cL.d.cts} +4 -0
- package/dist-plugin/{dataPaths-bJTCEO4H.d.cts → dataPaths-BhWzd5cL.d.ts} +4 -0
- package/dist-plugin/fontPairing/index.cjs +3 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/fontPairing/index.js +1 -1
- package/dist-plugin/generateColorsAndType/index.cjs +28 -2
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +2 -2
- package/dist-plugin/index.cjs +28 -2
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +3 -3
- package/dist-plugin/migrateData/index.cjs +28 -2
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +3 -3
- package/dist-plugin/tokensCssMigrations/index.cjs +3 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +5 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +1 -11
- package/src/editor/component-editor/contract.ts +175 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +0 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +5 -0
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +0 -21
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +1 -7
- package/src/editor/core/components/aliasKinds.ts +6 -2
- package/src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts +39 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/live-tokens/data/themes/autumn.json +1 -7
- package/src/live-tokens/data/themes/halloween.json +1 -7
- package/src/live-tokens/data/themes/midnight-study.json +1 -7
- package/src/live-tokens/data/themes/ocean.json +1 -7
- package/src/live-tokens/data/themes/royal-velvet.json +1 -7
- package/src/live-tokens/data/themes/sketchy.json +1 -7
- package/src/live-tokens/data/themes/spring-meadow.json +1 -7
- package/src/live-tokens/data/themes/sunset.json +1 -7
- package/src/system/components/SectionDivider.svelte +3 -143
- package/src/system/components/FloatingTokenTags.css +0 -284
- package/src/system/components/FloatingTokenTags.svelte +0 -570
|
@@ -976,6 +976,29 @@ var componentMigration_2026_09_01_gateSuffixEnabled = {
|
|
|
976
976
|
}
|
|
977
977
|
};
|
|
978
978
|
|
|
979
|
+
// src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts
|
|
980
|
+
var DROPPED2 = new Set(
|
|
981
|
+
["lg", "md", "sm"].flatMap((v) => [
|
|
982
|
+
`--sectiondivider-${v}-title-outline-width`,
|
|
983
|
+
`--sectiondivider-${v}-title-outline-color`
|
|
984
|
+
])
|
|
985
|
+
);
|
|
986
|
+
var componentMigration_2026_09_02_sectiondividerDropTitleOutline = {
|
|
987
|
+
id: "2026-09-02-sectiondivider-drop-title-outline",
|
|
988
|
+
fromVersion: 25,
|
|
989
|
+
toVersion: 26,
|
|
990
|
+
appliesTo: "component-config",
|
|
991
|
+
apply(rawVars, meta) {
|
|
992
|
+
if (meta.component !== "sectiondivider") return { ...rawVars };
|
|
993
|
+
const out = {};
|
|
994
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
995
|
+
if (DROPPED2.has(key)) continue;
|
|
996
|
+
out[key] = value;
|
|
997
|
+
}
|
|
998
|
+
return out;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
979
1002
|
// src/editor/core/themes/migrations/index.ts
|
|
980
1003
|
var MIGRATIONS = [
|
|
981
1004
|
colorsAndTypeMigration_2026_04_24_legacyKeysAndBgToCanvas,
|
|
@@ -1009,7 +1032,8 @@ var MIGRATIONS = [
|
|
|
1009
1032
|
colorsAndTypeMigration_2026_09_01_tintRename,
|
|
1010
1033
|
componentMigration_2026_09_01_tintRename,
|
|
1011
1034
|
componentMigration_2026_09_01_tabbarActiveTint,
|
|
1012
|
-
componentMigration_2026_09_01_gateSuffixEnabled
|
|
1035
|
+
componentMigration_2026_09_01_gateSuffixEnabled,
|
|
1036
|
+
componentMigration_2026_09_02_sectiondividerDropTitleOutline
|
|
1013
1037
|
];
|
|
1014
1038
|
function countFor(kind) {
|
|
1015
1039
|
return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
|
|
@@ -2178,7 +2202,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
2178
2202
|
"componentConfigsDir",
|
|
2179
2203
|
"themesDir",
|
|
2180
2204
|
"manifestsDir",
|
|
2181
|
-
"tokensCssPath"
|
|
2205
|
+
"tokensCssPath",
|
|
2206
|
+
"componentDirs",
|
|
2207
|
+
"checks"
|
|
2182
2208
|
]);
|
|
2183
2209
|
var cached = null;
|
|
2184
2210
|
function readLiveTokensConfig() {
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
normalizeTheme,
|
|
8
8
|
planLegacyRenames,
|
|
9
9
|
versionedFileResourceServer
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-RIXO2E55.js";
|
|
11
11
|
import {
|
|
12
12
|
CURRENT_COMPONENT_SCHEMA_VERSION
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-YLCOIGQC.js";
|
|
14
14
|
import {
|
|
15
15
|
readLiveTokensConfig,
|
|
16
16
|
resolveDataDirs
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-W6Y4BWFB.js";
|
|
18
18
|
|
|
19
19
|
// vite-plugin/migrateData/migrateData.ts
|
|
20
20
|
import fs from "fs";
|
|
@@ -648,7 +648,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
648
648
|
"componentConfigsDir",
|
|
649
649
|
"themesDir",
|
|
650
650
|
"manifestsDir",
|
|
651
|
-
"tokensCssPath"
|
|
651
|
+
"tokensCssPath",
|
|
652
|
+
"componentDirs",
|
|
653
|
+
"checks"
|
|
652
654
|
]);
|
|
653
655
|
var cached = null;
|
|
654
656
|
function readLiveTokensConfig() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
|
|
6
6
|
"keywords": [
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"types": "./src/editor/component-editor/index.ts",
|
|
73
73
|
"default": "./src/editor/component-editor/index.ts"
|
|
74
74
|
},
|
|
75
|
+
"./component-editor/contract": {
|
|
76
|
+
"types": "./src/editor/component-editor/contract.ts",
|
|
77
|
+
"default": "./src/editor/component-editor/contract.ts"
|
|
78
|
+
},
|
|
75
79
|
"./ui": {
|
|
76
80
|
"svelte": "./src/editor/ui/index.ts",
|
|
77
81
|
"types": "./src/editor/ui/index.ts",
|
|
@@ -36,25 +36,18 @@
|
|
|
36
36
|
{ label: 'hairline thickness', canBeLinked: true, groupKey: 'hairline-thickness', variable: `--sectiondivider-${v}-hairline-thickness`, element: 'hairline' },
|
|
37
37
|
];
|
|
38
38
|
}
|
|
39
|
-
function titleOutlineTokens(v: Variant): Token[] {
|
|
40
|
-
return [
|
|
41
|
-
{ label: 'outline thickness', canBeLinked: true, groupKey: 'title-outline-width', variable: `--sectiondivider-${v}-title-outline-width`, element: 'title' },
|
|
42
|
-
{ label: 'outline color', canBeLinked: true, groupKey: 'title-outline-color', variable: `--sectiondivider-${v}-title-outline-color`, element: 'title' },
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
39
|
function backgroundTokens(v: Variant): Token[] {
|
|
46
40
|
return [
|
|
47
41
|
{ label: 'background', groupKey: 'background', variable: `--sectiondivider-${v}-background`, kind: 'gradient', family: variants.find((x) => x.key === v)!.family },
|
|
48
42
|
];
|
|
49
43
|
}
|
|
50
44
|
function variantTokens(v: Variant): Token[] {
|
|
51
|
-
return [...containerTokens(v), ...hairlineTokens(v), ...
|
|
45
|
+
return [...containerTokens(v), ...hairlineTokens(v), ...backgroundTokens(v), ...typePaddingTokens(v)];
|
|
52
46
|
}
|
|
53
47
|
function stateTokens(v: Variant): Token[] {
|
|
54
48
|
return [
|
|
55
49
|
...containerTokens(v),
|
|
56
50
|
...hairlineTokens(v),
|
|
57
|
-
...titleOutlineTokens(v).map((t) => ({ ...t, hidden: true })),
|
|
58
51
|
...backgroundTokens(v).map((t) => ({ ...t, hidden: true })),
|
|
59
52
|
...typePaddingTokens(v),
|
|
60
53
|
];
|
|
@@ -72,8 +65,6 @@
|
|
|
72
65
|
weightVariable: `--sectiondivider-${v}-title-font-weight`,
|
|
73
66
|
lineHeightVariable: `--sectiondivider-${v}-title-line-height`,
|
|
74
67
|
letterSpacingVariable: `--sectiondivider-${v}-title-letter-spacing`,
|
|
75
|
-
outlineWidthVariable: `--sectiondivider-${v}-title-outline-width`,
|
|
76
|
-
outlineColorVariable: `--sectiondivider-${v}-title-outline-color`,
|
|
77
68
|
},
|
|
78
69
|
{
|
|
79
70
|
legend: '',
|
|
@@ -127,7 +118,6 @@
|
|
|
127
118
|
const LINKED_GROUP_KEYS = [
|
|
128
119
|
'container-padding', 'radius', 'border', 'border-width', 'shadow',
|
|
129
120
|
'hairline-color', 'hairline-thickness',
|
|
130
|
-
'title-outline-width', 'title-outline-color',
|
|
131
121
|
'title-color', 'description-color', 'eyebrow-color',
|
|
132
122
|
'title-padding', 'description-padding', 'eyebrow-padding',
|
|
133
123
|
'title-font-family', 'title-font-weight', 'title-line-height', 'title-letter-spacing',
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry contract, as a function a project outside this package can run.
|
|
3
|
+
*
|
|
4
|
+
* Node-only: it reads the runtime `.svelte` file and the component's
|
|
5
|
+
* `default.json` off disk, so it lives behind its own subpath export
|
|
6
|
+
* (`@motion-proto/live-tokens/component-editor/contract`) rather than in the
|
|
7
|
+
* browser barrel. A consumer's whole suite is a `describe.each` over their own
|
|
8
|
+
* registrations and one call:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // @vitest-environment happy-dom
|
|
12
|
+
* import { describe, it, expect } from 'vitest';
|
|
13
|
+
* import { getComponentRegistryEntries, registerComponent } from '@motion-proto/live-tokens';
|
|
14
|
+
* import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';
|
|
15
|
+
* import MyWidgetEditor, { allTokens } from '../src/system/components/MyWidgetEditor.svelte';
|
|
16
|
+
*
|
|
17
|
+
* registerComponent({ id: 'mywidget', label: 'My Widget', icon: 'fas fa-magic',
|
|
18
|
+
* sourceFile: 'src/system/components/MyWidget.svelte',
|
|
19
|
+
* editorComponent: MyWidgetEditor, schema: allTokens });
|
|
20
|
+
*
|
|
21
|
+
* const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');
|
|
22
|
+
*
|
|
23
|
+
* describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {
|
|
24
|
+
* it('meets the registry contract', () => {
|
|
25
|
+
* expect(checkRegistryEntry(entry)).toEqual([]);
|
|
26
|
+
* });
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The `origin` filter is load-bearing outside this package: the registry always
|
|
31
|
+
* carries the shipped components too, and their `sourceFile` paths are relative
|
|
32
|
+
* to the package root, not the consumer's.
|
|
33
|
+
*/
|
|
34
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { get } from 'svelte/store';
|
|
37
|
+
import type { RegistryEntry } from './registry';
|
|
38
|
+
import type { Token } from './scaffolding/types';
|
|
39
|
+
import { parseColorOpacity } from '../core/themes/parsers/colorOpacity';
|
|
40
|
+
import { editorState, setComponentAlias, clearComponentAlias } from '../core/store/editorStore';
|
|
41
|
+
|
|
42
|
+
export interface RegistryContractOptions {
|
|
43
|
+
/** Root that `entry.sourceFile` is relative to. Defaults to `process.cwd()`,
|
|
44
|
+
which is the project root under both vitest and `npm test`. */
|
|
45
|
+
projectRoot?: string;
|
|
46
|
+
/** Directory holding `<id>/default.json`. Defaults to
|
|
47
|
+
`<projectRoot>/src/live-tokens/data/component-configs`. Pass the
|
|
48
|
+
`componentConfigsDir` from `live-tokens.config.json` when a project moved it. */
|
|
49
|
+
componentConfigsDir?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PADDING_SIDE_RE = /-padding-(top|right|bottom|left)$/;
|
|
53
|
+
const PROBE = { kind: 'token', name: '--space-8' } as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Tokens excluded from the runtime-declaration and default-seed checks:
|
|
57
|
+
* - `hidden: true` (vestigial or hidden by design),
|
|
58
|
+
* - `kind: 'gradient'` (structured payloads, stored as gradient objects, not vars),
|
|
59
|
+
* - `-padding-(top|right|bottom|left)` (written on demand by the split-padding
|
|
60
|
+
* UI and consumed through the `themed-padding` mixin's fallback chain, so
|
|
61
|
+
* they exist as neither `:root` declarations nor default seeds).
|
|
62
|
+
*/
|
|
63
|
+
function isEditableSurfaceToken(t: Token): boolean {
|
|
64
|
+
if (t.hidden) return false;
|
|
65
|
+
if (t.kind === 'gradient') return false;
|
|
66
|
+
if (PADDING_SIDE_RE.test(t.variable)) return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Every `--xxx:` declaration in every `<style>` block of a Svelte file. */
|
|
71
|
+
function extractRuntimeDeclarations(source: string): Set<string> {
|
|
72
|
+
const out = new Set<string>();
|
|
73
|
+
const blocks = [...source.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]);
|
|
74
|
+
for (const block of blocks) {
|
|
75
|
+
for (const m of block.matchAll(/(?:^|[{;\n])\s*(--[a-z0-9-]+)\s*:/g)) {
|
|
76
|
+
out.add(m[1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Opacity a seeded alias resolves to. A plain token alias is fully opaque; a
|
|
83
|
+
gradient payload or `transparent` carries no guarantee, so it reads as 0. */
|
|
84
|
+
function seededOpacity(raw: unknown): number {
|
|
85
|
+
if (typeof raw !== 'string' || raw === 'transparent') return 0;
|
|
86
|
+
return parseColorOpacity(raw)?.opacity ?? 100;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run the registry contract against one entry and return a violation line per
|
|
91
|
+
* failure. An empty array is the pass.
|
|
92
|
+
*
|
|
93
|
+
* The contract:
|
|
94
|
+
* 1. Registration — `sourceFile` resolves to a real file, the schema is non-empty.
|
|
95
|
+
* 2. Uniqueness — no schema variable is declared twice.
|
|
96
|
+
* 3. Editor ↔ runtime — every editable token's CSS var is declared in the
|
|
97
|
+
* runtime's `<style>` block, so an edit has something to repaint.
|
|
98
|
+
* 4. Editor ↔ default config — every editable token has a seed alias in
|
|
99
|
+
* `<id>/default.json`, so a consumer adopts with full defaults. A component
|
|
100
|
+
* with no `default.json` is editor-only; the seed and floor checks skip it.
|
|
101
|
+
* 5. Opacity floors — a token declaring `minOpacity` ships a default at or
|
|
102
|
+
* above it, so a floating panel starts out legible over page content.
|
|
103
|
+
* 6. Round-trip — `setComponentAlias` persists into the slice under the same key.
|
|
104
|
+
*/
|
|
105
|
+
export function checkRegistryEntry(
|
|
106
|
+
entry: RegistryEntry,
|
|
107
|
+
options: RegistryContractOptions = {},
|
|
108
|
+
): string[] {
|
|
109
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
110
|
+
const configsDir =
|
|
111
|
+
options.componentConfigsDir ??
|
|
112
|
+
path.resolve(projectRoot, 'src/live-tokens/data/component-configs');
|
|
113
|
+
|
|
114
|
+
const violations: string[] = [];
|
|
115
|
+
const schema = entry.schema ?? [];
|
|
116
|
+
const runtimePath = path.resolve(projectRoot, entry.sourceFile);
|
|
117
|
+
|
|
118
|
+
if (schema.length === 0) violations.push('registration: schema is empty');
|
|
119
|
+
if (!existsSync(runtimePath)) {
|
|
120
|
+
violations.push(`registration: sourceFile does not resolve to a file (${runtimePath})`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
for (const t of schema) {
|
|
125
|
+
if (seen.has(t.variable)) violations.push(`uniqueness: ${t.variable} is declared twice`);
|
|
126
|
+
seen.add(t.variable);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const editable = schema.filter(isEditableSurfaceToken);
|
|
130
|
+
|
|
131
|
+
if (existsSync(runtimePath)) {
|
|
132
|
+
const runtimeVars = extractRuntimeDeclarations(readFileSync(runtimePath, 'utf-8'));
|
|
133
|
+
for (const t of editable) {
|
|
134
|
+
if (!runtimeVars.has(t.variable)) {
|
|
135
|
+
violations.push(`runtime: ${t.variable} is not declared in ${entry.sourceFile}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const configPath = path.join(configsDir, entry.id, 'default.json');
|
|
141
|
+
if (existsSync(configPath)) {
|
|
142
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8')) as {
|
|
143
|
+
aliases?: Record<string, unknown>;
|
|
144
|
+
};
|
|
145
|
+
const aliases = config.aliases ?? {};
|
|
146
|
+
for (const t of editable) {
|
|
147
|
+
if (!(t.variable in aliases)) {
|
|
148
|
+
violations.push(`default config: ${t.variable} has no seed in ${entry.id}/default.json`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const t of schema) {
|
|
152
|
+
if (t.minOpacity === undefined) continue;
|
|
153
|
+
const opacity = seededOpacity(aliases[t.variable]);
|
|
154
|
+
if (opacity < t.minOpacity) {
|
|
155
|
+
violations.push(
|
|
156
|
+
`opacity floor: ${t.variable} seeds at ${opacity}%, below its ${t.minOpacity}% floor`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sample = editable[0];
|
|
163
|
+
if (sample) {
|
|
164
|
+
setComponentAlias(entry.id, sample.variable, PROBE);
|
|
165
|
+
const stored = get(editorState).components[entry.id]?.aliases?.[sample.variable];
|
|
166
|
+
if (JSON.stringify(stored) !== JSON.stringify(PROBE)) {
|
|
167
|
+
violations.push(
|
|
168
|
+
`round-trip: setComponentAlias(${entry.id}, ${sample.variable}) stored ${JSON.stringify(stored)}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
clearComponentAlias(entry.id, sample.variable);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return violations;
|
|
175
|
+
}
|
|
@@ -140,10 +140,6 @@
|
|
|
140
140
|
lineHeightLabel={tg.lineHeightLabel ?? 'line height'}
|
|
141
141
|
letterSpacingVariable={tg.letterSpacingVariable}
|
|
142
142
|
letterSpacingLabel={tg.letterSpacingLabel ?? 'letter spacing'}
|
|
143
|
-
outlineWidthVariable={tg.outlineWidthVariable}
|
|
144
|
-
outlineWidthLabel={tg.outlineWidthLabel ?? 'outline thickness'}
|
|
145
|
-
outlineColorVariable={tg.outlineColorVariable}
|
|
146
|
-
outlineColorLabel={tg.outlineColorLabel ?? 'outline color'}
|
|
147
143
|
{component}
|
|
148
144
|
{onchange}
|
|
149
145
|
/>
|
|
@@ -183,10 +179,6 @@
|
|
|
183
179
|
lineHeightLabel={tg.lineHeightLabel ?? 'line height'}
|
|
184
180
|
letterSpacingVariable={tg.letterSpacingVariable}
|
|
185
181
|
letterSpacingLabel={tg.letterSpacingLabel ?? 'letter spacing'}
|
|
186
|
-
outlineWidthVariable={tg.outlineWidthVariable}
|
|
187
|
-
outlineWidthLabel={tg.outlineWidthLabel ?? 'outline thickness'}
|
|
188
|
-
outlineColorVariable={tg.outlineColorVariable}
|
|
189
|
-
outlineColorLabel={tg.outlineColorLabel ?? 'outline color'}
|
|
190
182
|
{component}
|
|
191
183
|
{onchange}
|
|
192
184
|
/>
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
'divider-height',
|
|
73
73
|
'divider-inset',
|
|
74
74
|
'dot-size',
|
|
75
|
+
'length',
|
|
75
76
|
'radius',
|
|
76
77
|
'padding',
|
|
77
78
|
'padding-split',
|
|
@@ -150,6 +151,9 @@
|
|
|
150
151
|
extra: () => ({ mode: 'sides' }),
|
|
151
152
|
},
|
|
152
153
|
'gap': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
|
|
154
|
+
/* A bare width/height/size is a length off the same `--space-*` scale gap
|
|
155
|
+
reads from, so it takes gap's picker rather than a scale of its own. */
|
|
156
|
+
'length': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
|
|
153
157
|
'duration': { component: UIVariantSelector, extra: () => ({ ...DURATION }) },
|
|
154
158
|
'easing': { component: UIEasingSelector },
|
|
155
159
|
'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
|
|
@@ -176,6 +180,7 @@
|
|
|
176
180
|
'divider-height',
|
|
177
181
|
'divider-inset',
|
|
178
182
|
'dot-size',
|
|
183
|
+
'length',
|
|
179
184
|
'radius',
|
|
180
185
|
'padding',
|
|
181
186
|
'padding-split',
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import UIPaletteSelector from '../../ui/UIPaletteSelector.svelte';
|
|
3
|
-
import UIVariantSelector from '../../ui/UIVariantSelector.svelte';
|
|
4
3
|
import UIFontFamilySelector from '../../ui/UIFontFamilySelector.svelte';
|
|
5
4
|
import UIFontSizeSelector from '../../ui/UIFontSizeSelector.svelte';
|
|
6
5
|
import UIFontWeightSelector from '../../ui/UIFontWeightSelector.svelte';
|
|
7
6
|
import UILineHeightSelector from '../../ui/UILineHeightSelector.svelte';
|
|
8
7
|
import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
|
|
9
8
|
import FieldsetWrapper from './FieldsetWrapper.svelte';
|
|
10
|
-
import { BORDER_WIDTH } from '../../ui/variantScales';
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
|
|
@@ -26,13 +24,6 @@
|
|
|
26
24
|
lineHeightLabel?: string;
|
|
27
25
|
letterSpacingVariable?: string | undefined;
|
|
28
26
|
letterSpacingLabel?: string;
|
|
29
|
-
/** Optional outline rows rendered under the typography rows so a text-with-
|
|
30
|
-
stroke group keeps stroke controls visually nested with the type they
|
|
31
|
-
drive (e.g. SectionDivider title outline). */
|
|
32
|
-
outlineWidthVariable?: string | undefined;
|
|
33
|
-
outlineWidthLabel?: string;
|
|
34
|
-
outlineColorVariable?: string | undefined;
|
|
35
|
-
outlineColorLabel?: string;
|
|
36
27
|
/** When set, writes persist through the editor store under this component. */
|
|
37
28
|
component?: string | undefined;
|
|
38
29
|
/** Legend text for the fieldset. */
|
|
@@ -54,10 +45,6 @@
|
|
|
54
45
|
lineHeightLabel = 'line-h',
|
|
55
46
|
letterSpacingVariable = undefined,
|
|
56
47
|
letterSpacingLabel = 'letter-sp',
|
|
57
|
-
outlineWidthVariable = undefined,
|
|
58
|
-
outlineWidthLabel = 'outline thickness',
|
|
59
|
-
outlineColorVariable = undefined,
|
|
60
|
-
outlineColorLabel = 'outline color',
|
|
61
48
|
component = undefined,
|
|
62
49
|
legend = 'type',
|
|
63
50
|
onchange,
|
|
@@ -89,14 +76,6 @@
|
|
|
89
76
|
<span class="row-label">{letterSpacingLabel}</span>
|
|
90
77
|
<UILetterSpacingSelector variable={letterSpacingVariable} {component} canBeLinked {onchange} />
|
|
91
78
|
{/if}
|
|
92
|
-
{#if outlineWidthVariable}
|
|
93
|
-
<span class="row-label">{outlineWidthLabel}</span>
|
|
94
|
-
<UIVariantSelector variable={outlineWidthVariable} {component} canBeLinked {...BORDER_WIDTH} {onchange} />
|
|
95
|
-
{/if}
|
|
96
|
-
{#if outlineColorVariable}
|
|
97
|
-
<span class="row-label">{outlineColorLabel}</span>
|
|
98
|
-
<UIPaletteSelector variable={outlineColorVariable} {component} canBeLinked {onchange} />
|
|
99
|
-
{/if}
|
|
100
79
|
</div>
|
|
101
80
|
</FieldsetWrapper>
|
|
102
81
|
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
|
|
106
106
|
let activeTab: string = $state('');
|
|
107
107
|
|
|
108
|
-
const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable'
|
|
108
|
+
const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable'] as const;
|
|
109
109
|
// Carry per-side derived vars so split padding fully transfers; no-op when absent.
|
|
110
110
|
const PADDING_SIDES = ['top', 'right', 'bottom', 'left'] as const;
|
|
111
111
|
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
|
|
148
148
|
/** TypeGroup props whose values are color tokens; preserve-color-families
|
|
149
149
|
skips these in the typeGroups copy loop. */
|
|
150
|
-
const COLOR_TYPE_PROPS = new Set(['colorVariable'
|
|
150
|
+
const COLOR_TYPE_PROPS = new Set(['colorVariable']);
|
|
151
151
|
|
|
152
152
|
/** True iff `colorRef` is a CSS-var token whose slug contains `family`
|
|
153
153
|
as a hyphen-delimited segment (e.g. `--surface-canvas-low` ∋ `canvas`). */
|
|
@@ -69,9 +69,7 @@ export type IntrinsicSpec = {
|
|
|
69
69
|
|
|
70
70
|
/** Editor type-group: a fieldset containing a coordinated set of typography tokens
|
|
71
71
|
(text color + font-family/size/weight/line-height) for a piece of content
|
|
72
|
-
(e.g. a card title, notification body).
|
|
73
|
-
text-with-stroke groups (e.g. SectionDivider title) keep their stroke
|
|
74
|
-
width/color visually nested with the typography that drives them. */
|
|
72
|
+
(e.g. a card title, notification body). */
|
|
75
73
|
export type TypeGroupConfig = {
|
|
76
74
|
legend?: string;
|
|
77
75
|
colorVariable: string;
|
|
@@ -89,10 +87,6 @@ export type TypeGroupConfig = {
|
|
|
89
87
|
lineHeightLabel?: string;
|
|
90
88
|
letterSpacingVariable?: string;
|
|
91
89
|
letterSpacingLabel?: string;
|
|
92
|
-
outlineWidthVariable?: string;
|
|
93
|
-
outlineWidthLabel?: string;
|
|
94
|
-
outlineColorVariable?: string;
|
|
95
|
-
outlineColorLabel?: string;
|
|
96
90
|
/** Explicit groupKey for this group's color token. Wins over every derivation
|
|
97
91
|
(structural or `groupKeyFor`) and is never recomputed — the durable, one-line
|
|
98
92
|
fix when the derived key is wrong. */
|
|
@@ -7,6 +7,7 @@ export type TokenKind =
|
|
|
7
7
|
| 'divider-height'
|
|
8
8
|
| 'divider-inset'
|
|
9
9
|
| 'dot-size'
|
|
10
|
+
| 'length'
|
|
10
11
|
| 'blur'
|
|
11
12
|
| 'scale'
|
|
12
13
|
| 'shadow'
|
|
@@ -60,11 +61,14 @@ export const KIND_RULES: ReadonlyArray<{
|
|
|
60
61
|
{ kind: 'border-width', suffix: ['-border-width', '-accent-width', '-hairline-thickness', '-thickness'],
|
|
61
62
|
prefix: ['--border-width-'] },
|
|
62
63
|
{ kind: 'border', suffix: ['-border'], prefix: ['--border-'] },
|
|
64
|
+
// A dimension with no more specific name behind it — a panel's width, an
|
|
65
|
+
// avatar's size. Last of the geometry rules, so every `-border-width`,
|
|
66
|
+
// `-divider-height`, `-icon-size` and the rest claim their token first.
|
|
67
|
+
{ kind: 'length', suffix: ['-width', '-height', '-size'] },
|
|
63
68
|
// Fills. A tint is a wash over a surface, so it takes the surface picker: the
|
|
64
69
|
// full palette with an alpha, not just the tint stops it defaults to.
|
|
65
70
|
{ kind: 'surface', suffix: ['-surface', '-fill', '-divider', '-background', '-indicator',
|
|
66
|
-
'-thumb', '-accent', '-color', '-tint', '-opacity',
|
|
67
|
-
'-width', '-height', '-size'],
|
|
71
|
+
'-thumb', '-accent', '-color', '-tint', '-opacity'],
|
|
68
72
|
prefix: ['--surface-', '--tint', '--color-'] },
|
|
69
73
|
];
|
|
70
74
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SectionDivider title outline, removed (2026-09-02).
|
|
5
|
+
*
|
|
6
|
+
* The outline was a decorative stroke around the title's glyphs, and the only
|
|
7
|
+
* reason the title was an SVG `<text>` behind a `feMorphology` filter rather
|
|
8
|
+
* than an element. That SVG cost a `getBBox()` viewBox, a per-instance
|
|
9
|
+
* MutationObserver on the document's inline style (filter primitives cannot
|
|
10
|
+
* read a CSS var), font-load listeners to re-measure, and a title that was not
|
|
11
|
+
* selectable or findable. It shipped transparent in the default and in all
|
|
12
|
+
* eight presets, and the trap-out it was built for is done by the layout: the
|
|
13
|
+
* `through-label` hairlines flank the title in a flex row, so no rule ever runs
|
|
14
|
+
* behind the glyphs.
|
|
15
|
+
*
|
|
16
|
+
* Dropping the keys rather than rebinding them: nothing reads the values now.
|
|
17
|
+
*/
|
|
18
|
+
const DROPPED = new Set(
|
|
19
|
+
['lg', 'md', 'sm'].flatMap((v) => [
|
|
20
|
+
`--sectiondivider-${v}-title-outline-width`,
|
|
21
|
+
`--sectiondivider-${v}-title-outline-color`,
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const componentMigration_2026_09_02_sectiondividerDropTitleOutline: Migration = {
|
|
26
|
+
id: '2026-09-02-sectiondivider-drop-title-outline',
|
|
27
|
+
fromVersion: 25,
|
|
28
|
+
toVersion: 26,
|
|
29
|
+
appliesTo: 'component-config',
|
|
30
|
+
apply(rawVars, meta) {
|
|
31
|
+
if (meta.component !== 'sectiondivider') return { ...rawVars };
|
|
32
|
+
const out: Record<string, string> = {};
|
|
33
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
34
|
+
if (DROPPED.has(key)) continue;
|
|
35
|
+
out[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
} from './2026-09-01-tint-rename';
|
|
73
73
|
import { componentMigration_2026_09_01_tabbarActiveTint } from './2026-09-01-tabbar-active-tint';
|
|
74
74
|
import { componentMigration_2026_09_01_gateSuffixEnabled } from './2026-09-01-gate-suffix-enabled';
|
|
75
|
+
import { componentMigration_2026_09_02_sectiondividerDropTitleOutline } from './2026-09-02-sectiondivider-drop-title-outline';
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* Registered migrations. Order in this array does not matter — the runner
|
|
@@ -110,6 +111,7 @@ export const MIGRATIONS: Migration[] = [
|
|
|
110
111
|
componentMigration_2026_09_01_tintRename,
|
|
111
112
|
componentMigration_2026_09_01_tabbarActiveTint,
|
|
112
113
|
componentMigration_2026_09_01_gateSuffixEnabled,
|
|
114
|
+
componentMigration_2026_09_02_sectiondividerDropTitleOutline,
|
|
113
115
|
];
|
|
114
116
|
|
|
115
117
|
function countFor(kind: 'colors-and-type' | 'component-config'): number {
|
|
@@ -3515,7 +3515,6 @@
|
|
|
3515
3515
|
"--sectiondivider-lg-title-font-size": "--font-size-5xl",
|
|
3516
3516
|
"--sectiondivider-lg-title-line-height": "--line-height-normal",
|
|
3517
3517
|
"--sectiondivider-lg-title-letter-spacing": "--letter-spacing-normal",
|
|
3518
|
-
"--sectiondivider-lg-title-outline-width": "--border-width-4",
|
|
3519
3518
|
"--sectiondivider-lg-description-font-family": "--font-sans",
|
|
3520
3519
|
"--sectiondivider-lg-description-font-weight": "--font-weight-medium",
|
|
3521
3520
|
"--sectiondivider-lg-description-font-size": "--font-size-lg",
|
|
@@ -3537,14 +3536,12 @@
|
|
|
3537
3536
|
"--sectiondivider-lg-description": "--text-alternate",
|
|
3538
3537
|
"--sectiondivider-lg-eyebrow": "--text-brand",
|
|
3539
3538
|
"--sectiondivider-lg-border": "--color-transparent",
|
|
3540
|
-
"--sectiondivider-lg-title-outline-color": "transparent",
|
|
3541
3539
|
"--sectiondivider-lg-hairline-color": "--border-brand-medium",
|
|
3542
3540
|
"--sectiondivider-md-title-font-family": "--font-display",
|
|
3543
3541
|
"--sectiondivider-md-title-font-weight": "--font-weight-bold",
|
|
3544
3542
|
"--sectiondivider-md-title-font-size": "--font-size-4xl",
|
|
3545
3543
|
"--sectiondivider-md-title-line-height": "--line-height-normal",
|
|
3546
3544
|
"--sectiondivider-md-title-letter-spacing": "--letter-spacing-normal",
|
|
3547
|
-
"--sectiondivider-md-title-outline-width": "--border-width-4",
|
|
3548
3545
|
"--sectiondivider-md-description-font-family": "--font-sans",
|
|
3549
3546
|
"--sectiondivider-md-description-font-weight": "--font-weight-medium",
|
|
3550
3547
|
"--sectiondivider-md-description-font-size": "--font-size-lg",
|
|
@@ -3566,14 +3563,12 @@
|
|
|
3566
3563
|
"--sectiondivider-md-description": "--text-secondary",
|
|
3567
3564
|
"--sectiondivider-md-eyebrow": "--text-tertiary",
|
|
3568
3565
|
"--sectiondivider-md-border": "--color-transparent",
|
|
3569
|
-
"--sectiondivider-md-title-outline-color": "transparent",
|
|
3570
3566
|
"--sectiondivider-md-hairline-color": "--border-brand-medium",
|
|
3571
3567
|
"--sectiondivider-sm-title-font-family": "--font-display",
|
|
3572
3568
|
"--sectiondivider-sm-title-font-weight": "--font-weight-medium",
|
|
3573
3569
|
"--sectiondivider-sm-title-font-size": "--font-size-3xl",
|
|
3574
3570
|
"--sectiondivider-sm-title-line-height": "--line-height-normal",
|
|
3575
3571
|
"--sectiondivider-sm-title-letter-spacing": "--letter-spacing-normal",
|
|
3576
|
-
"--sectiondivider-sm-title-outline-width": "--border-width-4",
|
|
3577
3572
|
"--sectiondivider-sm-description-font-family": "--font-sans",
|
|
3578
3573
|
"--sectiondivider-sm-description-font-weight": "--font-weight-medium",
|
|
3579
3574
|
"--sectiondivider-sm-description-font-size": "--font-size-5xl",
|
|
@@ -3595,7 +3590,6 @@
|
|
|
3595
3590
|
"--sectiondivider-sm-description": "--text-secondary",
|
|
3596
3591
|
"--sectiondivider-sm-eyebrow": "--text-tertiary",
|
|
3597
3592
|
"--sectiondivider-sm-border": "--color-transparent",
|
|
3598
|
-
"--sectiondivider-sm-title-outline-color": "transparent",
|
|
3599
3593
|
"--sectiondivider-sm-hairline-color": "color-mix(in srgb, var(--border-brand-medium) 50%, transparent)"
|
|
3600
3594
|
}
|
|
3601
3595
|
},
|
|
@@ -4036,5 +4030,5 @@
|
|
|
4036
4030
|
}
|
|
4037
4031
|
}
|
|
4038
4032
|
},
|
|
4039
|
-
"componentSchemaVersion":
|
|
4033
|
+
"componentSchemaVersion": 26
|
|
4040
4034
|
}
|