@vectoriox/iox-ui 5.16.0 → 5.17.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/design-core/index.d.ts +27 -6
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +103 -18
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/package.json +1 -1
- package/src/design-core/design-apply.ts +28 -3
- package/src/design-core/design-settings.ts +11 -0
- package/src/design-core/flavor-seed.ts +7 -4
- package/src/design-core/token-names.ts +50 -8
package/package.json
CHANGED
|
@@ -70,6 +70,29 @@ export function missingDesignRows(existing: ReadonlyArray<{ name: string }>, cho
|
|
|
70
70
|
return designTokenRows(choices).filter(r => !have.has(r.name));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The builder-load REPAIR (DS4.1): rows the design owns and nobody touched — stamped by the current
|
|
75
|
+
* pack, light value exactly the pack's — whose dark override differs from the pack's are rewritten.
|
|
76
|
+
* iox-cms dropped `values.dark` on every write from F0 to DS4 (`values` was an implicit relation),
|
|
77
|
+
* and a revert could leave the previous apply's dark behind; both leave a row whose light half is the
|
|
78
|
+
* design's and whose dark half is not. No author edit can produce that shape — the Tokens panel edits
|
|
79
|
+
* the light value only — so rewriting it heals existing orgs without the author doing anything.
|
|
80
|
+
*/
|
|
81
|
+
export function designTokenRepairs(existing: ReadonlyArray<StoredDesignTokenRow>, choices: DesignChoices): FlavorSeedPlan['update'] {
|
|
82
|
+
const pack = flavorPackById(choices.flavorId);
|
|
83
|
+
if (!pack) return [];
|
|
84
|
+
const stamp = flavorSourceStamp(pack.id);
|
|
85
|
+
const desired = new Map(designTokenRows(choices).map(r => [r.name, r]));
|
|
86
|
+
const out: FlavorSeedPlan['update'] = [];
|
|
87
|
+
for (const row of existing ?? []) {
|
|
88
|
+
const want = desired.get(row.name);
|
|
89
|
+
if (!want || !row._id || row.source !== stamp || want.source !== stamp || row.value !== want.value) continue;
|
|
90
|
+
if ((row.values?.dark ?? '') === (want.values?.dark ?? '')) continue;
|
|
91
|
+
out.push({ id: row._id, data: { value: want.value, values: want.values ?? {}, source: stamp, ...(want.group ? { group: want.group } : {}), ...(want.tier ? { tier: want.tier } : {}) } });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
export interface SwissMigration {
|
|
74
97
|
/** The design the org now wears: Crisp, carrying the stored brand seed when it is a colour. */
|
|
75
98
|
choices: DesignChoices;
|
|
@@ -87,9 +110,11 @@ const LEGACY_ALIASES: Readonly<Record<string, string>> = SPEC_LEGACY_ALIASES;
|
|
|
87
110
|
*
|
|
88
111
|
* - The look: `planDesignApply` from Swiss to Crisp — Swiss-stamped rows untouched since Swiss seeded
|
|
89
112
|
* them (incl. F2's `surface` / `line` / `accent`) become Crisp's; edited ones are kept.
|
|
90
|
-
* - The aliases
|
|
91
|
-
* their alias reference ONLY while still machine-owned and untouched, so a node saved since F1 that
|
|
92
|
-
* reads `var(--iox-radius-control)` keeps resolving — now to Crisp's button radius.
|
|
113
|
+
* - The aliases (`SPEC_LEGACY_ALIASES`: `radius-control` / `radius-surface` / `space-stack`) are rewritten
|
|
114
|
+
* to their alias reference ONLY while still machine-owned and untouched, so a node saved since F1 that
|
|
115
|
+
* reads `var(--iox-radius-control)` keeps resolving — now to Crisp's button radius. `space-control` is
|
|
116
|
+
* NOT an alias: it is a v2 semantic token (`button-pad` reads it), and aliasing it closed a `var()`
|
|
117
|
+
* cycle; it migrates like any other Swiss row.
|
|
93
118
|
*/
|
|
94
119
|
export function planSwissMigration(existing: ReadonlyArray<StoredDesignTokenRow>, stored: StoredDesign): SwissMigration | null {
|
|
95
120
|
const swissAliasRows = (existing ?? []).filter(r => r.name in LEGACY_ALIASES && SEED_STAMP.exec(r.source ?? '')?.[1] === SWISS_FLAVOR.id);
|
|
@@ -130,6 +130,17 @@ export function packDesignChoices(flavorId: string): DesignChoices {
|
|
|
130
130
|
return resolveDesignChoices({ flavorId });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* What picking a pack card means, given the design the studio wears: the applied pack → the applied
|
|
135
|
+
* choices; another pack → that pack's defaults, CARRYING the author's chosen fonts (§5b — a font the
|
|
136
|
+
* author chose survives a pack switch unless the author changes it). Both picker homes call this.
|
|
137
|
+
*/
|
|
138
|
+
export function choicesForPack(flavorId: string, applied: DesignChoices | null | undefined): DesignChoices {
|
|
139
|
+
const pack = flavorPackById(flavorId) ?? defaultPack();
|
|
140
|
+
if (applied && resolveDesignChoices(applied).flavorId === pack.id) return resolveDesignChoices(applied);
|
|
141
|
+
return resolveDesignChoices({ flavorId: pack.id, fonts: applied?.fonts });
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
/** A stored `settings.design` config → the resolved design. No row (or no config) → Crisp's defaults. */
|
|
134
145
|
export function readDesignConfig(config: ReadonlyArray<DesignConfigEntry> | null | undefined, hasRow = !!config): StoredDesign {
|
|
135
146
|
const map = configMap(config);
|
|
@@ -66,14 +66,16 @@ export function planFlavorRevert(
|
|
|
66
66
|
snapNames.add(snap.name);
|
|
67
67
|
const live = byName.get(snap.name);
|
|
68
68
|
if (!live) {
|
|
69
|
-
plan.create.push({ name: snap.name, type: snap.type, value: snap.value, values: snap.values, source: snap.source, ...classification(snap) });
|
|
69
|
+
plan.create.push({ name: snap.name, type: snap.type, value: snap.value, ...(snap.values?.dark ? { values: snap.values } : {}), source: snap.source, ...classification(snap) });
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
72
|
const differs = live.value !== snap.value
|
|
73
73
|
|| (live.values?.dark ?? null) !== (snap.values?.dark ?? null)
|
|
74
74
|
|| (live.source ?? null) !== (snap.source ?? null);
|
|
75
|
+
// `values` is ALWAYS sent: an absent key is dropped by JSON and a `$set` then keeps the
|
|
76
|
+
// apply's dark override — a snapshot row with no dark must clear it, so it sends `{}`.
|
|
75
77
|
if (differs && live._id) {
|
|
76
|
-
plan.update.push({ id: live._id, data: { value: snap.value, values: snap.values, source: snap.source } });
|
|
78
|
+
plan.update.push({ id: live._id, data: { value: snap.value, values: snap.values ?? {}, source: snap.source } });
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
for (const row of current ?? []) {
|
|
@@ -110,7 +112,9 @@ const sameValue = (row: { value: string; values?: { dark?: string } }, e: SeedEx
|
|
|
110
112
|
* write is never touched: no `flavor:` stamp, unless it is a `user` row (a font the author CHOSE in
|
|
111
113
|
* the design) whose value is exactly what the previous design wrote — that row is the design's own
|
|
112
114
|
* choice, so a new choice may replace it. `group`/`tier` ride every write; a desired row's own
|
|
113
|
-
* `source` (a chosen font's `user`) wins over the pack stamp.
|
|
115
|
+
* `source` (a chosen font's `user`) wins over the pack stamp. A machine-owned row that already holds
|
|
116
|
+
* the desired value but another pack's stamp is RE-STAMPED (§10 C2 provenance: the stamp names the
|
|
117
|
+
* design that owns the row, so the next switch judges it against the right baseline).
|
|
114
118
|
*/
|
|
115
119
|
export function planFlavorSeed(
|
|
116
120
|
desired: ReadonlyArray<FlavorTokenDef & { source?: string }>,
|
|
@@ -140,7 +144,6 @@ export function planFlavorSeed(
|
|
|
140
144
|
if (!designOwned) { plan.kept.push(def.name); continue; }
|
|
141
145
|
const sameAsDesired = row.value === def.value && (row.values?.dark ?? '') === (def.values?.dark ?? '');
|
|
142
146
|
if (sameAsDesired && (row.source ?? '') === source) continue; // already exactly this
|
|
143
|
-
if (sameAsDesired && isSeeded(row) && source !== USER_TOKEN_SOURCE) continue; // same value, another pack's stamp — nothing to write
|
|
144
147
|
const untouched = !!old && sameValue(row, old);
|
|
145
148
|
if ((untouched || sameAsDesired) && row._id) {
|
|
146
149
|
plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source, ...classification(def) } });
|
|
@@ -32,22 +32,64 @@ export function unknownTokenReferences(css: string, alsoAllowed: Iterable<string
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const LEGACY_SCOPE = /:not\(\s*\.iox-es\s*\)/;
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
/** Comments out: block comments, and SCSS `//` line comments (never the `//` of a `url(https://…)`). */
|
|
37
|
+
const stripComments = (css: string) => String(css ?? '')
|
|
38
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
39
|
+
.replace(/(^|[^:\\])\/\/[^\n]*/g, '$1');
|
|
40
|
+
|
|
41
|
+
/** A nested SCSS selector against its parent's: `&` is the parent; no `&` means a descendant. */
|
|
42
|
+
function resolveNested(parent: string, selector: string): string {
|
|
43
|
+
if (!parent) return selector;
|
|
44
|
+
return selector.split(',').map(part => {
|
|
45
|
+
const own = part.trim();
|
|
46
|
+
return own.includes('&') ? own.replace(/&/g, parent) : `${parent} ${own}`;
|
|
47
|
+
}).join(', ');
|
|
48
|
+
}
|
|
36
49
|
|
|
37
50
|
/**
|
|
38
51
|
* Tokens read by a LEGACY rule — any rule whose selector is scoped `:not(.iox-es)` (the pre-DS2 look
|
|
39
52
|
* of a node that wears no element style, design-system §3b-bis). Such a rule must read NO token at
|
|
40
53
|
* all: a token it reads repaints a pre-DS2 page the moment the org's rows are seeded or a flavor is
|
|
41
|
-
* applied (D5). Returns `selector → names` for each offending rule; comments are ignored.
|
|
54
|
+
* applied (D5). Returns `selector → names` for each offending rule; comments are ignored. SCSS
|
|
55
|
+
* nesting is resolved (DS4.1): `.x:where(:not(.iox-es)) { &:hover { … } }` is the legacy rule
|
|
56
|
+
* `.x:where(:not(.iox-es)):hover`, and an at-rule (`@media`) between them does not break the chain.
|
|
42
57
|
*/
|
|
43
58
|
export function legacyRuleTokenReferences(css: string): Array<{ selector: string; tokens: string[] }> {
|
|
44
|
-
const text =
|
|
59
|
+
const text = stripComments(css);
|
|
45
60
|
const out: Array<{ selector: string; tokens: string[] }> = [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!LEGACY_SCOPE.test(selector))
|
|
49
|
-
const tokens = tokenReferences(
|
|
50
|
-
if (tokens.length) out.push({ selector, tokens });
|
|
61
|
+
const stack: Array<{ selector: string; body: string }> = [];
|
|
62
|
+
const report = (frame: { selector: string; body: string }) => {
|
|
63
|
+
if (!LEGACY_SCOPE.test(frame.selector)) return;
|
|
64
|
+
const tokens = tokenReferences(frame.body);
|
|
65
|
+
if (tokens.length) out.push({ selector: frame.selector, tokens });
|
|
66
|
+
};
|
|
67
|
+
let buffer = '';
|
|
68
|
+
for (const ch of text) {
|
|
69
|
+
if (ch === '{') {
|
|
70
|
+
const head = buffer.trim().replace(/\s+/g, ' ');
|
|
71
|
+
buffer = '';
|
|
72
|
+
// Declarations before a nested block belong to the parent (`color: x; &:hover {`).
|
|
73
|
+
const cut = head.lastIndexOf(';');
|
|
74
|
+
const prelude = cut >= 0 ? head.slice(cut + 1).trim() : head;
|
|
75
|
+
const parent = stack[stack.length - 1];
|
|
76
|
+
if (parent && cut >= 0) parent.body += head.slice(0, cut + 1);
|
|
77
|
+
const inherited = parent?.selector ?? '';
|
|
78
|
+
const selector = prelude.startsWith('@') ? inherited : resolveNested(inherited, prelude);
|
|
79
|
+
stack.push({ selector, body: '' });
|
|
80
|
+
} else if (ch === '}') {
|
|
81
|
+
const frame = stack.pop();
|
|
82
|
+
if (frame) {
|
|
83
|
+
frame.body += buffer;
|
|
84
|
+
report(frame);
|
|
85
|
+
}
|
|
86
|
+
buffer = '';
|
|
87
|
+
} else if (ch === ';' && stack.length) {
|
|
88
|
+
stack[stack.length - 1].body += buffer + ch;
|
|
89
|
+
buffer = '';
|
|
90
|
+
} else {
|
|
91
|
+
buffer += ch;
|
|
92
|
+
}
|
|
51
93
|
}
|
|
52
94
|
return out;
|
|
53
95
|
}
|