@vectoriox/iox-ui 5.23.0 → 5.24.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/package.json
CHANGED
|
@@ -2,7 +2,9 @@ import { deriveBrandTokens } from './brand';
|
|
|
2
2
|
import { SPEC_LEGACY_ALIASES } from './flavor-spec.generated';
|
|
3
3
|
import { SWISS_FLAVOR } from './flavor';
|
|
4
4
|
import { buildFlavorTokens } from './flavor-tokens';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
FlavorRevertPlan, FlavorSeedOptions, FlavorSeedPlan, planFlavorRevert, planFlavorSeed, SeedBaseline, SeedExpectation, untouchedSeedRow,
|
|
7
|
+
} from './flavor-seed';
|
|
6
8
|
import { DesignChoices, DesignTokenRow, flavorSourceStamp, StoredDesignTokenRow } from './design-token.model';
|
|
7
9
|
import { fontFamilyKey, FontFamilyToken, fontRoleReferences, parseFontFamilies } from './fonts';
|
|
8
10
|
import { flavorPackById, isRetiredFlavorId, packDesignChoices, StoredDesign } from './design-settings';
|
|
@@ -37,32 +39,64 @@ function swissExpectations(seed: string | null): Map<string, SeedExpectation> {
|
|
|
37
39
|
return expectations(rows);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** What a v2 pack writes at its OWN defaults, by pack id — computed once per id per caller. */
|
|
43
|
+
function packDefaultsLookup(): (packId: string) => Map<string, SeedExpectation> | undefined {
|
|
44
|
+
const cache = new Map<string, Map<string, SeedExpectation> | undefined>();
|
|
45
|
+
return packId => {
|
|
46
|
+
if (!cache.has(packId)) {
|
|
47
|
+
const pack = isRetiredFlavorId(packId) ? undefined : flavorPackById(packId);
|
|
48
|
+
cache.set(packId, pack && pack.id === packId ? expectations(designTokenRows(packDesignChoices(pack.id))) : undefined);
|
|
49
|
+
}
|
|
50
|
+
return cache.get(packId);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* What the PREVIOUS design wrote, as a baseline keyed by the row's own stamp: a row Swiss stamped is
|
|
42
56
|
* judged against what Swiss wrote, every other row against `buildFlavorTokens` of the stored choices
|
|
43
57
|
* (for an org with no design row, Crisp's defaults — exactly what DS2's interim seed wrote). In a
|
|
44
58
|
* Swiss org the only v2 rows are that interim seed's, so they are judged against Crisp's DEFAULTS,
|
|
45
59
|
* never against the brand-seeded Crisp the migration is about to write.
|
|
60
|
+
*
|
|
61
|
+
* A row a v2 pack stamped is ALSO untouched while it holds what that pack writes at its own defaults —
|
|
62
|
+
* what DS2's interim seed, a first run and every pack card at its defaults wrote. Without it, one row
|
|
63
|
+
* kept for any reason (a dark half iox-cms dropped, `untouchedSeedRow`) was judged against a design that
|
|
64
|
+
* had moved on, apply after apply, and stayed on the old value for good: the studio's brand colour
|
|
65
|
+
* reached the design row and never its accent.
|
|
46
66
|
*/
|
|
47
67
|
export function designBaseline(previous: StoredDesign): SeedBaseline {
|
|
48
68
|
const retired = isRetiredFlavorId(previous.storedFlavorId);
|
|
49
69
|
const current = expectations(designTokenRows(retired ? packDesignChoices(previous.choices.flavorId) : previous.choices));
|
|
50
70
|
const swiss = swissExpectations(retired ? previous.storedSeed : null);
|
|
71
|
+
const packDefaults = packDefaultsLookup();
|
|
51
72
|
return (row: StoredDesignTokenRow) => {
|
|
52
73
|
const stampedBy = SEED_STAMP.exec(row.source ?? '')?.[1];
|
|
53
|
-
|
|
74
|
+
if (stampedBy === SWISS_FLAVOR.id) return swiss.get(row.name);
|
|
75
|
+
const ownDefault = stampedBy ? packDefaults(stampedBy)?.get(row.name) : undefined;
|
|
76
|
+
return [current.get(row.name), ownDefault].filter((e): e is SeedExpectation => !!e);
|
|
54
77
|
};
|
|
55
78
|
}
|
|
56
79
|
|
|
57
|
-
/**
|
|
80
|
+
/** Options of an apply. */
|
|
81
|
+
export interface DesignApplyOptions {
|
|
82
|
+
/** The kept rows the author asked to take the look's values too (`FlavorSeedOptions.reclaim`). */
|
|
83
|
+
reclaim?: FlavorSeedOptions['reclaim'];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Apply `next` over the org's rows, judged against `previous` — the plan FlavorApplyService runs. The
|
|
88
|
+
* plan's `kept` names the author's rows the look leaves alone; the Design card lists them before Apply,
|
|
89
|
+
* and `options.reclaim` writes the look's value into the ones the author hands back.
|
|
90
|
+
*/
|
|
58
91
|
export function planDesignApply(
|
|
59
92
|
existing: ReadonlyArray<StoredDesignTokenRow>,
|
|
60
93
|
previous: StoredDesign,
|
|
61
94
|
next: DesignChoices,
|
|
95
|
+
options: DesignApplyOptions = {},
|
|
62
96
|
): FlavorSeedPlan {
|
|
63
97
|
const pack = flavorPackById(next.flavorId);
|
|
64
98
|
if (!pack) return { create: [], update: [], kept: [] };
|
|
65
|
-
return planFlavorSeed(designTokenRows(next), existing, flavorSourceStamp(pack.id), designBaseline(previous));
|
|
99
|
+
return planFlavorSeed(designTokenRows(next), existing, flavorSourceStamp(pack.id), designBaseline(previous), { reclaim: options.reclaim });
|
|
66
100
|
}
|
|
67
101
|
|
|
68
102
|
/**
|
|
@@ -85,23 +119,34 @@ export function missingDesignRows(existing: ReadonlyArray<{ name: string }>, cho
|
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
/**
|
|
88
|
-
* The builder-load REPAIR (DS4.1): rows the design
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* the
|
|
122
|
+
* The builder-load REPAIR (DS4.1): rows the design writes that are the machine's and nobody touched,
|
|
123
|
+
* yet hold something other than the design's value, are rewritten — so an org heals on its next load
|
|
124
|
+
* with no author action. Two shapes:
|
|
125
|
+
* - stamped by the current pack, light value exactly the pack's, dark half not: iox-cms dropped
|
|
126
|
+
* `values.dark` on every write from F0 to DS4 (`values` was an implicit relation), and a revert could
|
|
127
|
+
* leave the previous apply's dark behind;
|
|
128
|
+
* - stamped by a v2 pack and still holding what THAT pack writes at its own defaults (its dark half
|
|
129
|
+
* possibly lost the same way) while the stored design says otherwise: an apply that judged the row
|
|
130
|
+
* edited (before 5.24.0, a lost dark half did) left it on the default — the six brand rows of a studio
|
|
131
|
+
* whose brand colour never reached its accent.
|
|
132
|
+
* No author edit produces either shape — the Tokens panel edited the light value only, and since DS8 an
|
|
133
|
+
* edit makes the row `source: user`, which this never touches.
|
|
94
134
|
*/
|
|
95
135
|
export function designTokenRepairs(existing: ReadonlyArray<StoredDesignTokenRow>, choices: DesignChoices): FlavorSeedPlan['update'] {
|
|
96
136
|
const pack = flavorPackById(choices.flavorId);
|
|
97
137
|
if (!pack) return [];
|
|
98
138
|
const stamp = flavorSourceStamp(pack.id);
|
|
99
139
|
const desired = new Map(designTokenRows(choices).map(r => [r.name, r]));
|
|
140
|
+
const packDefaults = packDefaultsLookup();
|
|
100
141
|
const out: FlavorSeedPlan['update'] = [];
|
|
101
142
|
for (const row of existing ?? []) {
|
|
102
143
|
const want = desired.get(row.name);
|
|
103
|
-
if (!want || !row._id ||
|
|
104
|
-
if ((row.values?.dark ?? '') === (want.values?.dark ?? '')) continue;
|
|
144
|
+
if (!want || !row._id || want.source !== stamp) continue;
|
|
145
|
+
if (row.value === want.value && (row.values?.dark ?? '') === (want.values?.dark ?? '') && row.source === stamp) continue;
|
|
146
|
+
const stampedBy = SEED_STAMP.exec(row.source ?? '')?.[1];
|
|
147
|
+
const darkOnly = row.source === stamp && row.value === want.value;
|
|
148
|
+
const ownDefault = stampedBy ? packDefaults(stampedBy)?.get(row.name) : undefined;
|
|
149
|
+
if (!darkOnly && !(ownDefault && untouchedSeedRow(row, [ownDefault]))) continue;
|
|
105
150
|
out.push({ id: row._id, data: { value: want.value, values: want.values ?? {}, source: stamp, ...(want.group ? { group: want.group } : {}), ...(want.tier ? { tier: want.tier } : {}) } });
|
|
106
151
|
}
|
|
107
152
|
return out;
|
|
@@ -125,19 +125,44 @@ export interface SeedExpectation {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* What
|
|
129
|
-
*
|
|
128
|
+
* What a previous seed wrote, per row: a map by name, or a function of the stored row (a baseline that
|
|
129
|
+
* depends on WHICH seed stamped the row — the Swiss → Crisp migration's case). The function may name
|
|
130
|
+
* several values: a machine row is untouched when it holds ANY of them (`designBaseline`: what the
|
|
131
|
+
* previous design wrote, and what the row's own pack writes at its defaults).
|
|
130
132
|
*/
|
|
131
133
|
export type SeedBaseline =
|
|
132
134
|
| ReadonlyMap<string, SeedExpectation>
|
|
133
|
-
| ((row: StoredDesignTokenRow) => SeedExpectation | undefined);
|
|
135
|
+
| ((row: StoredDesignTokenRow) => SeedExpectation | ReadonlyArray<SeedExpectation> | undefined);
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
/** Options of a seed pass. */
|
|
138
|
+
export interface FlavorSeedOptions {
|
|
139
|
+
/** Rows the author asked to take the look's values too ("Use the look's values for these too"):
|
|
140
|
+
* written like an untouched row instead of being kept. */
|
|
141
|
+
reclaim?: Iterable<string> | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function expectationsFor(baseline: SeedBaseline | null | undefined, row: StoredDesignTokenRow): SeedExpectation[] {
|
|
145
|
+
if (!baseline) return [];
|
|
146
|
+
const found = typeof baseline === 'function' ? baseline(row) : baseline.get(row.name);
|
|
147
|
+
if (!found) return [];
|
|
148
|
+
return (Array.isArray(found) ? found : [found]).filter(Boolean) as SeedExpectation[];
|
|
149
|
+
}
|
|
137
150
|
|
|
138
151
|
const sameValue = (row: { value: string; values?: { dark?: string } }, e: SeedExpectation) =>
|
|
139
152
|
row.value === e.value && (row.values?.dark ?? '') === (e.dark ?? '');
|
|
140
153
|
|
|
154
|
+
/**
|
|
155
|
+
* A machine-stamped row is untouched when it holds what a seed wrote — or that seed's LIGHT value with no
|
|
156
|
+
* dark half at all: iox-cms dropped every `values.dark` from F0 to DS4.1, and no author edit produces that
|
|
157
|
+
* shape (the Tokens panel edited the light value only, and since DS8 an edit makes the row `source: user`).
|
|
158
|
+
* Judging it edited kept the six brand rows of every studio seeded before DS4.1 on the default blue for
|
|
159
|
+
* good — a later brand never reached them.
|
|
160
|
+
*/
|
|
161
|
+
export function untouchedSeedRow(row: StoredDesignTokenRow, expected: ReadonlyArray<SeedExpectation>): boolean {
|
|
162
|
+
const darkLost = isSeeded(row) && !row.values?.dark;
|
|
163
|
+
return expected.some(e => sameValue(row, e) || (darkLost && row.value === e.value));
|
|
164
|
+
}
|
|
165
|
+
|
|
141
166
|
/**
|
|
142
167
|
* The seeding MERGE: desired tokens vs existing rows, deciding create / update / keep. The §10 C2
|
|
143
168
|
* rule — "an upgrade only rewrites untouched rows" — is a comparison against what the PREVIOUS
|
|
@@ -148,15 +173,18 @@ const sameValue = (row: { value: string; values?: { dark?: string } }, e: SeedEx
|
|
|
148
173
|
* choice, so a new choice may replace it. `group`/`tier` ride every write; a desired row's own
|
|
149
174
|
* `source` (a chosen font's `user`) wins over the pack stamp. A machine-owned row that already holds
|
|
150
175
|
* the desired value but another pack's stamp is RE-STAMPED (§10 C2 provenance: the stamp names the
|
|
151
|
-
* design that owns the row, so the next switch judges it against the right baseline).
|
|
176
|
+
* design that owns the row, so the next switch judges it against the right baseline). A row named in
|
|
177
|
+
* `options.reclaim` is written whatever it holds — the author chose the look's value over their own.
|
|
152
178
|
*/
|
|
153
179
|
export function planFlavorSeed(
|
|
154
180
|
desired: ReadonlyArray<FlavorTokenDef & { source?: string }>,
|
|
155
181
|
existing: ReadonlyArray<StoredDesignTokenRow>,
|
|
156
182
|
stamp: string,
|
|
157
183
|
baseline: SeedBaseline | null,
|
|
184
|
+
options: FlavorSeedOptions = {},
|
|
158
185
|
): FlavorSeedPlan {
|
|
159
186
|
const byName = new Map((existing ?? []).map(r => [r.name, r]));
|
|
187
|
+
const reclaim = new Set(options.reclaim ?? []);
|
|
160
188
|
const plan: FlavorSeedPlan = { create: [], update: [], kept: [] };
|
|
161
189
|
for (const def of desired ?? []) {
|
|
162
190
|
if (!def?.name) continue;
|
|
@@ -173,13 +201,14 @@ export function planFlavorSeed(
|
|
|
173
201
|
});
|
|
174
202
|
continue;
|
|
175
203
|
}
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
204
|
+
const olds = expectationsFor(baseline, row);
|
|
205
|
+
const reclaimed = reclaim.has(def.name);
|
|
206
|
+
const designOwned = isSeeded(row) || (row.source === USER_TOKEN_SOURCE && olds.some(o => sameValue(row, o)));
|
|
207
|
+
if (!designOwned && !reclaimed) { plan.kept.push(def.name); continue; }
|
|
179
208
|
const sameAsDesired = row.value === def.value && (row.values?.dark ?? '') === (def.values?.dark ?? '');
|
|
180
209
|
if (sameAsDesired && (row.source ?? '') === source) continue; // already exactly this
|
|
181
|
-
const untouched =
|
|
182
|
-
if ((untouched || sameAsDesired) && row._id) {
|
|
210
|
+
const untouched = untouchedSeedRow(row, olds);
|
|
211
|
+
if ((untouched || sameAsDesired || reclaimed) && row._id) {
|
|
183
212
|
plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source, ...classification(def) } });
|
|
184
213
|
} else {
|
|
185
214
|
plan.kept.push(def.name); // edited since seeding (or unverifiable) — the author's hand wins
|