@vectoriox/iox-ui 5.9.0 → 5.11.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 +8460 -0
- package/design-core/package.json +3 -0
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +10334 -0
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -0
- package/fesm2022/vectoriox-iox-ui.mjs +7 -345
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +3 -151
- package/package.json +5 -1
- package/src/design-core/brand.ts +335 -0
- package/src/design-core/css-names.ts +4 -0
- package/src/design-core/design-token.model.ts +121 -0
- package/src/design-core/flavor-seed.ts +126 -0
- package/src/design-core/flavor-spec.generated.ts +9470 -0
- package/src/design-core/flavor-tokens.ts +103 -0
- package/src/design-core/flavor.ts +137 -0
- package/src/design-core/fonts.ts +98 -0
- package/src/design-core/public-api.ts +20 -0
- package/src/design-core/spring.ts +49 -0
- package/src/design-core/token-css.ts +233 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FLAVOR_SPEC_VERSION, SPEC_AXES, SPEC_FLAVOR_PACKS, SPEC_FONT_ROLES, SPEC_SCHEMES, SPEC_TIER_IDS, SPEC_TOKEN_TYPES,
|
|
3
|
+
} from './flavor-spec.generated';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The design-token vocabulary (design-system plan §3a/§3f). Every list here is READ from the
|
|
7
|
+
* generated spec, so the contract has one source: iox-ai-guidance `flavor-spec.v1.json`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type DesignTokenType = (typeof SPEC_TOKEN_TYPES)[number];
|
|
11
|
+
export const DESIGN_TOKEN_TYPES: ReadonlyArray<DesignTokenType> = SPEC_TOKEN_TYPES;
|
|
12
|
+
|
|
13
|
+
export type DesignTokenTier = (typeof SPEC_TIER_IDS)[number];
|
|
14
|
+
export const DESIGN_TOKEN_TIERS: ReadonlyArray<DesignTokenTier> = SPEC_TIER_IDS;
|
|
15
|
+
|
|
16
|
+
/** The spec carries groups per row, not as a list — `design-token.model.spec` pins this list
|
|
17
|
+
* to exactly the set of groups the spec's rows use. */
|
|
18
|
+
export type DesignTokenGroup = 'colour' | 'type' | 'shape' | 'depth' | 'space' | 'motion' | 'components';
|
|
19
|
+
export const DESIGN_TOKEN_GROUPS: ReadonlyArray<DesignTokenGroup> =
|
|
20
|
+
['colour', 'type', 'shape', 'depth', 'space', 'motion', 'components'];
|
|
21
|
+
|
|
22
|
+
/** One `designtokens` row, transport-agnostic (stored rows add `_id`/`organization`). */
|
|
23
|
+
export interface DesignTokenRow {
|
|
24
|
+
name: string;
|
|
25
|
+
type: DesignTokenType;
|
|
26
|
+
value: string;
|
|
27
|
+
/** Per-scheme overrides. Only `dark` exists; absent means the base value everywhere. */
|
|
28
|
+
values?: { dark?: string };
|
|
29
|
+
/** Machine stamp: `flavor:<id>@<version>` on seeded rows, `user` on a font the author chose. */
|
|
30
|
+
source?: string;
|
|
31
|
+
group?: DesignTokenGroup;
|
|
32
|
+
tier?: DesignTokenTier;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A row as the planners see it: a stored row may carry its id. */
|
|
36
|
+
export interface StoredDesignTokenRow extends DesignTokenRow {
|
|
37
|
+
_id?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ShapeId = keyof typeof SPEC_AXES.shapes;
|
|
41
|
+
export type DensityId = keyof typeof SPEC_AXES.densities;
|
|
42
|
+
export type MotionId = keyof typeof SPEC_AXES.motions;
|
|
43
|
+
export type DesignMode = (typeof SPEC_AXES.modes)[number];
|
|
44
|
+
export type SchemeId = keyof typeof SPEC_SCHEMES;
|
|
45
|
+
export type FontRoleToken = (typeof SPEC_FONT_ROLES)[number]['token'];
|
|
46
|
+
|
|
47
|
+
/** Everything a studio chose about its look — the `settings.design` row, typed. */
|
|
48
|
+
export interface DesignChoices {
|
|
49
|
+
flavorId: string;
|
|
50
|
+
seed: string;
|
|
51
|
+
pairId: string;
|
|
52
|
+
shape: ShapeId;
|
|
53
|
+
density: DensityId;
|
|
54
|
+
motion: MotionId;
|
|
55
|
+
pillButtons: boolean;
|
|
56
|
+
mode: DesignMode;
|
|
57
|
+
fonts?: Partial<Record<FontRoleToken, string>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface FlavorSpecToken {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly tier: DesignTokenTier;
|
|
63
|
+
readonly type: DesignTokenType;
|
|
64
|
+
readonly group: DesignTokenGroup;
|
|
65
|
+
readonly value: string;
|
|
66
|
+
readonly dark?: string;
|
|
67
|
+
/** The flavor departs from the component token's semantic default. */
|
|
68
|
+
readonly overridden?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FontPair {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly label: string;
|
|
74
|
+
readonly tokens: Readonly<Record<string, string>>;
|
|
75
|
+
readonly families: ReadonlyArray<string>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A flavor exactly as the spec writes it. */
|
|
79
|
+
export interface FlavorPackV2 {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly personality: string;
|
|
83
|
+
readonly inspiredBy: string;
|
|
84
|
+
readonly blurb: string;
|
|
85
|
+
readonly popularFor: ReadonlyArray<string>;
|
|
86
|
+
readonly replaces?: string;
|
|
87
|
+
readonly defaults: {
|
|
88
|
+
readonly shape: ShapeId;
|
|
89
|
+
readonly density: DensityId;
|
|
90
|
+
readonly motion: MotionId;
|
|
91
|
+
readonly pillButtons: boolean;
|
|
92
|
+
readonly mode: DesignMode;
|
|
93
|
+
};
|
|
94
|
+
readonly seed: string;
|
|
95
|
+
readonly seeds: ReadonlyArray<string>;
|
|
96
|
+
readonly pairs: ReadonlyArray<FontPair>;
|
|
97
|
+
readonly tokens: ReadonlyArray<FlavorSpecToken>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The spec's flavors, typed. Assignment here is the compile-time proof that the generated
|
|
101
|
+
* data fits `FlavorPackV2`. */
|
|
102
|
+
export const SPEC_FLAVORS: ReadonlyArray<FlavorPackV2> = SPEC_FLAVOR_PACKS;
|
|
103
|
+
|
|
104
|
+
/** The stamp seeded rows carry, so an upgrade can tell the machine's rows from the author's. */
|
|
105
|
+
export function flavorSourceStamp(flavorId: string, version: number = FLAVOR_SPEC_VERSION): string {
|
|
106
|
+
return `flavor:${flavorId}@${version}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const USER_TOKEN_SOURCE = 'user';
|
|
110
|
+
|
|
111
|
+
export function isDesignTokenType(v: unknown): v is DesignTokenType {
|
|
112
|
+
return typeof v === 'string' && (DESIGN_TOKEN_TYPES as ReadonlyArray<string>).includes(v);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isDesignTokenGroup(v: unknown): v is DesignTokenGroup {
|
|
116
|
+
return typeof v === 'string' && (DESIGN_TOKEN_GROUPS as ReadonlyArray<string>).includes(v);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isDesignTokenTier(v: unknown): v is DesignTokenTier {
|
|
120
|
+
return typeof v === 'string' && (DESIGN_TOKEN_TIERS as ReadonlyArray<string>).includes(v);
|
|
121
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { FlavorTokenDef } from './flavor';
|
|
2
|
+
import { DesignTokenRow, StoredDesignTokenRow } from './design-token.model';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Seeding and reverting a look, as pure plans (flavors F2, moved here from iox-cms-client in
|
|
6
|
+
* design-system DS1 so the builder, the wizard and the server-side site build share ONE merge).
|
|
7
|
+
* Each caller applies a plan with its own transport.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const FLAVOR_SOURCE_PREFIX = 'flavor:';
|
|
11
|
+
const DEFAULT_TOKEN_TYPE = 'other';
|
|
12
|
+
|
|
13
|
+
/** One row's fate in a seed/re-seed pass (the §10 C2 merge semantics). */
|
|
14
|
+
export interface FlavorSeedPlan {
|
|
15
|
+
create: DesignTokenRow[];
|
|
16
|
+
update: { id: string; data: Partial<DesignTokenRow> }[];
|
|
17
|
+
/** Names deliberately left alone: hand-made rows, and seeded rows the author edited. */
|
|
18
|
+
kept: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** What Apply stores so the 7-day revert is honest: the WHOLE pre-apply token set plus the
|
|
22
|
+
* design identity it belonged to. */
|
|
23
|
+
export interface FlavorRevertSnapshot {
|
|
24
|
+
appliedAt: number;
|
|
25
|
+
/** The design row's identity BEFORE the apply, restored with the tokens. */
|
|
26
|
+
from: { flavorId: string | null; flavorVersion: string | null; brandSeed: string | null };
|
|
27
|
+
tokens: Array<Pick<DesignTokenRow, 'name' | 'type' | 'value' | 'values' | 'source' | 'group' | 'tier'>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FlavorRevertPlan {
|
|
31
|
+
create: DesignTokenRow[];
|
|
32
|
+
update: Array<{ id: string; data: Partial<DesignTokenRow> }>;
|
|
33
|
+
/** Rows the apply created (flavor-stamped, absent from the snapshot) — removed on revert. */
|
|
34
|
+
remove: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const REVERT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
38
|
+
|
|
39
|
+
const isSeeded = (row: { source?: string } | null | undefined) => (row?.source ?? '').startsWith(FLAVOR_SOURCE_PREFIX);
|
|
40
|
+
|
|
41
|
+
const classification = (def: Pick<DesignTokenRow, 'group' | 'tier'>): Pick<DesignTokenRow, 'group' | 'tier'> => ({
|
|
42
|
+
...(def.group ? { group: def.group } : {}),
|
|
43
|
+
...(def.tier ? { tier: def.tier } : {}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Board F7's footer promise, as a boundary: available through the 7th day, not beyond. */
|
|
47
|
+
export function revertAvailable(appliedAt: number | null | undefined, now: number): boolean {
|
|
48
|
+
return typeof appliedAt === 'number' && appliedAt > 0 && now - appliedAt <= REVERT_WINDOW_MS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Restore the snapshot EXACTLY: snapshot rows come back to their stored value/dark/source
|
|
53
|
+
* (update when present, recreate when deleted); rows the apply CREATED (flavor-stamped and
|
|
54
|
+
* absent from the snapshot) are removed. Hand-made rows born after the apply are not ours
|
|
55
|
+
* to touch — a revert undoes the FLAVOR, not the author's week.
|
|
56
|
+
*/
|
|
57
|
+
export function planFlavorRevert(
|
|
58
|
+
current: ReadonlyArray<StoredDesignTokenRow>,
|
|
59
|
+
snapshot: FlavorRevertSnapshot['tokens'],
|
|
60
|
+
): FlavorRevertPlan {
|
|
61
|
+
const byName = new Map<string, StoredDesignTokenRow>();
|
|
62
|
+
for (const row of current ?? []) byName.set(row.name, row);
|
|
63
|
+
const snapNames = new Set<string>();
|
|
64
|
+
const plan: FlavorRevertPlan = { create: [], update: [], remove: [] };
|
|
65
|
+
for (const snap of snapshot ?? []) {
|
|
66
|
+
snapNames.add(snap.name);
|
|
67
|
+
const live = byName.get(snap.name);
|
|
68
|
+
if (!live) {
|
|
69
|
+
plan.create.push({ name: snap.name, type: snap.type, value: snap.value, values: snap.values, source: snap.source, ...classification(snap) });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const differs = live.value !== snap.value
|
|
73
|
+
|| (live.values?.dark ?? null) !== (snap.values?.dark ?? null)
|
|
74
|
+
|| (live.source ?? null) !== (snap.source ?? null);
|
|
75
|
+
if (differs && live._id) {
|
|
76
|
+
plan.update.push({ id: live._id, data: { value: snap.value, values: snap.values, source: snap.source } });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const row of current ?? []) {
|
|
80
|
+
if (!snapNames.has(row.name) && row._id && isSeeded(row)) plan.remove.push(row._id);
|
|
81
|
+
}
|
|
82
|
+
return plan;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The seeding MERGE: desired tokens vs existing rows, deciding create / update / keep. The §10 C2
|
|
87
|
+
* rule — "an upgrade only rewrites untouched rows" — is a comparison against what the PREVIOUS
|
|
88
|
+
* seed wrote (`oldExpected`): a seeded row whose value still equals it is the machine's and may be
|
|
89
|
+
* rewritten; any drift means the author took it over, and their hand wins. A row without a
|
|
90
|
+
* `flavor:` stamp is never touched at all. `group`/`tier` ride every write.
|
|
91
|
+
*/
|
|
92
|
+
export function planFlavorSeed(
|
|
93
|
+
desired: ReadonlyArray<FlavorTokenDef>,
|
|
94
|
+
existing: ReadonlyArray<StoredDesignTokenRow>,
|
|
95
|
+
stamp: string,
|
|
96
|
+
oldExpected: Map<string, { value: string; dark?: string }> | null,
|
|
97
|
+
): FlavorSeedPlan {
|
|
98
|
+
const byName = new Map((existing ?? []).map(r => [r.name, r]));
|
|
99
|
+
const plan: FlavorSeedPlan = { create: [], update: [], kept: [] };
|
|
100
|
+
for (const def of desired ?? []) {
|
|
101
|
+
if (!def?.name) continue;
|
|
102
|
+
const row = byName.get(def.name);
|
|
103
|
+
if (!row) {
|
|
104
|
+
plan.create.push({
|
|
105
|
+
name: def.name,
|
|
106
|
+
type: def.type ?? DEFAULT_TOKEN_TYPE,
|
|
107
|
+
value: def.value,
|
|
108
|
+
...(def.values ? { values: def.values } : {}),
|
|
109
|
+
source: stamp,
|
|
110
|
+
...classification(def),
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!isSeeded(row)) { plan.kept.push(def.name); continue; }
|
|
115
|
+
const sameAsDesired = row.value === def.value && (row.values?.dark ?? '') === (def.values?.dark ?? '');
|
|
116
|
+
if (sameAsDesired) continue; // already exactly this — nothing to write, nothing kept-against
|
|
117
|
+
const old = oldExpected?.get(def.name);
|
|
118
|
+
const untouched = !!old && row.value === old.value && (row.values?.dark ?? '') === (old.dark ?? '');
|
|
119
|
+
if (untouched && row._id) {
|
|
120
|
+
plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source: stamp, ...classification(def) } });
|
|
121
|
+
} else {
|
|
122
|
+
plan.kept.push(def.name); // edited since seeding (or unverifiable) — the author's hand wins
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return plan;
|
|
126
|
+
}
|