@vectoriox/iox-ui 5.42.0 → 5.44.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.
@@ -1,13 +1,14 @@
1
1
  import { deriveBrandTokens } from './brand';
2
- import { SPEC_LEGACY_ALIASES } from './flavor-spec.generated';
2
+ import { FLAVOR_SPEC_VERSION, SPEC_LEGACY_ALIASES } from './flavor-spec.generated';
3
3
  import { SWISS_FLAVOR } from './flavor';
4
4
  import { buildFlavorTokens, legacyFlavorTokens } from './flavor-tokens';
5
5
  import {
6
- FlavorRevertPlan, FlavorSeedOptions, FlavorSeedPlan, planFlavorRevert, planFlavorSeed, SeedBaseline, SeedExpectation, untouchedSeedRow,
6
+ FlavorRevertPlan, FlavorRevertSnapshot, FlavorSeedOptions, FlavorSeedPlan, flavorSeedWrites, planFlavorRevert, planFlavorSeed, SeedBaseline,
7
+ SeedExpectation, untouchedSeedRow,
7
8
  } from './flavor-seed';
8
9
  import { DesignChoices, DesignTokenRow, flavorSourceStamp, StoredDesignTokenRow } from './design-token.model';
9
10
  import { fontFamilyKey, FontFamilyToken, fontRoleReferences, parseFontFamilies } from './fonts';
10
- import { flavorPackById, isRetiredFlavorId, packDesignChoices, StoredDesign } from './design-settings';
11
+ import { DesignConfigEntry, DesignConfigKey, designConfigEntries, flavorPackById, isRetiredFlavorId, packDesignChoices, StoredDesign } from './design-settings';
11
12
 
12
13
  /**
13
14
  * Applying a look (design-system DS4): the COMPLETE `buildFlavorTokens` set for the chosen
@@ -125,6 +126,58 @@ export function planDesignRevert(current: ReadonlyArray<StoredDesignTokenRow>, d
125
126
  return planFlavorRevert(current, snapshot.tokens, snapshot.wrote ?? designTokenRows(design.choices));
126
127
  }
127
128
 
129
+ /** The revert snapshot an Apply stores in `settings.design` (board F7): design-core's snapshot plus the design
130
+ * row's previous config VERBATIM, so a revert restores every choice (pair, axes, mode, fonts), not only the
131
+ * flavor id and seed. */
132
+ export interface DesignRevertSnapshot extends FlavorRevertSnapshot {
133
+ fromConfig?: DesignConfigEntry[];
134
+ }
135
+
136
+ /**
137
+ * What an Apply of `plan` stores as its revert snapshot — the ONE rule the builder's Apply and the
138
+ * server-side site build (W2) share: the whole pre-apply token set, what the apply writes into each row,
139
+ * and where a revert goes back to — never a null flavor (a studio with no design row wore its design's
140
+ * defaults, so that is what it returns to). An apply that writes nothing keeps the previous snapshot:
141
+ * clobbering a useful one with "nothing changed" would be a lie.
142
+ */
143
+ export function designApplySnapshot(
144
+ previous: StoredDesign,
145
+ previousConfig: ReadonlyArray<DesignConfigEntry> | null | undefined,
146
+ existing: ReadonlyArray<StoredDesignTokenRow>,
147
+ plan: FlavorSeedPlan,
148
+ appliedAt: number,
149
+ ): DesignRevertSnapshot | null {
150
+ if (!plan.create.length && !plan.update.length) return previous.revert;
151
+ const origin: Pick<DesignRevertSnapshot, 'from' | 'fromConfig'> = previous.storedFlavorId
152
+ ? {
153
+ from: { flavorId: previous.storedFlavorId, flavorVersion: previous.flavorVersion, brandSeed: previous.storedSeed },
154
+ fromConfig: (previousConfig ?? []).filter(kv => kv?.key !== DesignConfigKey.Revert),
155
+ }
156
+ : {
157
+ from: { flavorId: previous.choices.flavorId, flavorVersion: String(FLAVOR_SPEC_VERSION), brandSeed: previous.choices.seed },
158
+ fromConfig: designConfigEntries(previous.choices, FLAVOR_SPEC_VERSION, null),
159
+ };
160
+ return {
161
+ appliedAt,
162
+ ...origin,
163
+ tokens: (existing ?? []).map(r => ({ name: r.name, type: r.type, value: r.value, values: r.values, source: r.source, group: r.group, tier: r.tier })),
164
+ wrote: flavorSeedWrites(plan, existing),
165
+ };
166
+ }
167
+
168
+ /** The config a revert puts back: the snapshot's verbatim config, else (a snapshot older than `fromConfig`)
169
+ * the identity it recorded. */
170
+ export function designRevertConfig(snapshot: DesignRevertSnapshot): DesignConfigEntry[] {
171
+ if (snapshot.fromConfig) return snapshot.fromConfig;
172
+ return [
173
+ ...(snapshot.from.flavorId ? [
174
+ { key: DesignConfigKey.FlavorId, value: snapshot.from.flavorId },
175
+ { key: DesignConfigKey.FlavorVersion, value: snapshot.from.flavorVersion ?? '' },
176
+ ] : []),
177
+ ...(snapshot.from.brandSeed ? [{ key: DesignConfigKey.BrandSeed, value: snapshot.from.brandSeed }] : []),
178
+ ];
179
+ }
180
+
128
181
  /** The rows a design is missing by name — the builder-load top-up (never touches an existing row). */
