@vectoriox/iox-ui 5.83.1 → 5.85.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 (53) hide show
  1. package/actions-core/index.d.ts +18 -2
  2. package/composite-core/index.d.ts +93 -15
  3. package/design-core/index.d.ts +566 -4
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +18 -1
  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 +680 -15
  9. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +665 -24
  11. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  12. package/fesm2022/vectoriox-iox-ui-harness-core.mjs +39 -1
  13. package/fesm2022/vectoriox-iox-ui-harness-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 +285 -12
  17. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  18. package/generation-core/index.d.ts +414 -8
  19. package/harness-core/index.d.ts +81 -3
  20. package/index.d.ts +143 -42
  21. package/package.json +1 -1
  22. package/scene-core/index.d.ts +32 -4
  23. package/src/actions-core/dialog-motion.ts +25 -0
  24. package/src/composite-core/header.ts +25 -0
  25. package/src/composite-core/public-api.ts +1 -0
  26. package/src/composite-core/slider-runtime.ts +82 -0
  27. package/src/composite-core/slider.ts +194 -27
  28. package/src/composite-core/structure.ts +14 -9
  29. package/src/design-core/flavor-spec.generated.ts +596 -9
  30. package/src/design-core/header-behaviour.ts +76 -0
  31. package/src/design-core/public-api.ts +1 -0
  32. package/src/design-core/theme-color.ts +21 -5
  33. package/src/generation-core/ai-run.ts +10 -2
  34. package/src/generation-core/art-direction.ts +122 -0
  35. package/src/generation-core/audit-site.ts +14 -0
  36. package/src/generation-core/compose-site.ts +8 -0
  37. package/src/generation-core/node-baselines.generated.ts +17 -3
  38. package/src/generation-core/patterns.ts +2 -0
  39. package/src/generation-core/pictures.ts +464 -0
  40. package/src/generation-core/plan-wire.ts +2 -0
  41. package/src/generation-core/public-api.ts +2 -0
  42. package/src/generation-core/site-build-run.ts +3 -0
  43. package/src/generation-core/site-plan.model.ts +4 -0
  44. package/src/generation-core/site-spec.generated.ts +126 -1
  45. package/src/generation-core/stock-photos.ts +40 -10
  46. package/src/generation-core/validate-site-plan.ts +14 -0
  47. package/src/harness-core/business-types.ts +3 -1
  48. package/src/harness-core/generation-view.ts +50 -1
  49. package/src/harness-core/harness-spec.generated.ts +30 -0
  50. package/src/scene-core/scene.ts +18 -0
  51. package/src/scene-core/scroll-driver.ts +83 -6
  52. package/styles/iox-design.css +232 -1
  53. package/styles/iox-forms.css +5 -2
@@ -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
  }
@@ -27,6 +27,8 @@ export enum AiStep {
27
27
  PlanSite = 'plan',
28
28
  ChangePlan = 'planEdit',
29
29
  MakePictures = 'image',
30
+ /** M7: a small vision model looks at each made picture against its slot (one structured answer). */
31
+ CheckPictures = 'imageReview',
30
32
  NameStructures = 'scanNaming',
31
33
  }
32
34
 
@@ -38,6 +40,7 @@ export const AI_STEP_LABELS: Readonly<Record<AiStep, string>> = {
38
40
  [AiStep.PlanSite]: 'Plan the site',
39
41
  [AiStep.ChangePlan]: 'Change the plan',
40
42
  [AiStep.MakePictures]: 'Make pictures',
43
+ [AiStep.CheckPictures]: 'Check pictures',
41
44
  [AiStep.NameStructures]: 'Name repeated structures',
42
45
  };
43
46
 
@@ -53,6 +56,7 @@ export const AI_STEP_NEEDS: Readonly<Record<AiStep, ReadonlyArray<AiCapability>>
53
56
  [AiStep.PlanSite]: [AiCapability.StructuredOutput],
54
57
  [AiStep.ChangePlan]: [AiCapability.StructuredOutput],
