@vectoriox/iox-ui 5.22.0 → 5.23.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.22.0",
3
+ "version": "5.23.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -2,7 +2,7 @@ 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 { FlavorSeedPlan, planFlavorSeed, SeedBaseline, SeedExpectation } from './flavor-seed';
5
+ import { FlavorRevertPlan, FlavorSeedPlan, planFlavorRevert, planFlavorSeed, SeedBaseline, SeedExpectation } from './flavor-seed';
6
6
  import { DesignChoices, DesignTokenRow, flavorSourceStamp, StoredDesignTokenRow } from './design-token.model';
7
7
  import { fontFamilyKey, FontFamilyToken, fontRoleReferences, parseFontFamilies } from './fonts';
8
8
  import { flavorPackById, isRetiredFlavorId, packDesignChoices, StoredDesign } from './design-settings';
@@ -65,6 +65,19 @@ export function planDesignApply(
65
65
  return planFlavorSeed(designTokenRows(next), existing, flavorSourceStamp(pack.id), designBaseline(previous));
66
66
  }
67
67
 
68
+ /**
69
+ * "Revert to previous look" over the org's rows — the ONE plan the Revert executes and its hint counts
70
+ * (`kept`: the author's edits since the apply, left as they are). The apply's record of what it wrote
71
+ * decides what an edit since is; a snapshot stored before that record existed (DS8.1) is judged against
72
+ * the rows the applied design writes — the look's own author rows (a chosen font) still go back.
73
+ * `null` when the design holds no snapshot.
74
+ */
75
+ export function planDesignRevert(current: ReadonlyArray<StoredDesignTokenRow>, design: StoredDesign): FlavorRevertPlan | null {
76
+ const snapshot = design?.revert;
77
+ if (!snapshot) return null;
78
+ return planFlavorRevert(current, snapshot.tokens, snapshot.wrote ?? designTokenRows(design.choices));
79
+ }
80
+
68
81
  /** The rows a design is missing by name — the builder-load top-up (never touches an existing row). */
