@vectoriox/iox-ui 5.84.0 → 5.86.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.
Files changed (47) hide show
  1. package/actions-core/index.d.ts +47 -3
  2. package/composite-core/index.d.ts +93 -15
  3. package/design-core/index.d.ts +2013 -187
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +153 -70
  5. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +307 -39
  7. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs +2222 -85
  9. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-forms-core.mjs +27 -4
  11. package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
  12. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +250 -12
  13. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  14. package/fesm2022/vectoriox-iox-ui-scene-core.mjs +101 -5
  15. package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
  16. package/fesm2022/vectoriox-iox-ui.mjs +1545 -78
  17. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  18. package/forms-core/index.d.ts +23 -4
  19. package/generation-core/index.d.ts +13 -0
  20. package/index.d.ts +563 -62
  21. package/package.json +1 -1
  22. package/scene-core/index.d.ts +32 -4
  23. package/src/actions-core/action.model.ts +1 -1
  24. package/src/actions-core/dialog-motion.ts +25 -0
  25. package/src/actions-core/dialog-opener.ts +9 -3
  26. package/src/actions-core/element-actions.ts +25 -1
  27. package/src/actions-core/popover-opener.ts +50 -0
  28. package/src/actions-core/public-api.ts +1 -0
  29. package/src/composite-core/header.ts +25 -0
  30. package/src/composite-core/public-api.ts +1 -0
  31. package/src/composite-core/slider-runtime.ts +82 -0
  32. package/src/composite-core/slider.ts +194 -27
  33. package/src/composite-core/structure.ts +14 -9
  34. package/src/design-core/flavor-spec.generated.ts +2145 -85
  35. package/src/design-core/header-behaviour.ts +76 -0
  36. package/src/design-core/public-api.ts +1 -0
  37. package/src/design-core/theme-color.ts +21 -5
  38. package/src/forms-core/submit-lifecycle.ts +30 -3
  39. package/src/generation-core/baseline-roles.ts +1 -0
  40. package/src/generation-core/compose-site.ts +8 -0
  41. package/src/generation-core/node-baselines.generated.ts +231 -11
  42. package/src/generation-core/patterns.ts +2 -0
  43. package/src/generation-core/site-spec.generated.ts +8 -0
  44. package/src/scene-core/scene.ts +18 -0
  45. package/src/scene-core/scroll-driver.ts +83 -6
  46. package/styles/iox-design.css +431 -1
  47. package/styles/iox-forms.css +29 -4
