@vectoriox/iox-ui 5.25.0 → 5.26.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 +1153 -191
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +1680 -241
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/package.json +1 -1
- package/src/design-core/brand-neutrals.ts +236 -0
- package/src/design-core/brand.ts +24 -14
- package/src/design-core/colour-eval.ts +117 -0
- package/src/design-core/contrast-audit.ts +214 -0
- package/src/design-core/design-apply.ts +34 -14
- package/src/design-core/design-settings.ts +12 -3
- package/src/design-core/design-token.model.ts +26 -0
- package/src/design-core/flavor-fixtures.generated.ts +150 -0
- package/src/design-core/flavor-spec.generated.ts +1061 -189
- package/src/design-core/flavor-tokens.ts +36 -8
- package/src/design-core/public-api.ts +3 -0
- package/src/design-core/token-css.ts +48 -15
- package/styles/iox-design.css +16 -0
- package/styles/iox-forms.css +15 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { deriveBrandTokens } from './brand';
|
|
2
2
|
import { SPEC_LEGACY_ALIASES } from './flavor-spec.generated';
|
|
3
3
|
import { SWISS_FLAVOR } from './flavor';
|
|
4
|
-
import { buildFlavorTokens } from './flavor-tokens';
|
|
4
|
+
import { buildFlavorTokens, legacyFlavorTokens } from './flavor-tokens';
|
|
5
5
|
import {
|
|
6
6
|
FlavorRevertPlan, FlavorSeedOptions, FlavorSeedPlan, planFlavorRevert, planFlavorSeed, SeedBaseline, SeedExpectation, untouchedSeedRow,
|
|
7
7
|
} from './flavor-seed';
|
|
@@ -28,6 +28,13 @@ export function designTokenRows(choices: DesignChoices): DesignTokenRow[] {
|
|
|
28
28
|
return pack ? buildFlavorTokens(pack, { ...choices, flavorId: pack.id }) : [];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** What a design wrote BEFORE DS9 (the look's own palette, the rows DS9 changed at their old values, the
|
|
32
|
+
* accent derived against `bg` alone) — empty for an unknown pack. */
|
|
33
|
+
export function legacyDesignTokenRows(choices: DesignChoices): DesignTokenRow[] {
|
|
34
|
+
const pack = flavorPackById(choices.flavorId);
|
|
35
|
+
return pack ? legacyFlavorTokens(pack, { ...choices, flavorId: pack.id }) : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
/** What the F1/F2 Swiss seeding wrote: the four Swiss rows + the brand set derived from its seed. */
|
|
32
39
|
function swissExpectations(seed: string | null): Map<string, SeedExpectation> {
|
|
33
40
|
const rows: Array<Pick<DesignTokenRow, 'name' | 'value' | 'values'>> =
|
|
@@ -39,18 +46,22 @@ function swissExpectations(seed: string | null): Map<string, SeedExpectation> {
|
|
|
39
46
|
return expectations(rows);
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
/** What a v2 pack writes at its OWN defaults, by pack id —
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/** What a v2 pack writes at its OWN defaults, by pack id — today and before DS9 (a studio seeded then holds
|
|
50
|
+
* the old defaults) — computed once per id per caller. */
|
|
51
|
+
function packDefaultsLookup(): (packId: string) => ReadonlyArray<Map<string, SeedExpectation>> {
|
|
52
|
+
const cache = new Map<string, ReadonlyArray<Map<string, SeedExpectation>>>();
|
|
45
53
|
return packId => {
|
|
46
54
|
if (!cache.has(packId)) {
|
|
47
55
|
const pack = isRetiredFlavorId(packId) ? undefined : flavorPackById(packId);
|
|
48
|
-
|
|
56
|
+
const choices = pack && pack.id === packId ? packDesignChoices(pack.id) : null;
|
|
57
|
+
cache.set(packId, choices ? [expectations(designTokenRows(choices)), expectations(legacyDesignTokenRows(choices))] : []);
|
|
49
58
|
}
|
|
50
|
-
return cache.get(packId)
|
|
59
|
+
return cache.get(packId) as ReadonlyArray<Map<string, SeedExpectation>>;
|
|
51
60
|
};
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
const present = (list: ReadonlyArray<SeedExpectation | undefined>): SeedExpectation[] => list.filter((e): e is SeedExpectation => !!e);
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* What the PREVIOUS design wrote, as a baseline keyed by the row's own stamp: a row Swiss stamped is
|
|
56
67
|
* judged against what Swiss wrote, every other row against `buildFlavorTokens` of the stored choices
|
|
@@ -66,14 +77,16 @@ function packDefaultsLookup(): (packId: string) => Map<string, SeedExpectation>
|
|
|
66
77
|
*/
|
|
67
78
|
export function designBaseline(previous: StoredDesign): SeedBaseline {
|
|
68
79
|
const retired = isRetiredFlavorId(previous.storedFlavorId);
|
|
69
|
-
const
|
|
80
|
+
const wrote = retired ? packDesignChoices(previous.choices.flavorId) : previous.choices;
|
|
81
|
+
const current = expectations(designTokenRows(wrote));
|
|
82
|
+
const beforeTint = expectations(legacyDesignTokenRows(wrote));
|
|
70
83
|
const swiss = swissExpectations(retired ? previous.storedSeed : null);
|
|
71
84
|
const packDefaults = packDefaultsLookup();
|
|
72
85
|
return (row: StoredDesignTokenRow) => {
|
|
73
86
|
const stampedBy = SEED_STAMP.exec(row.source ?? '')?.[1];
|
|
74
87
|
if (stampedBy === SWISS_FLAVOR.id) return swiss.get(row.name);
|
|
75
|
-
const
|
|
76
|
-
return [current.get(row.name),
|
|
88
|
+
const ownDefaults = stampedBy ? packDefaults(stampedBy).map(m => m.get(row.name)) : [];
|
|
89
|
+
return present([current.get(row.name), beforeTint.get(row.name), ...ownDefaults]);
|
|
77
90
|
};
|
|
78
91
|
}
|
|
79
92
|
|
|
@@ -121,15 +134,18 @@ export function missingDesignRows(existing: ReadonlyArray<{ name: string }>, cho
|
|
|
121
134
|
/**
|
|
122
135
|
* The builder-load REPAIR (DS4.1): rows the design writes that are the machine's and nobody touched,
|
|
123
136
|
* yet hold something other than the design's value, are rewritten — so an org heals on its next load
|
|
124
|
-
* with no author action.
|
|
137
|
+
* with no author action. Three shapes:
|
|
125
138
|
* - stamped by the current pack, light value exactly the pack's, dark half not: iox-cms dropped
|
|
126
139
|
* `values.dark` on every write from F0 to DS4 (`values` was an implicit relation), and a revert could
|
|
127
140
|
* leave the previous apply's dark behind;
|
|
128
141
|
* - stamped by a v2 pack and still holding what THAT pack writes at its own defaults (its dark half
|
|
129
142
|
* possibly lost the same way) while the stored design says otherwise: an apply that judged the row
|
|
130
143
|
* 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
|
-
*
|
|
144
|
+
* whose brand colour never reached its accent;
|
|
145
|
+
* - stamped by the current pack and still holding what the design wrote BEFORE DS9
|
|
146
|
+
* (`legacyDesignTokenRows`): the contrast fixes — a 3:1 `line-strong`, a 3:1 focus ring, an
|
|
147
|
+
* `accent-text` that reads on every ground — reach a studio that applied its look before them.
|
|
148
|
+
* No author edit produces any of these shapes — the Tokens panel edited the light value only, and since DS8 an
|
|
133
149
|
* edit makes the row `source: user`, which this never touches.
|
|
134
150
|
*/
|
|
135
151
|
export function designTokenRepairs(existing: ReadonlyArray<StoredDesignTokenRow>, choices: DesignChoices): FlavorSeedPlan['update'] {
|
|
@@ -137,6 +153,7 @@ export function designTokenRepairs(existing: ReadonlyArray<StoredDesignTokenRow>
|
|
|
137
153
|
if (!pack) return [];
|
|
138
154
|
const stamp = flavorSourceStamp(pack.id);
|
|
139
155
|
const desired = new Map(designTokenRows(choices).map(r => [r.name, r]));
|
|
156
|
+
const beforeTint = expectations(legacyDesignTokenRows(choices));
|
|
140
157
|
const packDefaults = packDefaultsLookup();
|
|
141
158
|
const out: FlavorSeedPlan['update'] = [];
|
|
142
159
|
for (const row of existing ?? []) {
|
|
@@ -145,8 +162,11 @@ export function designTokenRepairs(existing: ReadonlyArray<StoredDesignTokenRow>
|
|
|
145
162
|
if (row.value === want.value && (row.values?.dark ?? '') === (want.values?.dark ?? '') && row.source === stamp) continue;
|
|
146
163
|
const stampedBy = SEED_STAMP.exec(row.source ?? '')?.[1];
|
|
147
164
|
const darkOnly = row.source === stamp && row.value === want.value;
|
|
148
|
-
const
|
|
149
|
-
|
|
165
|
+
const machineWrote = present([
|
|
166
|
+
...(stampedBy ? packDefaults(stampedBy).map(m => m.get(row.name)) : []),
|
|
167
|
+
...(stampedBy === pack.id ? [beforeTint.get(row.name)] : []),
|
|
168
|
+
]);
|
|
169
|
+
if (!darkOnly && !untouchedSeedRow(row, machineWrote)) continue;
|
|
150
170
|
out.push({ id: row._id, data: { value: want.value, values: want.values ?? {}, source: stamp, ...(want.group ? { group: want.group } : {}), ...(want.tier ? { tier: want.tier } : {}) } });
|
|
151
171
|
}
|
|
152
172
|
return out;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SPEC_AXES, SPEC_FONT_ROLES } from './flavor-spec.generated';
|
|
2
2
|
import { parseHexColor } from './brand';
|
|
3
|
-
import { DesignChoices, DesignMode, FlavorPackV2, FontRoleToken, SPEC_FLAVORS } from './design-token.model';
|
|
3
|
+
import { DesignChoices, DesignMode, FlavorPackV2, FontRoleToken, PRE_TINT_DEFAULT, SPEC_FLAVORS, TintId } from './design-token.model';
|
|
4
4
|
import type { FlavorRevertSnapshot } from './flavor-seed';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -30,6 +30,7 @@ export enum DesignConfigKey {
|
|
|
30
30
|
Motion = 'motion',
|
|
31
31
|
PillButtons = 'pillButtons',
|
|
32
32
|
Mode = 'mode',
|
|
33
|
+
Tint = 'tint',
|
|
33
34
|
Fonts = 'fonts',
|
|
34
35
|
Revert = 'revert',
|
|
35
36
|
}
|
|
@@ -121,6 +122,7 @@ export function resolveDesignChoices(partial: Partial<DesignChoices> | null | un
|
|
|
121
122
|
motion: keyOf(SPEC_AXES.motions, asString(partial?.motion)) ?? pack.defaults.motion,
|
|
122
123
|
pillButtons: asBoolean(partial?.pillButtons) ?? pack.defaults.pillButtons,
|
|
123
124
|
mode: (MODES.includes(asString(partial?.mode) ?? '') ? asString(partial?.mode) : pack.defaults.mode) as DesignMode,
|
|
125
|
+
tint: keyOf(SPEC_AXES.tints, asString(partial?.tint)) ?? pack.defaults.tint,
|
|
124
126
|
...(fonts ? { fonts } : {}),
|
|
125
127
|
};
|
|
126
128
|
}
|
|
@@ -141,9 +143,14 @@ export function choicesForPack(flavorId: string, applied: DesignChoices | null |
|
|
|
141
143
|
return resolveDesignChoices({ flavorId: pack.id, fonts: applied?.fonts });
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* A stored `settings.design` config → the resolved design. No row (or no config) → Crisp's defaults. A
|
|
148
|
+
* row stored before DS9 has no `tint`: it was applied with the look's own palette, so it reads `neutral`
|
|
149
|
+
* (never the pack's new default — the studio's rows hold the palette, and nothing may repaint them unasked).
|
|
150
|
+
*/
|
|
145
151
|
export function readDesignConfig(config: ReadonlyArray<DesignConfigEntry> | null | undefined, hasRow = !!config): StoredDesign {
|
|
146
152
|
const map = configMap(config);
|
|
153
|
+
const storedTint = asString(map.get(DesignConfigKey.Tint)) ?? (hasRow ? PRE_TINT_DEFAULT : null);
|
|
147
154
|
const storedFlavorId = asString(map.get(DesignConfigKey.FlavorId));
|
|
148
155
|
const storedSeed = asString(map.get(DesignConfigKey.BrandSeed));
|
|
149
156
|
const choices = resolveDesignChoices({
|
|
@@ -155,6 +162,7 @@ export function readDesignConfig(config: ReadonlyArray<DesignConfigEntry> | null
|
|
|
155
162
|
motion: map.get(DesignConfigKey.Motion) as DesignChoices['motion'],
|
|
156
163
|
pillButtons: asBoolean(map.get(DesignConfigKey.PillButtons)) ?? undefined,
|
|
157
164
|
mode: map.get(DesignConfigKey.Mode) as DesignMode,
|
|
165
|
+
tint: (storedTint ?? undefined) as TintId | undefined,
|
|
158
166
|
fonts: map.get(DesignConfigKey.Fonts) as DesignChoices['fonts'],
|
|
159
167
|
});
|
|
160
168
|
return {
|
|
@@ -189,6 +197,7 @@ export function designConfigEntries(
|
|
|
189
197
|
{ key: DesignConfigKey.Motion, value: c.motion },
|
|
190
198
|
{ key: DesignConfigKey.PillButtons, value: String(c.pillButtons) },
|
|
191
199
|
{ key: DesignConfigKey.Mode, value: c.mode },
|
|
200
|
+
{ key: DesignConfigKey.Tint, value: c.tint },
|
|
192
201
|
...(c.fonts ? [{ key: DesignConfigKey.Fonts, value: JSON.stringify(c.fonts) }] : []),
|
|
193
202
|
...(revert ? [{ key: DesignConfigKey.Revert, value: JSON.stringify(revert) }] : []),
|
|
194
203
|
];
|
|
@@ -200,6 +209,6 @@ export function sameDesignChoices(a: DesignChoices, b: DesignChoices): boolean {
|
|
|
200
209
|
const y = resolveDesignChoices(b);
|
|
201
210
|
return x.flavorId === y.flavorId && x.seed.toLowerCase() === y.seed.toLowerCase() && x.pairId === y.pairId
|
|
202
211
|
&& x.shape === y.shape && x.density === y.density && x.motion === y.motion
|
|
203
|
-
&& x.pillButtons === y.pillButtons && x.mode === y.mode
|
|
212
|
+
&& x.pillButtons === y.pillButtons && x.mode === y.mode && x.tint === y.tint
|
|
204
213
|
&& JSON.stringify(x.fonts ?? {}) === JSON.stringify(y.fonts ?? {});
|
|
205
214
|
}
|
|
@@ -41,6 +41,11 @@ export type ShapeId = keyof typeof SPEC_AXES.shapes;
|
|
|
41
41
|
export type DensityId = keyof typeof SPEC_AXES.densities;
|
|
42
42
|
export type MotionId = keyof typeof SPEC_AXES.motions;
|
|
43
43
|
export type DesignMode = (typeof SPEC_AXES.modes)[number];
|
|
44
|
+
/** Background tint (DS9): Neutral keeps the look's own palette; Subtle / Rich tint its neutrals with the brand. */
|
|
45
|
+
export type TintId = keyof typeof SPEC_AXES.tints;
|
|
46
|
+
export const TINT_IDS = Object.keys(SPEC_AXES.tints) as ReadonlyArray<TintId>;
|
|
47
|
+
/** What a studio that stored its look BEFORE the tint existed wears: exactly the look's own palette. */
|
|
48
|
+
export const PRE_TINT_DEFAULT: TintId = 'neutral';
|
|
44
49
|
export type SchemeId = keyof typeof SPEC_SCHEMES;
|
|
45
50
|
export type FontRoleToken = (typeof SPEC_FONT_ROLES)[number]['token'];
|
|
46
51
|
|
|
@@ -54,6 +59,9 @@ export interface DesignChoices {
|
|
|
54
59
|
motion: MotionId;
|
|
55
60
|
pillButtons: boolean;
|
|
56
61
|
mode: DesignMode;
|
|
62
|
+
/** Absent = the pack's default (`defaults.tint`). `readDesignConfig` reads a row stored before DS9 as
|
|
63
|
+
* `neutral` — the palette it was applied with. */
|
|
64
|
+
tint?: TintId;
|
|
57
65
|
fonts?: Partial<Record<FontRoleToken, string>>;
|
|
58
66
|
}
|
|
59
67
|
|
|
@@ -75,6 +83,19 @@ export interface FontPair {
|
|
|
75
83
|
readonly families: ReadonlyArray<string>;
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
/** A flavor's own colour palette (the Neutral tint): every colour token, light + the dark values that differ. */
|
|
87
|
+
export interface FlavorPalette {
|
|
88
|
+
readonly light: Readonly<Record<string, string>>;
|
|
89
|
+
readonly dark: Readonly<Record<string, string>>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** How a flavor takes the brand's tint: which neutral roles, how strongly, and its tinted paints. */
|
|
93
|
+
export interface FlavorTintConfig {
|
|
94
|
+
readonly scale: number;
|
|
95
|
+
readonly roles: ReadonlyArray<string>;
|
|
96
|
+
readonly paints?: ReadonlyArray<string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
/** A flavor exactly as the spec writes it. */
|
|
79
100
|
export interface FlavorPackV2 {
|
|
80
101
|
readonly id: string;
|
|
@@ -90,10 +111,15 @@ export interface FlavorPackV2 {
|
|
|
90
111
|
readonly motion: MotionId;
|
|
91
112
|
readonly pillButtons: boolean;
|
|
92
113
|
readonly mode: DesignMode;
|
|
114
|
+
readonly tint: TintId;
|
|
93
115
|
};
|
|
94
116
|
readonly seed: string;
|
|
95
117
|
readonly seeds: ReadonlyArray<string>;
|
|
96
118
|
readonly pairs: ReadonlyArray<FontPair>;
|
|
119
|
+
readonly palette: FlavorPalette;
|
|
120
|
+
readonly tint: FlavorTintConfig;
|
|
121
|
+
/** What the contract wrote before DS9 for the rows DS9 changed at the Neutral tint (`legacyFlavorTokens`). */
|
|
122
|
+
readonly legacy: Readonly<Record<string, { readonly value: string; readonly dark?: string }>>;
|
|
97
123
|
readonly tokens: ReadonlyArray<FlavorSpecToken>;
|
|
98
124
|
}
|
|
99
125
|
|