69
82
  export function missingDesignRows(existing: ReadonlyArray<{ name: string }>, choices: DesignChoices): DesignTokenRow[] {
70
83
  const have = new Set((existing ?? []).map(r => r.name));
@@ -25,13 +25,21 @@ export interface FlavorRevertSnapshot {
25
25
  /** The design row's identity BEFORE the apply, restored with the tokens. */
26
26
  from: { flavorId: string | null; flavorVersion: string | null; brandSeed: string | null };
27
27
  tokens: Array<Pick<DesignTokenRow, 'name' | 'type' | 'value' | 'values' | 'source' | 'group' | 'tier'>>;
28
+ /** What the apply left in every row it wrote (`flavorSeedWrites`). A row holding something else
29
+ * since, as the author's (`source: user`), was edited AFTER the apply — the revert keeps it. */
30
+ wrote?: FlavorRevertWrite[];
28
31
  }
29
32
 
33
+ /** One row as an apply left it — the value and dark half the revert compares a live row against. */
34
+ export type FlavorRevertWrite = Pick<DesignTokenRow, 'name' | 'value' | 'values'>;
35
+
30
36
  export interface FlavorRevertPlan {
31
37
  create: DesignTokenRow[];
32
38
  update: Array<{ id: string; data: Partial<DesignTokenRow> }>;
33
39
  /** Rows the apply created (flavor-stamped, absent from the snapshot) — removed on revert. */
34
40
  remove: string[];
41
+ /** The author's edits made after the apply, left as they are (names) — what the Revert hint counts. */
42
+ kept: string[];
35
43
  }
36
44
 
37
45
  export const REVERT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
@@ -49,19 +57,30 @@ export function revertAvailable(appliedAt: number | null | undefined, now: numbe
49
57
  }
50
58
 
51
59
  /**
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.
60
+ * Restore the snapshot: snapshot rows come back to their stored value/dark/source (update when
61
+ * present, recreate when deleted); rows the apply CREATED (flavor-stamped and absent from the
62
+ * snapshot) are removed. A revert undoes the FLAVOR, not the author's week: hand-made rows born after
63
+ * the apply are not touched, and a row the author EDITED after the apply is kept (DS8: an edit makes
64
+ * the row the author's, and a pack switch keeps it — so does going back). Edited-after = `source: user`,
65
+ * differing from the snapshot, and holding something other than what the apply wrote (`wrote` — the
66
+ * apply's own author rows, a chosen font or an Element-styles edit, still go back with it). With no
67
+ * `wrote` at all, every author row that differs from the snapshot is judged edited after.
56
68
  */
57
69
  export function planFlavorRevert(
58
70
  current: ReadonlyArray<StoredDesignTokenRow>,
59
71
  snapshot: FlavorRevertSnapshot['tokens'],
72
+ wrote?: ReadonlyArray<FlavorRevertWrite> | null,
60
73
  ): FlavorRevertPlan {
61
74
  const byName = new Map<string, StoredDesignTokenRow>();
62
75
  for (const row of current ?? []) byName.set(row.name, row);
76
+ const written = new Map((wrote ?? []).map(w => [w.name, w]));
77
+ const editedAfter = (live: StoredDesignTokenRow) => {
78
+ if (live.source !== USER_TOKEN_SOURCE) return false;
79
+ const w = written.get(live.name);
80
+ return !w || !sameValue(live, { value: w.value, dark: w.values?.dark });
81
+ };
63
82
  const snapNames = new Set<string>();
64
- const plan: FlavorRevertPlan = { create: [], update: [], remove: [] };
83
+ const plan: FlavorRevertPlan = { create: [], update: [], remove: [], kept: [] };
65
84
  for (const snap of snapshot ?? []) {
66
85
  snapNames.add(snap.name);
67
86
  const live = byName.get(snap.name);
@@ -72,18 +91,33 @@ export function planFlavorRevert(
72
91
  const differs = live.value !== snap.value
73
92
  || (live.values?.dark ?? null) !== (snap.values?.dark ?? null)
74
93
  || (live.source ?? null) !== (snap.source ?? null);
94
+ if (!differs || !live._id) continue;
95
+ if (editedAfter(live)) { plan.kept.push(live.name); continue; }
75
96
  // `values` is ALWAYS sent: an absent key is dropped by JSON and a `$set` then keeps the
76
97
  // apply's dark override — a snapshot row with no dark must clear it, so it sends `{}`.
77
- if (differs && live._id) {
78
- plan.update.push({ id: live._id, data: { value: snap.value, values: snap.values ?? {}, source: snap.source } });
79
- }
98
+ plan.update.push({ id: live._id, data: { value: snap.value, values: snap.values ?? {}, source: snap.source } });
80
99
  }
81
100
  for (const row of current ?? []) {
82
- if (!snapNames.has(row.name) && row._id && isSeeded(row)) plan.remove.push(row._id);
101
+ if (snapNames.has(row.name) || !row._id) continue;
102
+ if (isSeeded(row)) plan.remove.push(row._id);
103
+ else if (written.has(row.name) && editedAfter(row)) plan.kept.push(row.name);
83
104
  }
84
105
  return plan;
85
106
  }
86
107
 
108
+ /** What a seed plan leaves in each row it writes, by name — the snapshot's `wrote`. */
109
+ export function flavorSeedWrites(plan: FlavorSeedPlan, existing: ReadonlyArray<StoredDesignTokenRow>): FlavorRevertWrite[] {
110
+ const byId = new Map((existing ?? []).filter(r => r._id).map(r => [r._id as string, r]));
111
+ const out: FlavorRevertWrite[] = (plan?.create ?? []).map(r => ({ name: r.name, value: r.value, ...(r.values ? { values: r.values } : {}) }));
112
+ for (const u of plan?.update ?? []) {
113
+ const row = byId.get(u.id);
114
+ if (!row) continue;
115
+ const values = u.data.values ?? row.values;
116
+ out.push({ name: row.name, value: u.data.value ?? row.value, ...(values ? { values } : {}) });
117
+ }
118
+ return out;
119
+ }
120
+
87
121
  /** What a previous seed wrote for one row — the "untouched" comparison base. */
88
122
  export interface SeedExpectation {
89
123
  value: string;