@@ -0,0 +1,76 @@
1
+ /**
2
+ * How a site's header behaves as the page scrolls (premium components T2, M4). Two traits on the header's ROOT (the
3
+ * Navigation bar, generation-core `NavRole.Root`) — edited in its "Header" section, stamped by the composer from a
4
+ * `nav.*` pattern's `header` — and ONE answer every renderer applies: the attributes below, which the shared CSS
5
+ * (`iox-design.css`) and the header runtime (`HeaderRuntime`, main entry) read. A header with neither trait renders
6
+ * exactly as before (it scrolls away with the page, its divider line as its look draws it).
7
+ *
8
+ * Framework-free: the composer (generation-core) and both renderers read it.
9
+ */
10
+
11
+ export enum HeaderBehaviour {
12
+ /** It scrolls away with the page (the default; what every header saved before M4 does). */
13
+ Scrolls = 'scrolls',
14
+ /** It stays at the top while the page scrolls under it. */
15
+ Stays = 'stays',
16
+ /** It steps aside while the page scrolls down and comes back the moment it scrolls up. */
17
+ Hides = 'hides',
18
+ }
19
+
20
+ /** What separates the header from the page once the page has moved under it (Emil 99: an edge, not a divider). */
21
+ export enum HeaderEdge {
22
+ /** A soft fade of the header's own ground over the content under it. */
23
+ Fade = 'fade',
24
+ /** A hairline in the header's border colour. */
25
+ Line = 'line',
26
+ None = 'none',
27
+ }
28
+
29
+ /** The traits, stored in the root's props (no default: a plain Container saves neither). */
30
+ export enum HeaderTrait {
31
+ Behaviour = 'headerBehaviour',
32
+ Edge = 'headerEdge',
33
+ }
34
+
35
+ export enum HeaderAttribute {
36
+ /** `stays` | `hides` on a header that stays at the top. Absent: it scrolls away. */
37
+ Behaviour = 'data-iox-header',
38
+ /** `fade` | `line` | `none`: the edge it shows once the page has scrolled. Absent: its look's own divider stays. */
39
+ Edge = 'data-iox-header-edge',
40
+ /** Written by the header runtime while a `hides` header has stepped aside. */
41
+ Hidden = 'data-iox-header-hidden',
42
+ }
43
+
44
+ /** What a composed header gets (the `nav.*` patterns' `header`, the palette's Navigation bar). */
45
+ export const COMPOSED_HEADER: Readonly<{ behaviour: HeaderBehaviour; edge: HeaderEdge }> = {
46
+ behaviour: HeaderBehaviour.Hides,
47
+ edge: HeaderEdge.Fade,
48
+ };
49
+
50
+ const BEHAVIOURS = new Set<string>(Object.values(HeaderBehaviour));
51
+ const EDGES = new Set<string>(Object.values(HeaderEdge));
52
+
53
+ /** A stored behaviour, or Scrolls for anything else. */
54
+ export function headerBehaviourOf(value: unknown): HeaderBehaviour {
55
+ return typeof value === 'string' && BEHAVIOURS.has(value) ? value as HeaderBehaviour : HeaderBehaviour.Scrolls;
56
+ }
57
+
58
+ /** A stored edge, or null (none chosen: the look's divider stays as it is). */
59
+ export function headerEdgeOf(value: unknown): HeaderEdge | null {
60
+ return typeof value === 'string' && EDGES.has(value) ? value as HeaderEdge : null;
61
+ }
62
+
63
+ /** True when the header keeps its place at the top of the screen (it stays, or it hides and comes back). */
64
+ export function headerStaysAtTop(behaviour: unknown): boolean {
65
+ return headerBehaviourOf(behaviour) !== HeaderBehaviour.Scrolls;
66
+ }
67
+
68
+ /** The attributes a header renders, from its two trait values — `{}` for a header that keeps today's behaviour. */
69
+ export function headerAttributes(behaviour: unknown, edge: unknown): Record<string, string> {
70
+ const out: Record<string, string> = {};
71
+ const b = headerBehaviourOf(behaviour);
72
+ if (b !== HeaderBehaviour.Scrolls) out[HeaderAttribute.Behaviour] = b;
73
+ const e = headerEdgeOf(edge);
74
+ if (e) out[HeaderAttribute.Edge] = e;
75
+ return out;
76
+ }
@@ -13,6 +13,7 @@ export * from './design-token.model';
13
13
  export * from './css-names';
14
14
  export * from './token-css';
15
15
  export * from './theme-color';
16
+ export * from './header-behaviour';
16
17
  export * from './flavor';
17
18
  export * from './element-styles';
18
19
  export * from './adopt';
@@ -3,6 +3,8 @@ import { DesignTokenLike } from './token-css';
3
3
 
4
4
  /** The token the browser's own chrome takes its colour from: the page's ground. */
5
5
  export const THEME_COLOR_TOKEN = 'bg';
6
+ /** …or the header's ground, when a header stays at the top of the screen (premium components T2, M4). */
7
+ export const HEADER_THEME_COLOR_TOKEN = 'nav-bg';
6
8
 
7
9
  const VAR_REF = /^var\(\s*--iox-([\w-]+)\s*(?:,\s*([^()]+))?\)$/;
8
10
  /** A colour the browser's chrome can wear: opaque hex, rgb() or hsl() — never a gradient, a mix or a translucent one. */
@@ -20,22 +22,36 @@ function resolveIn(map: ReadonlyMap<string, string>, value: string | undefined,
20
22
  return resolveIn(map, map.get(ref[1]) ?? ref[2]?.trim(), depth + 1);
21
23
  }
22
24
 