55
58
  [AiStep.MakePictures]: [AiCapability.ImageOutput],
59
+ [AiStep.CheckPictures]: [AiCapability.StructuredOutput, AiCapability.ImageInput],
56
60
  [AiStep.NameStructures]: [AiCapability.StructuredOutput],
57
61
  };
58
62
 
@@ -91,8 +95,12 @@ export const AI_STEP_TYPICAL_TOKENS: Readonly<Record<AiStep, AiTokenCounts>> = {
91
95
  [AiStep.ReadMessage]: { input: 8_800, output: 1_500, cacheWrite: 0, cacheRead: 0 },
92
96
  [AiStep.PlanSite]: partsTotal(AiStep.PlanSite),
93
97
  [AiStep.ChangePlan]: partsTotal(AiStep.ChangePlan),
94
- // a 1 K picture is 1,120 image tokens (Google's figure for Nano Banana 2) — I-D4
95
- [AiStep.MakePictures]: { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, imageOutput: 1_120 },
98
+ // a 1 K picture is 1,120 image tokens (Google's figure for Nano Banana 2) — I-D4; the prompt (the art direction's
99
+ // preamble, the slot's subject, its frame and empty area) about 400 more in (M7)
100
+ [AiStep.MakePictures]: { input: 400, output: 0, cacheWrite: 0, cacheRead: 0, imageOutput: 1_120 },
101
+ // M7: one picture downscaled to 512 px (≈ 350 image tokens for Claude, ≈ 260 for Gemini) + the slot's question,
102
+ // and a short structured answer
103
+ [AiStep.CheckPictures]: { input: 1_200, output: 150, cacheWrite: 0, cacheRead: 0 },
96
104
  [AiStep.NameStructures]: { input: 80_000, output: 10_000, cacheWrite: 0, cacheRead: 0 },
97
105
  };
98
106
 