129
182
  export function missingDesignRows(existing: ReadonlyArray<{ name: string }>, choices: DesignChoices): DesignTokenRow[] {
130
183
  const have = new Set((existing ?? []).map(r => r.name));
@@ -1,6 +1,9 @@
1
1
  import { SPEC_AXES, SPEC_FONT_ROLES } from './flavor-spec.generated';
2
2
  import { parseHexColor } from './brand';
3
- import { DesignChoices, DesignMode, FlavorPackV2, FontRoleToken, PRE_TINT_DEFAULT, SPEC_FLAVORS, TintId } from './design-token.model';
3
+ import {
4
+ AnimateId, BetweenId, DesignChoices, DesignMode, DesignMotion, FlavorPackV2, FontRoleToken, PRE_MOTION, PRE_TINT_DEFAULT, ScrollId,
5
+ SPEC_FLAVORS, TintId,
6
+ } from './design-token.model';
4
7
  import type { FlavorRevertSnapshot } from './flavor-seed';
5
8
 
6
9
  /**
@@ -32,6 +35,9 @@ export enum DesignConfigKey {
32
35
  Mode = 'mode',
33
36
  Tint = 'tint',
34
37
  Fonts = 'fonts',
38
+ Scroll = 'scroll',
39
+ Animate = 'animate',
40
+ Between = 'between',
35
41
  Revert = 'revert',
36
42
  }
37
43
 
@@ -124,6 +130,16 @@ export function resolveDesignChoices(partial: Partial<DesignChoices> | null | un
124
130
  mode: (MODES.includes(asString(partial?.mode) ?? '') ? asString(partial?.mode) : pack.defaults.mode) as DesignMode,
125
131
  tint: keyOf(SPEC_AXES.tints, asString(partial?.tint)) ?? pack.defaults.tint,
126
132
  ...(fonts ? { fonts } : {}),
133
+ ...resolveDesignMotion(partial),
134
+ };
135
+ }
136
+
137
+ /** The look's motion, completed: an absent or unknown choice reads `PRE_MOTION` — what the site already does. */
138
+ export function resolveDesignMotion(partial: Readonly<Partial<Record<keyof DesignMotion, unknown>>> | null | undefined): DesignMotion {
139
+ return {
140
+ scroll: keyOf<ScrollId>(SPEC_AXES.scrolls, asString(partial?.scroll)) ?? PRE_MOTION.scroll,
141
+ animate: keyOf<AnimateId>(SPEC_AXES.animations, asString(partial?.animate)) ?? PRE_MOTION.animate,
142
+ between: keyOf<BetweenId>(SPEC_AXES.betweens, asString(partial?.between)) ?? PRE_MOTION.between,
127
143
  };
128
144
  }
129
145
 
@@ -135,12 +151,13 @@ export function packDesignChoices(flavorId: string): DesignChoices {
135
151
  /**
136
152
  * What picking a pack card means, given the design the studio wears: the applied pack → the applied
137
153
  * choices; another pack → that pack's defaults, CARRYING the author's chosen fonts (§5b — a font the
138
- * author chose survives a pack switch unless the author changes it). Both picker homes call this.
154
+ * author chose survives a pack switch unless the author changes it) and the site's motion (rev 3.1 MO-D3 —
155
+ * how it scrolls and changes page is not a pack's, so a Look pick never resets it). Both picker homes call this.
139
156
  */
140
157
  export function choicesForPack(flavorId: string, applied: DesignChoices | null | undefined): DesignChoices {
141
158
  const pack = flavorPackById(flavorId) ?? defaultPack();
142
159
  if (applied && resolveDesignChoices(applied).flavorId === pack.id) return resolveDesignChoices(applied);
143
- return resolveDesignChoices({ flavorId: pack.id, fonts: applied?.fonts });
160
+ return resolveDesignChoices({ flavorId: pack.id, fonts: applied?.fonts, ...resolveDesignMotion(applied) });
144
161
  }
145
162
 
146
163
  /**
@@ -164,6 +181,9 @@ export function readDesignConfig(config: ReadonlyArray<DesignConfigEntry> | null
164
181
  mode: map.get(DesignConfigKey.Mode) as DesignMode,
165
182
  tint: (storedTint ?? undefined) as TintId | undefined,
166
183
  fonts: map.get(DesignConfigKey.Fonts) as DesignChoices['fonts'],
184
+ scroll: map.get(DesignConfigKey.Scroll) as ScrollId,
185
+ animate: map.get(DesignConfigKey.Animate) as AnimateId,
186
+ between: map.get(DesignConfigKey.Between) as BetweenId,
167
187
  });
168
188
  return {
169
189
  choices,
@@ -180,6 +200,11 @@ export function designModeOf(config: ReadonlyArray<DesignConfigEntry> | null | u
180
200
  return readDesignConfig(config).choices.mode;
181
201
  }
182
202
 
203
+ /** The look's motion of a stored config — what the public page read hands the engine (`designMotion`). */
204
+ export function designMotionOf(config: ReadonlyArray<DesignConfigEntry> | null | undefined): DesignMotion {
205
+ return resolveDesignMotion(readDesignConfig(config).choices);
206
+ }
207
+
183
208
  /** Resolved choices (+ the revert snapshot) → the config array a settings row stores. */
184
209
  export function designConfigEntries(
185
210
  choices: DesignChoices,
@@ -198,6 +223,9 @@ export function designConfigEntries(
198
223
  { key: DesignConfigKey.PillButtons, value: String(c.pillButtons) },
199
224
  { key: DesignConfigKey.Mode, value: c.mode },
200
225
  { key: DesignConfigKey.Tint, value: c.tint },
226
+ { key: DesignConfigKey.Scroll, value: c.scroll },
227
+ { key: DesignConfigKey.Animate, value: c.animate },
228
+ { key: DesignConfigKey.Between, value: c.between },
201
229
  ...(c.fonts ? [{ key: DesignConfigKey.Fonts, value: JSON.stringify(c.fonts) }] : []),
202
230
  ...(revert ? [{ key: DesignConfigKey.Revert, value: JSON.stringify(revert) }] : []),
203
231
  ];
@@ -210,5 +238,6 @@ export function sameDesignChoices(a: DesignChoices, b: DesignChoices): boolean {
210
238
  return x.flavorId === y.flavorId && x.seed.toLowerCase() === y.seed.toLowerCase() && x.pairId === y.pairId
211
239
  && x.shape === y.shape && x.density === y.density && x.motion === y.motion
212
240
  && x.pillButtons === y.pillButtons && x.mode === y.mode && x.tint === y.tint
241
+ && x.scroll === y.scroll && x.animate === y.animate && x.between === y.between
213
242
  && JSON.stringify(x.fonts ?? {}) === JSON.stringify(y.fonts ?? {});
214
243
  }
@@ -46,6 +46,25 @@ export type TintId = keyof typeof SPEC_AXES.tints;
46
46
  export const TINT_IDS = Object.keys(SPEC_AXES.tints) as ReadonlyArray<TintId>;
47
47
  /** What a studio that stored its look BEFORE the tint existed wears: exactly the look's own palette. */
48
48
  export const PRE_TINT_DEFAULT: TintId = 'neutral';
49
+ /** How the site scrolls (AI plan rev 3.1 §3p): Normal (the browser's own) · Smooth · Extra smooth. */
50
+ export type ScrollId = keyof typeof SPEC_AXES.scrolls;
51
+ /** How much the sections move in as they scroll into view: Still · Subtle · Standard · Lively. */
52
+ export type AnimateId = keyof typeof SPEC_AXES.animations;
53
+ /** What plays between pages: None · Fade · Slide · Cinematic. */
54
+ export type BetweenId = keyof typeof SPEC_AXES.betweens;
55
+ export const SCROLL_IDS = Object.keys(SPEC_AXES.scrolls) as ReadonlyArray<ScrollId>;
56
+ export const ANIMATE_IDS = Object.keys(SPEC_AXES.animations) as ReadonlyArray<AnimateId>;
57
+ export const BETWEEN_IDS = Object.keys(SPEC_AXES.betweens) as ReadonlyArray<BetweenId>;
58
+ /** The look's motion — the three choices the runtime and the composer read from `settings.design`. */
59
+ export interface DesignMotion {
60
+ scroll: ScrollId;
61
+ animate: AnimateId;
62
+ between: BetweenId;
63
+ }
64
+ /** What a look stored BEFORE rev 3.1 wears: exactly what every live site did (Smooth · Still · Cinematic). */
65
+ export const PRE_MOTION: Readonly<DesignMotion> = SPEC_AXES.motionChoices.pre;
66
+ /** What a visitor who asks for reduced motion always gets, whatever the look chose. */
67
+ export const REDUCED_DESIGN_MOTION: Readonly<DesignMotion> = SPEC_AXES.motionChoices.reduced;
49
68
  export type SchemeId = keyof typeof SPEC_SCHEMES;
50
69
  export type FontRoleToken = (typeof SPEC_FONT_ROLES)[number]['token'];
51
70
 
@@ -63,6 +82,10 @@ export interface DesignChoices {
63
82
  * `neutral` — the palette it was applied with. */
64
83
  tint?: TintId;
65
84
  fonts?: Partial<Record<FontRoleToken, string>>;
85
+ /** Absent = `PRE_MOTION` (a row stored before rev 3.1 reads what its site already did). */
86
+ scroll?: ScrollId;
87
+ animate?: AnimateId;
88
+ between?: BetweenId;
66
89
  }
67
90
 
68
91
  export interface FlavorSpecToken {
@@ -1429,6 +1429,208 @@ export const SPEC_AXES = {
1429
1429
  "inverse-line": 0.03
1430
1430
  }
1431
1431
  }
1432
+ },
1433
+ "scrolls": {
1434
+ "normal": {
1435
+ "label": "Normal",
1436
+ "he": "רגילה",
1437
+ "hint": "The browser's own scrolling.",
1438
+ "lenis": null
1439
+ },
1440
+ "smooth": {
1441
+ "label": "Smooth",
1442
+ "he": "חלקה",
1443
+ "hint": "A short glide after each wheel turn — what every IOX site does today.",
1444
+ "lenis": {
1445
+ "duration": 1.2,
1446
+ "easing": "expo-out",
1447
+ "wheelMultiplier": 1,
1448
+ "syncTouch": false
1449
+ }
1450
+ },
1451
+ "extra": {
1452
+ "label": "Extra smooth",
1453
+ "he": "חלקה מאוד",
1454
+ "hint": "A longer, softer glide; each wheel turn moves a little less.",
1455
+ "lenis": {
1456
+ "duration": 1.8,
1457
+ "easing": "expo-out",
1458
+ "wheelMultiplier": 0.9,
1459
+ "syncTouch": false
1460
+ }
1461
+ }
1462
+ },
1463
+ "animations": {
1464
+ "still": {
1465
+ "label": "Still",
1466
+ "he": "ללא",
1467
+ "hint": "Nothing moves in as you scroll. Buttons and cards still respond to the pointer (Speed).",
1468
+ "parts": {}
1469
+ },
1470
+ "subtle": {
1471
+ "label": "Subtle",
1472
+ "he": "עדינה",
1473
+ "hint": "Each section's content fades in once, together.",
1474
+ "parts": {
1475
+ "content": {
1476
+ "type": "fadeIn",
1477
+ "duration": 500
1478
+ }
1479
+ }
1480
+ },
1481
+ "standard": {
1482
+ "label": "Standard",
1483
+ "he": "רגילה",
1484
+ "hint": "Headings rise in; cards, prices and steps follow one after another.",
1485
+ "parts": {
1486
+ "heading": {
1487
+ "type": "moveUp",
1488
+ "distance": 16,
1489
+ "duration": 500
1490
+ },
1491
+ "items": {
1492
+ "type": "moveUp",
1493
+ "distance": 16,
1494
+ "duration": 450,
1495
+ "stagger": 80,
1496
+ "cap": 6
1497
+ },
1498
+ "media": {
1499
+ "type": "fadeIn",
1500
+ "duration": 600
1501
+ }
1502
+ }
1503
+ },
1504
+ "lively": {
1505
+ "label": "Lively",
1506
+ "he": "תוססת",
1507
+ "hint": "Bigger, longer entrances; side-by-side rows slide in from their side; the hero's words fade in.",
1508
+ "parts": {
1509
+ "heroText": {
1510
+ "type": "fadeIn",
1511
+ "duration": 400
1512
+ },
1513
+ "heading": {
1514
+ "type": "moveUp",
1515
+ "distance": 24,
1516
+ "duration": 600
1517
+ },
1518
+ "items": {
1519
+ "type": "moveUp",
1520
+ "distance": 24,
1521
+ "duration": 600,
1522
+ "stagger": 100,
1523
+ "cap": 8
1524
+ },
1525
+ "media": {
1526
+ "type": "fadeIn",
1527
+ "duration": 700
1528
+ },
1529
+ "rowMedia": {
1530
+ "type": "moveLeft",
1531
+ "mirror": "moveRight",
1532
+ "distance": 24,
1533
+ "duration": 600
1534
+ }
1535
+ }
1536
+ }
1537
+ },
1538
+ "animationFamilies": {
1539
+ "nav": [],
1540
+ "footer": [],
1541
+ "hero": [
1542
+ "heroText"
1543
+ ],
1544
+ "header": [
1545
+ "heading"
1546
+ ],
1547
+ "detail": [
1548
+ "heading",
1549
+ "media"
1550
+ ],
1551
+ "proof": [
1552
+ "content",
1553
+ "heading",
1554
+ "items"
1555
+ ],
1556
+ "feature": [
1557
+ "content",
1558
+ "heading",
1559
+ "items",
1560
+ "media",
1561
+ "rowMedia"
1562
+ ],
1563
+ "collection": [
1564
+ "content",
1565
+ "heading",
1566
+ "items"
1567
+ ],
1568
+ "story": [
1569
+ "content",
1570
+ "heading",
1571
+ "media"
1572
+ ],
1573
+ "cta": [
1574
+ "content",
1575
+ "heading"
1576
+ ],
1577
+ "contact": [
1578
+ "content",
1579
+ "heading"
1580
+ ],
1581
+ "faq": [
1582
+ "content",
1583
+ "heading"
1584
+ ],
1585
+ "pricing": [
1586
+ "content",
1587
+ "heading",
1588
+ "items"
1589
+ ]
1590
+ },
1591
+ "betweens": {
1592
+ "none": {
1593
+ "label": "None",
1594
+ "he": "ללא",
1595
+ "hint": "The next page simply appears.",
1596
+ "transition": "none"
1597
+ },
1598
+ "fade": {
1599
+ "label": "Fade",
1600
+ "he": "דהייה",
1601
+ "hint": "The page fades into the next.",
1602
+ "transition": "fade",
1603
+ "feel": "snappy"
1604
+ },
1605
+ "slide": {
1606
+ "label": "Slide",
1607
+ "he": "החלקה",
1608
+ "hint": "The next page pushes this one aside (from the left on a Hebrew site). On phones: Fade.",
1609
+ "transition": "move-left",
1610
+ "rtl": "move-right",
1611
+ "feel": "smooth",
1612
+ "phone": "fade"
1613
+ },
1614
+ "cinematic": {
1615
+ "label": "Cinematic",
1616
+ "he": "קולנועית",
1617
+ "hint": "This page sinks back while the next rises over it. On phones: Fade.",
1618
+ "transition": "parallax-up",
1619
+ "feel": "cinematic",
1620
+ "phone": "fade"
1621
+ }
1622
+ },
1623
+ "motionChoices": {
1624
+ "pre": {
1625
+ "scroll": "smooth",
1626
+ "animate": "still",
1627
+ "between": "cinematic"
1628
+ },
1629
+ "reduced": {
1630
+ "scroll": "normal",
1631
+ "animate": "still",
1632
+ "between": "none"
1633
+ }
1432
1634
  }
1433
1635
  } as const;
1434
1636
 
@@ -3,7 +3,7 @@ import { flavorColours, FlavorColours, preTintAccentGrounds } from './brand-neut
3
3
  import { fontStackWithout, formatFontFamily } from './fonts';
4
4
  import { SPEC_AXES } from './flavor-spec.generated';
5
5
  import {
6
- DesignChoices, DesignTokenRow, FlavorPackV2, FlavorSpecToken, flavorSourceStamp, USER_TOKEN_SOURCE,
6
+ DesignChoices, DesignTokenRow, FlavorPackV2, FlavorSpecToken, flavorSourceStamp, PRE_MOTION, USER_TOKEN_SOURCE,
7
7
  } from './design-token.model';
8
8
 
9
9
  /**
@@ -37,6 +37,7 @@ export function defaultDesignChoices(pack: FlavorPackV2): DesignChoices {
37
37
  seed: pack.seed,
38
38
  pairId: pack.pairs[0]?.id ?? '',
39
39
  ...pack.defaults,
40
+ ...PRE_MOTION,
40
41
  };
41
42
  }
42
43
 
@@ -37,7 +37,10 @@ export const SPEC_SITE_PLAN_SHAPE = {
37
37
  "pillButtons": "boolean",
38
38
  "mode": "'light' | 'dark' | 'auto'",
39
39
  "tint?": "string — axes.tints (DS9 neutral tint)",
40
- "fonts?": "object — uploaded font families per role (DS5)"
40
+ "fonts?": "object — uploaded font families per role (DS5)",
41
+ "scroll?": "string — axes.scrolls: how the site scrolls (rev 3.1; absent = axes.motionChoices.pre)",
42
+ "animate?": "string — axes.animations: how much the sections move in as they scroll into view",
43
+ "between?": "string — axes.betweens: what plays between pages"
41
44
  },
42
45
  "contentType": {
43
46
  "name": "string — unique",
@@ -200,6 +200,9 @@ function validateDesign(c: Collector, design: unknown): void {
200
200
  axis('density', SPEC_AXES.densities);
201
201
  axis('motion', SPEC_AXES.motions);
202
202
  axis('tint', SPEC_AXES.tints);
203
+ axis('scroll', SPEC_AXES.scrolls);
204
+ axis('animate', SPEC_AXES.animations);
205
+ axis('between', SPEC_AXES.betweens);
203
206
  if (design['mode'] !== undefined && !(SPEC_AXES.modes as ReadonlyArray<unknown>).includes(design['mode'])) bad('mode', `must be one of ${SPEC_AXES.modes.join(', ')}`);
204
207
  if (design['pillButtons'] !== undefined && typeof design['pillButtons'] !== 'boolean') bad('pillButtons', 'must be a boolean');
205
208
  if (design['fonts'] !== undefined && !isObject(design['fonts'])) bad('fonts', 'must be an object');