23
- function colourFor(rows: ReadonlyArray<DesignTokenLike>, dark: boolean): string | undefined {
25
+ function colourFor(rows: ReadonlyArray<DesignTokenLike>, dark: boolean, token: string): string | undefined {
24
26
  const map = new Map(rows.map(r => [r.name, dark ? (r.values?.dark ?? r.value) : r.value] as [string, string]));
25
- const colour = resolveIn(map, map.get(THEME_COLOR_TOKEN));
27
+ const colour = resolveIn(map, map.get(token));
26
28
  return colour && SOLID_COLOUR.test(colour) ? colour : undefined;
27
29
  }
28
30
 
31
+ export interface ThemeColorOptions {
32
+ /** A header stays at the top of the screen (it stays, or hides and comes back): the browser's chrome meets the
33
+ * HEADER, so it takes the header's ground when that is a solid colour (a translucent one falls back to the page's). */
34
+ header?: boolean;
35
+ }
36
+
37
+ function themeColourFor(rows: ReadonlyArray<DesignTokenLike>, dark: boolean, options: ThemeColorOptions): string | undefined {
38
+ return (options.header ? colourFor(rows, dark, HEADER_THEME_COLOR_TOKEN) : undefined) ?? colourFor(rows, dark, THEME_COLOR_TOKEN);
39
+ }
40
+
29
41
  /**
30
42
  * The `theme-color` the browser's chrome takes per colour scheme (premium components G15 — mobile-native M10): the
31
43
  * page's ground in each mode the site shows, resolved through the org's rows. A light or dark site answers its one
32
44
  * mode; `auto` answers both (the engine writes them with `media="(prefers-color-scheme: …)"`). A value that is not a
33
45
  * solid colour answers nothing — the browser keeps its own.
34
46
  */
35
- export function themeColors(rows: ReadonlyArray<DesignTokenLike> | null | undefined, mode: DesignMode | string | null | undefined = 'auto'): ThemeColors {
47
+ export function themeColors(
48
+ rows: ReadonlyArray<DesignTokenLike> | null | undefined,
49
+ mode: DesignMode | string | null | undefined = 'auto',
50
+ options: ThemeColorOptions = {},
51
+ ): ThemeColors {
36
52
  const list = rows ?? [];
37
53
  const out: ThemeColors = {};
38
- if (mode !== 'dark') { const light = colourFor(list, false); if (light) out.light = light; }
39
- if (mode !== 'light') { const dark = colourFor(list, true); if (dark) out.dark = dark; }
54
+ if (mode !== 'dark') { const light = themeColourFor(list, false, options); if (light) out.light = light; }
55
+ if (mode !== 'light') { const dark = themeColourFor(list, true, options); if (dark) out.dark = dark; }
40
56
  return out;
41
57
  }
@@ -24,6 +24,27 @@ export enum FormRenderState {
24
24
  /** The attribute every renderer writes a form's state on (the canvas switcher, both live paths). */
25
25
  export const FORM_STATE_ATTRIBUTE = 'data-iox-state';
26
26
 
27
+ /**
28
+ * What a form shows once it has sent (premium M5, N3) — the Form's "After sending" choice:
29
+ * - `message`: the success in place (M3): the fields leave, the Success block shows, Send reads "Sent" and stays so;
30
+ * - `toast`: the fields STAY, cleared for the next answer, and a toast says the form's Sent words — a newsletter in a
31
+ * footer, a form someone may send again.
32
+ */
33
+ export enum FormAfterSend {
34
+ Message = 'message',
35
+ Toast = 'toast',
36
+ }
37
+
38
+ /** The Form trait holding the choice. */
39
+ export const FORM_AFTER_SEND_TRAIT = 'afterSend';
40
+ /** The attribute every renderer writes it on (only when it is `toast` — `message` is what a form always did). */
41
+ export const FORM_AFTER_SEND_ATTRIBUTE = 'data-iox-after-send';
42
+
43
+ /** The choice a stored value names (`message` unless it says `toast`). */
44
+ export function formAfterSend(value: unknown): FormAfterSend {
45
+ return value === FormAfterSend.Toast ? FormAfterSend.Toast : FormAfterSend.Message;
46
+ }
47
+
27
48
  /** What the form does once the server has answered. */
28
49
  export interface SubmitOutcome {
29
50
  state: FormRenderState;
@@ -32,13 +53,19 @@ export interface SubmitOutcome {
32
53
  /** A success clears the answers, so they cannot be sent twice. */
33
54
  reset: boolean;
34
55
  redirect?: string;
56
+ /** Say it in a toast (the form keeps its fields and goes back to Idle). */
57
+ toast?: boolean;
35
58
  }
36
59
 
37
60
  /**
38
- * The server's answer → what the visitor sees. Only a confirmed success is Success; per-field verdicts
39
- * land on the fields; anything else (a dropped network included) is the designed Error block.
61
+ * The server's answer → what the visitor sees. Only a confirmed success is Success (or, for a form that keeps its
62
+ * fields, Idle again with a toast); per-field verdicts land on the fields; anything else (a dropped network included)
63
+ * is the designed Error block.
40
64
  */
41
- export function submitOutcome(response: SubmitResponse | null | undefined): SubmitOutcome {
65
+ export function submitOutcome(response: SubmitResponse | null | undefined, afterSend: FormAfterSend | string | null = FormAfterSend.Message): SubmitOutcome {
66
+ if (response?.ok && formAfterSend(afterSend) === FormAfterSend.Toast) {
67
+ return { state: FormRenderState.Idle, reset: true, toast: true, ...(response.redirect ? { redirect: response.redirect } : {}) };
68
+ }
42
69
  if (response?.ok) return { state: FormRenderState.Success, reset: true, ...(response.redirect ? { redirect: response.redirect } : {}) };
43
70
  if (response?.errors && Object.keys(response.errors).length) return { state: FormRenderState.Idle, errors: response.errors, reset: false };
44
71
  return { state: FormRenderState.Error, reset: false };
@@ -61,6 +61,7 @@ export const BASELINE_ROLES: Readonly<Record<string, Readonly<Record<string, Bas
61
61
  FormSuccess: { width: Behaviour, color: Look, padding: Look, borderRadius: Look, backgroundColor: Look },
62
62
  FormError: { width: Behaviour, color: Look, padding: Look, borderRadius: Look, backgroundColor: Look },
63
63
  Dialog: { width: Behaviour, padding: Look, borderRadius: Look, backgroundColor: Look, boxShadow: Look },
64
+ Popover: { width: Behaviour, padding: Look, borderRadius: Look, backgroundColor: Look, boxShadow: Look },
64
65
  // CC3: a Video fills its column at 16 / 9 — the shape the poster and the player take.
65
66
  Video: { width: Behaviour, aspectRatio: Behaviour },
66
67
  };
@@ -7,6 +7,7 @@ import {
7
7
  AccordionAutoBinds, CompositeNodeSpec, findByRole, LIGHTBOX_LABEL, SLIDE_TO_ROW, SLIDE_TYPE, buildTabsRecipe, tabLabelNodeOf,
8
8
  tabPanelImageOf, tabPanelTextOf, tabsListOf, tabsPanels, tabsPanelsOf, tabsTriggers, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, TABS_LABEL_BIND_PROP,
9
9
  TABS_TEXT_BIND_PROP, TABS_WORDS, TabsAutoBinds, BadgeTone, BADGE_TONES, buildSegmentedRecipe, CardWords, ITEM_OPEN_STATE, segmentedOptions,
10
+ setHeaderBehaviour,
10
11
  } from '@vectoriox/iox-ui/composite-core';
11
12
 
12
13
  import { EntranceMark, EntrancePart, planEntrances } from './entrances';
@@ -1210,6 +1211,12 @@ export function composeSite(plan: SitePlan, context: ComposeContext): ComposedSi
1210
1211
  const shape = patternById(plan.globals[slot])?.phoneMenu;
1211
1212
  if (shape) globals = planNavShape(globals, shape as NavShape, {}, slot).globals;
1212
1213
  }
1214
+ // Premium M4 (T2): a header pattern's behaviour as the page scrolls — the same edit the Header section makes.
1215
+ for (const slot of Object.values(GlobalSlot)) {
1216
+ const header = patternById(plan.globals[slot])?.header;
1217
+ const root = header ? globals.find(g => g.id === slot) : null;
1218
+ if (header && root) setHeaderBehaviour(root as unknown as CompositeNode, header.behaviour, header.edge ?? null);
1219
+ }
1213
1220
 
1214
1221
  return {
1215
1222
  source: clone(plan.source),
@@ -1302,6 +1309,7 @@ export function composeNavHeader(input: NavHeaderInput, options: NavHeaderOption
1302
1309
  root.globalPosition = pattern.global;
1303
1310
  const shape = pattern.phoneMenu as NavShape | undefined;
1304
1311
  const header = shape ? planNavShape([root], shape, { newId: options.newId }, root.id).globals[0] : root;
1312
+ if (pattern.header) setHeaderBehaviour(header as unknown as CompositeNode, pattern.header.behaviour, pattern.header.edge ?? null);
1305
1313
  return { header, issues };
1306
1314
  }
1307
1315
 
@@ -206,7 +206,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
206
206
  "pageRoute": "",
207
207
  "target": "_self",
208
208
  "type": "button",
209
- "ariaLabel": ""
209
+ "ariaLabel": "",
210
+ "tooltip": "",
211
+ "tooltipPlacement": "above"
210
212
  },
211
213
  "styleProps": {
212
214
  "position": "static",
@@ -302,7 +304,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
302
304
  "target",
303
305
  "type",
304
306
  "__accessibilityLabel",
305
- "ariaLabel"
307
+ "ariaLabel",
308
+ "tooltip",
309
+ "tooltipPlacement"
306
310
  ],
307
311
  "styleTraits": [
308
312
  "position",
@@ -691,7 +695,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
691
695
  "defaultOn",
692
696
  "activateOn",
693
697
  "orientation",
694
- "ariaLabel"
698
+ "ariaLabel",
699
+ "headerBehaviour",
700
+ "headerEdge"
695
701
  ],
696
702
  "styleTraits": [
697
703
  "position",
@@ -1381,7 +1387,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1381
1387
  "url": "https://",
1382
1388
  "pageRoute": "",
1383
1389
  "target": "_self",
1384
- "ariaLabel": ""
1390
+ "ariaLabel": "",
1391
+ "tooltip": "",
1392
+ "tooltipPlacement": "above"
1385
1393
  },
1386
1394
  "styleProps": {
1387
1395
  "position": "static",
@@ -1476,7 +1484,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1476
1484
  "pageRoute",
1477
1485
  "target",
1478
1486
  "__accessibilityLabel",
1479
- "ariaLabel"
1487
+ "ariaLabel",
1488
+ "tooltip",
1489
+ "tooltipPlacement"
1480
1490
  ],
1481
1491
  "styleTraits": [
1482
1492
  "position",
@@ -1581,7 +1591,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1581
1591
  "pageRoute": "",
1582
1592
  "target": "_self",
1583
1593
  "colorScheme": "default",
1584
- "ariaLabel": ""
1594
+ "ariaLabel": "",
1595
+ "tooltip": "",
1596
+ "tooltipPlacement": "above"
1585
1597
  },
1586
1598
  "styleProps": {
1587
1599
  "position": "static",
@@ -1676,7 +1688,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1676
1688
  "target",
1677
1689
  "colorScheme",
1678
1690
  "__accessibilityLabel",
1679
- "ariaLabel"
1691
+ "ariaLabel",
1692
+ "tooltip",
1693
+ "tooltipPlacement"
1680
1694
  ],
1681
1695
  "styleTraits": [
1682
1696
  "position",
@@ -3114,6 +3128,10 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3114
3128
  "slideToClickedSlide": false,
3115
3129
  "effect": "slide",
3116
3130
  "speed": 300,
3131
+ "scrollPlays": false,
3132
+ "controlsPlacement": "under",
3133
+ "enableCaption": false,
3134
+ "enableCount": false,
3117
3135
  "enableNavigation": false,
3118
3136
  "navigationColor": "#ffffff",
3119
3137
  "navigationSize": 44,
@@ -3221,7 +3239,11 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3221
3239
  "__effectsLabel",
3222
3240
  "effect",
3223
3241
  "speed",
3242
+ "scrollPlays",
3224
3243
  "__navLabel",
3244
+ "controlsPlacement",
3245
+ "enableCaption",
3246
+ "enableCount",
3225
3247
  "enableNavigation",
3226
3248
  "navigationColor",
3227
3249
  "navigationSize",
@@ -3326,7 +3348,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3326
3348
  "container": true
3327
3349
  },
3328
3350
  "SliderSlide": {
3329
- "props": {},
3351
+ "props": {
3352
+ "caption": ""
3353
+ },
3330
3354
  "styleProps": {
3331
3355
  "position": "static",
3332
3356
  "top": "auto",
@@ -3405,7 +3429,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3405
3429
  "transitionDelay": "0ms",
3406
3430
  "transitionTimingFunction": "ease"
3407
3431
  },
3408
- "traits": [],
3432
+ "traits": [
3433
+ "caption"
3434
+ ],
3409
3435
  "styleTraits": [
3410
3436
  "position",
3411
3437
  "top",
@@ -3635,7 +3661,7 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3635
3661
  "name": "Contact form",
3636
3662
  "formId": "",
3637
3663
  "feedback": "smart",
3638
- "resetAfterSubmit": false,
3664
+ "afterSend": "message",
3639
3665
  "focusFirstProblem": true,
3640
3666
  "announceFailure": true,
3641
3667
  "honeypot": true,
@@ -3731,7 +3757,7 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3731
3757
  "name",
3732
3758
  "formId",
3733
3759
  "feedback",
3734
- "resetAfterSubmit",
3760
+ "afterSend",
3735
3761
  "focusFirstProblem",
3736
3762
  "announceFailure",
3737
3763
  "__spamLabel",
@@ -5021,6 +5047,200 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
5021
5047
  ],
5022
5048
  "container": true
5023
5049
  },
5050
+ "Popover": {
5051
+ "props": {
5052
+ "placement": "below",
5053
+ "ariaLabel": "",
5054
+ "colorScheme": "default"
5055
+ },
5056
+ "styleProps": {
5057
+ "position": "static",
5058
+ "top": "auto",
5059
+ "right": "auto",
5060
+ "bottom": "auto",
5061
+ "left": "auto",
5062
+ "zIndex": "0",
5063
+ "display": "block",
5064
+ "flexDirection": "row",
5065
+ "flexWrap": "nowrap",
5066
+ "justifyContent": "flex-start",
5067
+ "alignItems": "stretch",
5068
+ "gap": "0px",
5069
+ "gridTemplateColumns": "1fr",
5070
+ "gridTemplateRows": "auto",
5071
+ "justifyItems": "stretch",
5072
+ "alignContent": "stretch",
5073
+ "alignSelf": "auto",
5074
+ "order": "0",
5075
+ "flexGrow": "0",
5076
+ "flexShrink": "1",
5077
+ "flexBasis": "auto",
5078
+ "justifySelf": "auto",
5079
+ "gridColumn": "auto",
5080
+ "gridRow": "auto",
5081
+ "width": "320px",
5082
+ "height": "auto",
5083
+ "minWidth": "auto",
5084
+ "minHeight": "auto",
5085
+ "maxWidth": "auto",
5086
+ "maxHeight": "auto",
5087
+ "aspectRatio": "auto",
5088
+ "objectFit": "cover",
5089
+ "overflow": "visible",
5090
+ "fontFamily": "Poppins, sans-serif",
5091
+ "fontSize": "16px",
5092
+ "fontWeight": "400",
5093
+ "color": "#000000",
5094
+ "lineHeight": "1.5",
5095
+ "letterSpacing": "0px",
5096
+ "textTransform": "none",
5097
+ "textAlign": "start",
5098
+ "fontStyle": "normal",
5099
+ "textDecoration": "none",
5100
+ "fontVariantNumeric": "normal",
5101
+ "WebkitTextStrokeWidth": "0px",
5102
+ "WebkitTextStrokeColor": "transparent",
5103
+ "margin": "0px 0px 0px 0px",
5104
+ "padding": "16px",
5105
+ "borderWidth": "0px",
5106
+ "borderStyle": "solid",
5107
+ "borderColor": "#CCCCCC",
5108
+ "borderRadius": "12px",
5109
+ "backgroundColor": "#ffffff",
5110
+ "opacity": "1",
5111
+ "mixBlendMode": "normal",
5112
+ "cursor": "auto",
5113
+ "boxShadow": "0 12px 32px rgba(0, 0, 0, 0.18)",
5114
+ "textShadow": "",
5115
+ "translateX": "0px",
5116
+ "translateY": "0px",
5117
+ "scaleX": "1",
5118
+ "scaleY": "1",
5119
+ "rotate": "0deg",
5120
+ "skewX": "0deg",
5121
+ "skewY": "0deg",
5122
+ "transformOrigin": "center",
5123
+ "filterBlur": "0px",
5124
+ "filterHueRotate": "0deg",
5125
+ "filterBrightness": "1",
5126
+ "filterContrast": "1",
5127
+ "filterSaturate": "1",
5128
+ "filterGrayscale": "0",
5129
+ "filterSepia": "0",
5130
+ "filterInvert": "0",
5131
+ "backdropFilter": "none",
5132
+ "backdropBlur": "0px",
5133
+ "backdropBrightness": "1",
5134
+ "backdropContrast": "1",
5135
+ "backdropSaturate": "1",
5136
+ "clipPath": "none",
5137
+ "transitionProperty": "",
5138
+ "transitionDuration": "0ms",
5139
+ "transitionDelay": "0ms",
5140
+ "transitionTimingFunction": "ease"
5141
+ },
5142
+ "traits": [
5143
+ "placement",
5144
+ "__accessibilityLabel",
5145
+ "ariaLabel",
5146
+ "colorScheme"
5147
+ ],
5148
+ "styleTraits": [
5149
+ "position",
5150
+ "top",
5151
+ "right",
5152
+ "bottom",
5153
+ "left",
5154
+ "zIndex",
5155
+ "display",
5156
+ "flexDirection",
5157
+ "flexWrap",
5158
+ "justifyContent",
5159
+ "alignItems",
5160
+ "gap",
5161
+ "gridTemplateColumns",
5162
+ "gridTemplateRows",
5163
+ "justifyItems",
5164
+ "alignContent",
5165
+ "textDirection",
5166
+ "alignSelf",
5167
+ "order",
5168
+ "flexGrow",
5169
+ "flexShrink",
5170
+ "flexBasis",
5171
+ "justifySelf",
5172
+ "gridColumn",
5173
+ "gridRow",
5174
+ "width",
5175
+ "height",
5176
+ "minWidth",
5177
+ "minHeight",
5178
+ "maxWidth",
5179
+ "maxHeight",
5180
+ "aspectRatio",
5181
+ "objectFit",
5182
+ "overflow",
5183
+ "fontFamily",
5184
+ "fontSize",
5185
+ "fontWeight",
5186
+ "color",
5187
+ "lineHeight",
5188
+ "letterSpacing",
5189
+ "textTransform",
5190
+ "textAlign",
5191
+ "fontStyle",
5192
+ "textDecoration",
5193
+ "fontVariantNumeric",
5194
+ "_sep_stroke",
5195
+ "WebkitTextStrokeWidth",
5196
+ "WebkitTextStrokeColor",
5197
+ "margin",
5198
+ "padding",
5199
+ "borderWidth",
5200
+ "borderStyle",
5201
+ "borderColor",
5202
+ "borderRadius",
5203
+ "backgroundColor",
5204
+ "opacity",
5205
+ "mixBlendMode",
5206
+ "cursor",
5207
+ "_sep_shadow",
5208
+ "boxShadow",
5209
+ "textShadow",
5210
+ "_sep_transform",
5211
+ "translateX",
5212
+ "translateY",
5213
+ "scaleX",
5214
+ "scaleY",
5215
+ "rotate",
5216
+ "skewX",
5217
+ "skewY",
5218
+ "transformOrigin",
5219
+ "_sep_filter",
5220
+ "filterBlur",
5221
+ "filterHueRotate",
5222
+ "filterBrightness",
5223
+ "filterContrast",
5224
+ "filterSaturate",
5225
+ "filterGrayscale",
5226
+ "filterSepia",
5227
+ "filterInvert",
5228
+ "_sep_backdrop",
5229
+ "backdropFilter",
5230
+ "backdropBlur",
5231
+ "backdropBrightness",
5232
+ "backdropContrast",
5233
+ "backdropSaturate",
5234
+ "_sep_clip",
5235
+ "clipPath",
5236
+ "_sep_transition",
5237
+ "transitionProperty",
5238
+ "transitionDuration",
5239
+ "transitionDelay",
5240
+ "transitionTimingFunction"
5241
+ ],
5242
+ "container": true
5243
+ },
5024
5244
  "Video": {
5025
5245
  "props": {
5026
5246
  "videoSource": "youtube",
@@ -153,6 +153,8 @@ export interface SitePattern {
153
153
  readonly global?: `${PatternGlobalPosition}`;
154
154
  /** A navigation pattern's phone shape (CC1): `planNavShape` adds it after the pattern is stamped. */
155
155
  readonly phoneMenu?: 'drawer' | 'bar' | 'both';
156
+ /** A navigation pattern's behaviour as the page scrolls (premium M4, T2): design-core HeaderBehaviour / HeaderEdge. */
157
+ readonly header?: { readonly behaviour: 'scrolls' | 'stays' | 'hides'; readonly edge?: 'fade' | 'line' | 'none' };
156
158
  /** SC6: a Scene section — held while the page scrolls, moved by the scroll. Only at How animated Expressive, at
157
159
  * most one per page and on at most two pages (`validateSitePlan`); it never takes the look's entrances. */
158
160
  readonly scene?: boolean;
@@ -1148,6 +1148,10 @@ export const SPEC_SITE_PATTERNS = {
1148
1148
  "label": "Navigation bar",
1149
1149
  "global": "before",
1150
1150
  "phoneMenu": "drawer",
1151
+ "header": {
1152
+ "behaviour": "hides",
1153
+ "edge": "fade"
1154
+ },
1151
1155
  "intent": "The brand on one side, the pages and one call to action on the other — the default header.",
1152
1156
  "node": {
1153
1157
  "t": "Container",
@@ -1217,6 +1221,10 @@ export const SPEC_SITE_PATTERNS = {
1217
1221
  "label": "Navigation — the name centred",
1218
1222
  "global": "before",
1219
1223
  "phoneMenu": "drawer",
1224
+ "header": {
1225
+ "behaviour": "hides",
1226
+ "edge": "fade"
1227
+ },
1220
1228
  "intent": "The name centred above a centred row of pages — when the name is the brand.",
1221
1229
  "node": {
1222
1230
  "t": "Container",
@@ -80,6 +80,24 @@ export enum SceneAttribute {
80
80
  Root = 'data-iox-scene',
81
81
  Tracks = 'data-iox-tracks',
82
82
  PhoneTracks = 'data-iox-tracks-phone',
83
+ /** A root's stage that is not a Scene's `scene.stage` part: the slider a scroll plays (premium M4). */
84
+ Stage = 'data-iox-scene-stage',
85
+ }
86
+
87
+ /**
88
+ * A SLIDER THE SCROLL PLAYS (premium components M4 — M1.3's held gallery). Its progress rests on each picture and changes
89
+ * only over the middle share of each step, so every picture arrives and stays a moment while the page holds it: `p` is
90
+ * the scroll's progress through the runway (0–1), `n` the slides; the answer is Swiper's progress (0–1).
91
+ */
92
+ export const SLIDER_HOLD_SHARE = 0.2;
93
+ /** Page px of scroll per picture, as a share of the screen's height (the runway the renderers reserve). */
94
+ export const SLIDER_SCROLL_STEP_SCREENS = 0.55;
95
+ export function heldProgress(p: number, n: number): number {
96
+ if (!(n >= 2) || !Number.isFinite(p)) return 0;
97
+ const s = clamp01(p) * (n - 1);
98
+ const i = Math.min(n - 2, Math.floor(s));
99
+ const f = clamp01((s - i - SLIDER_HOLD_SHARE) / (1 - 2 * SLIDER_HOLD_SHARE));
100
+ return round((i + smoothstep(f)) / (n - 1));
83
101
  }
84
102
 
85
103
  /** At or under this stage width a layer plays its phone tracks. The `mobile` breakpoint's width. */