@@ -0,0 +1,122 @@
1
+ import { DesignChoices, parseHexColor, rgbToOklab } from '@vectoriox/iox-ui/design-core';
2
+
3
+ import { SPEC_SITE_PICTURE_NEVER, SPEC_SITE_PICTURE_WORLDS } from './site-spec.generated';
4
+
5
+ /**
6
+ * A site's ART DIRECTION (premium-components M7, premium sites PS4): what every picture made for the site shares, so its
7
+ * pictures read as one photographer's and never as a stock pool. A closed WORLD (the contract's `site.pictureWorlds`)
8
+ * gives the five parts — medium, light, grade, texture and a negative list — and ONE preamble built from them is carried
9
+ * VERBATIM at the head of every picture prompt of the site (`artDirectionPreamble`).
10
+ *
11
+ * Decided here, not by a model: the world is the business type's (harness-core `BusinessType.world`), a dark look moves it
12
+ * to the world's `dark`, and the brand's hue joins the grade. Zero tokens, the same site always gets the same direction,
13
+ * and the quality lever — each world's wording — is written once in the contract, as the proof sites' briefs were.
14
+ */
15
+
16
+ export enum PictureWorld {
17
+ Documentary = 'documentary',
18
+ HighKey = 'high-key',
19
+ LowKey = 'low-key',
20
+ HardLight = 'hard-light',
21
+ Macro = 'macro',
22
+ Architectural = 'architectural',
23
+ Nocturne = 'nocturne',
24
+ Drawing = 'drawing',
25
+ }
26
+
27
+ export const PICTURE_WORLD_IDS: ReadonlyArray<PictureWorld> = Object.values(PictureWorld);
28
+
29
+ /** One world as the contract writes it. */
30
+ export interface PictureWorldSpec {
31
+ readonly label: string;
32
+ readonly medium: string;
33
+ readonly light: string;
34
+ readonly grade: string;
35
+ readonly texture: string;
36
+ readonly negative: ReadonlyArray<string>;
37
+ /** Where a dark look's pictures go. */
38
+ readonly dark: string;
39
+ }
40
+
41
+ export const PICTURE_WORLDS = SPEC_SITE_PICTURE_WORLDS as unknown as Readonly<Record<PictureWorld, PictureWorldSpec>>;
42
+
43
+ /** What no picture of any site shows — every world adds its own after these. */
44
+ export const PICTURE_NEVER: ReadonlyArray<string> = SPEC_SITE_PICTURE_NEVER;
45
+
46
+ /** The art direction a plan carries (`plan.artDirection`): the world and its five parts as this site wears them. */
47
+ export interface ArtDirection {
48
+ readonly world: `${PictureWorld}`;
49
+ readonly medium: string;
50
+ readonly light: string;
51
+ readonly grade: string;
52
+ readonly texture: string;
53
+ readonly negative: ReadonlyArray<string>;
54
+ }
55
+
56
+ /** The five parts every art direction must carry, in the order the preamble says them. */
57
+ export const ART_DIRECTION_PARTS = ['medium', 'light', 'grade', 'texture', 'negative'] as const;
58
+
59
+ export function isPictureWorld(value: unknown): value is PictureWorld {
60
+ return typeof value === 'string' && (PICTURE_WORLD_IDS as ReadonlyArray<string>).includes(value);
61
+ }
62
+
63
+ /** OKLCH hue bands (degrees, upper bound) → the word a grade says; below the chroma floor a brand is neutral. */
64
+ const HUE_WORDS: ReadonlyArray<readonly [number, string]> = [
65
+ [12, 'rose'], [33, 'red'], [55, 'terracotta orange'], [80, 'warm amber'], [110, 'golden yellow'], [135, 'olive green'],
66
+ [170, 'green'], [215, 'teal'], [255, 'blue'], [285, 'deep blue'], [315, 'violet'], [345, 'plum'], [360, 'rose'],
67
+ ];
68
+ const NEUTRAL_CHROMA = 0.04;
69
+ const RADIANS_TO_DEGREES = 180 / Math.PI;
70
+ const FULL_TURN = 360;
71
+
72
+ /** The word for a brand colour's hue (`#9A4F2B` → "terracotta orange"), or null for a grey, a near-black or a bad value. */
73
+ export function brandHueWord(seed: string | null | undefined): string | null {
74
+ const rgb = seed ? parseHexColor(seed) : null;
75
+ if (!rgb) return null;
76
+ const { a, b } = rgbToOklab(rgb);
77
+ if (Math.hypot(a, b) < NEUTRAL_CHROMA) return null;
78
+ const hue = (Math.atan2(b, a) * RADIANS_TO_DEGREES + FULL_TURN) % FULL_TURN;
79
+ return HUE_WORDS.find(([upTo]) => hue < upTo)?.[1] ?? null;
80
+ }
81
+
82
+ /**
83
+ * The site's art direction: the business type's world (moved to its `dark` world when the look is dark), its five parts,
84
+ * the brand's hue in the grade, PICTURE_NEVER before the world's own negatives. An unknown world is documentary.
85
+ */
86
+ export function artDirectionFor(world: string | null | undefined, design: Pick<DesignChoices, 'mode' | 'seed'>): ArtDirection {
87
+ const start = isPictureWorld(world) ? world : PictureWorld.Documentary;
88
+ const moved = design.mode === 'dark' ? PICTURE_WORLDS[start].dark : start;
89
+ const id = isPictureWorld(moved) ? moved : start;
90
+ const spec = PICTURE_WORLDS[id];
91
+ const hue = brandHueWord(design.seed);
92
+ return {
93
+ world: id,
94
+ medium: spec.medium,
95
+ light: spec.light,
96
+ grade: hue ? `${spec.grade}, leaning toward ${hue} so the pictures sit with the brand` : spec.grade,
97
+ texture: spec.texture,
98
+ negative: [...PICTURE_NEVER, ...spec.negative],
99
+ };
100
+ }
101
+
102
+ /** The ONE preamble a site's every picture prompt opens with — the same words, in the same order, every time. */
103
+ export function artDirectionPreamble(direction: ArtDirection): string {
104
+ const medium = direction.medium.charAt(0).toUpperCase() + direction.medium.slice(1);
105
+ return `${medium}. Light: ${direction.light}. Colour: ${direction.grade}. Texture: ${direction.texture}. Never: ${direction.negative.join('; ')}.`;
106
+ }
107
+
108
+ /** What is wrong with a value as an art direction — a path and a message each (`validateSitePlan` reports them). */
109
+ export function artDirectionIssues(value: unknown): Array<{ path: string; message: string }> {
110
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return [{ path: '', message: 'must be an object' }];
111
+ const v = value as Record<string, unknown>;
112
+ const issues: Array<{ path: string; message: string }> = [];
113
+ if (!isPictureWorld(v['world'])) issues.push({ path: 'world', message: `must be one of ${PICTURE_WORLD_IDS.join(', ')}` });
114
+ for (const part of ART_DIRECTION_PARTS.filter(p => p !== 'negative')) {
115
+ if (typeof v[part] !== 'string' || !(v[part] as string).trim()) issues.push({ path: part, message: 'must be words' });
116
+ }
117
+ const negative = v['negative'];
118
+ if (!Array.isArray(negative) || !negative.length || negative.some(n => typeof n !== 'string' || !n.trim())) {
119
+ issues.push({ path: 'negative', message: 'must list what no picture may show' });
120
+ }
121
+ return issues;
122
+ }
@@ -51,6 +51,10 @@ export enum SiteFindingCode {
51
51
  CopyPlaceholder = 'copy-placeholder',
52
52
  SampleItems = 'sample-items',
53
53
  StockPhoto = 'stock-photo',
54
+ /** M7: a picture made with AI for the site still stands where one of the studio's own could. */
55
+ MadePicture = 'made-picture',
56
+ /** M7: a picture the build could not make (its review said no twice) and nothing stood in for: an empty box. */
57
+ PictureMissing = 'picture-missing',
54
58
  NoPrivacyPage = 'no-privacy-page',
55
59
  PrivacyPagePlaceholder = 'privacy-page-placeholder',
56
60
  LowContrast = 'low-contrast',
@@ -76,6 +80,8 @@ export const FINDING_SEVERITY: Readonly<Record<SiteFindingCode, CheckSeverity>>
76
80
  [SiteFindingCode.CopyPlaceholder]: CheckSeverity.Ask,
77
81
  [SiteFindingCode.SampleItems]: CheckSeverity.Ask,
78
82
  [SiteFindingCode.StockPhoto]: CheckSeverity.Ask,
83
+ [SiteFindingCode.MadePicture]: CheckSeverity.Ask,
84
+ [SiteFindingCode.PictureMissing]: CheckSeverity.Warn,
79
85
  [SiteFindingCode.NoPrivacyPage]: CheckSeverity.Ask,
80
86
  [SiteFindingCode.PrivacyPagePlaceholder]: CheckSeverity.Block,
81
87
  [SiteFindingCode.LowContrast]: CheckSeverity.Warn,
@@ -100,6 +106,8 @@ const CHECK_OF: Readonly<Record<SiteFindingCode, SiteCheckId>> = {
100
106
  [SiteFindingCode.CopyPlaceholder]: SiteCheckId.Facts,
101
107
  [SiteFindingCode.SampleItems]: SiteCheckId.Facts,
102
108
  [SiteFindingCode.StockPhoto]: SiteCheckId.Facts,
109
+ [SiteFindingCode.MadePicture]: SiteCheckId.Facts,
110
+ [SiteFindingCode.PictureMissing]: SiteCheckId.Facts,
103
111
  [SiteFindingCode.NoPrivacyPage]: SiteCheckId.Facts,
104
112
  [SiteFindingCode.PrivacyPagePlaceholder]: SiteCheckId.Facts,
105
113
  [SiteFindingCode.LowContrast]: SiteCheckId.Accessibility,
@@ -164,6 +172,10 @@ export interface SiteAuditSnapshot {
164
172
  samples?: ReadonlyArray<{ contentType: string; id: string; label?: string }>;
165
173
  /** Stock photos a build placed that are still on the page. */
166
174
  stock?: ReadonlyArray<{ nodeId: string; pageId?: string; owner?: string }>;
175
+ /** M7: pictures made with AI that are still on the page. */
176
+ made?: ReadonlyArray<{ nodeId: string; pageId?: string; owner?: string }>;
177
+ /** M7: picture slots the build could not make a picture for, still empty. */
178
+ missing?: ReadonlyArray<{ nodeId: string; pageId?: string; owner?: string }>;
167
179
  }
168
180
 
169
181
  export interface SiteAuditOptions {
@@ -280,6 +292,8 @@ function checkFacts(ctx: Ctx, snapshot: SiteAuditSnapshot, pages: ReadonlyArray<
280
292
  }
281
293
  for (const s of snapshot.samples ?? []) add(ctx, SiteFindingCode.SampleItems, `"${s.label ?? s.id}" is a sample — replace or remove it.`, { nodeId: s.id });
282
294
  for (const s of (snapshot.stock ?? []).filter(inScope)) add(ctx, SiteFindingCode.StockPhoto, 'A stock photo stands in for one of yours.', { pageId: s.pageId, nodeId: s.nodeId, owner: s.owner });
295
+ for (const s of (snapshot.made ?? []).filter(inScope)) add(ctx, SiteFindingCode.MadePicture, 'A picture made with AI stands in for one of yours.', { pageId: s.pageId, nodeId: s.nodeId, owner: s.owner });
296
+ for (const s of (snapshot.missing ?? []).filter(inScope)) add(ctx, SiteFindingCode.PictureMissing, 'A picture could not be made for this spot — add one of yours.', { pageId: s.pageId, nodeId: s.nodeId, owner: s.owner });
283
297
  if (!personal) return;
284
298
  const privacy = snapshot.pages.find(p => PRIVACY_PAGE.test(p.route) || PRIVACY_PAGE.test(p.name));
285
299
  if (!privacy) return add(ctx, SiteFindingCode.NoPrivacyPage, 'A form asks for personal details, and the site has no privacy page saying what you do with them.');
@@ -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
 
@@ -691,7 +691,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
691
691
  "defaultOn",
692
692
  "activateOn",
693
693
  "orientation",
694
- "ariaLabel"
694
+ "ariaLabel",
695
+ "headerBehaviour",
696
+ "headerEdge"
695
697
  ],
696
698
  "styleTraits": [
697
699
  "position",
@@ -3114,6 +3116,10 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3114
3116
  "slideToClickedSlide": false,
3115
3117
  "effect": "slide",
3116
3118
  "speed": 300,
3119
+ "scrollPlays": false,
3120
+ "controlsPlacement": "under",
3121
+ "enableCaption": false,
3122
+ "enableCount": false,
3117
3123
  "enableNavigation": false,
3118
3124
  "navigationColor": "#ffffff",
3119
3125
  "navigationSize": 44,
@@ -3221,7 +3227,11 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3221
3227
  "__effectsLabel",
3222
3228
  "effect",
3223
3229
  "speed",
3230
+ "scrollPlays",
3224
3231
  "__navLabel",
3232
+ "controlsPlacement",
3233
+ "enableCaption",
3234
+ "enableCount",
3225
3235
  "enableNavigation",
3226
3236
  "navigationColor",
3227
3237
  "navigationSize",
@@ -3326,7 +3336,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3326
3336
  "container": true
3327
3337
  },
3328
3338
  "SliderSlide": {
3329
- "props": {},
3339
+ "props": {
3340
+ "caption": ""
3341
+ },
3330
3342
  "styleProps": {
3331
3343
  "position": "static",
3332
3344
  "top": "auto",
@@ -3405,7 +3417,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3405
3417
  "transitionDelay": "0ms",
3406
3418
  "transitionTimingFunction": "ease"
3407
3419
  },
3408
- "traits": [],
3420
+ "traits": [
3421
+ "caption"
3422
+ ],
3409
3423
  "styleTraits": [
3410
3424
  "position",
3411
3425
  "top",
@@ -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;