@vectoriox/iox-ui 5.28.0 → 5.29.1

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.
@@ -379,5 +379,45 @@ declare function withAntiSpamFields(values: Record<string, any>, renderedAt: num
379
379
  */
380
380
  declare function interpretSubmitResponse(body: any): SubmitResponse;
381
381
 
382
- export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, fieldOptionsOf, firstInvalidField, interpretSubmitResponse, onFieldBlur, onFieldChange, onSubmitAttempt, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
383
- export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormResult, FormRuntimeState, FormSpec, FormTransport, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
382
+ /**
383
+ * FORM PRESETS — a complete, working form as one insert: its fields, its submit label and the words of
384
+ * its Success and Error blocks. ONE definition (W1.1): the builder's Forms tray builds a preset from
385
+ * here (`buildFormPreset`, iox-builder), and the site composer (`generation-core`) stamps the SAME preset
386
+ * into a composed page, so a generated contact form is the real one — never a second, drifting copy.
387
+ * Data only: framework-free, like the rest of forms-core.
388
+ */
389
+ declare enum FormPresetId {
390
+ Contact = "contact",
391
+ Newsletter = "newsletter",
392
+ Application = "application",
393
+ Booking = "booking"
394
+ }
395
+ interface FormPresetField {
396
+ label: string;
397
+ control: string;
398
+ required?: boolean;
399
+ help?: string;
400
+ placeholder?: string;
401
+ /** Choices for select/radio, `value|Label` per line — the Field's `optionsText` trait. */
402
+ optionsText?: string;
403
+ rows?: number;
404
+ }
405
+ interface FormPresetDef {
406
+ id: `${FormPresetId}`;
407
+ name: string;
408
+ icon: string;
409
+ /** One line in the panel: what it collects and where it goes. */
410
+ description: string;
411
+ /** The form's author-facing name, which the submissions inbox groups by. */
412
+ formName: string;
413
+ fields: FormPresetField[];
414
+ submitLabel: string;
415
+ successText: string;
416
+ errorText: string;
417
+ }
418
+ declare const FORM_PRESETS: readonly FormPresetDef[];
419
+ /** The preset with this id, or undefined. */
420
+ declare function formPresetById(id: string | null | undefined): FormPresetDef | undefined;
421
+
422
+ export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FORM_PRESETS, FeedbackTiming, FormPresetId, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, fieldOptionsOf, firstInvalidField, formPresetById, interpretSubmitResponse, onFieldBlur, onFieldChange, onSubmitAttempt, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
423
+ export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormPresetDef, FormPresetField, FormResult, FormRuntimeState, FormSpec, FormTransport, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
@@ -1,14 +1,99 @@
1
1
  import { DesignChoices } from '@vectoriox/iox-ui/design-core';
2
2
 
3
3
  declare const SPEC_SITE_PLAN_SHAPE: {
4
- readonly version: "1";
5
- readonly source: "{ kind: 'template', templateId, templateVersion } | { kind: 'ai', runId }";
6
- readonly business: "{ name (required), tagline?, facts: { email?, phone?, city? } }";
7
- readonly design: "{ flavorId, seed, pairId, shape, density, motion, pillButtons, mode } — the DesignChoices of the design system";
8
- readonly contentTypes: "ContentTypePlan[] — { name, label, fields[], seed }";
9
- readonly dataSources: "DataSourcePlan[] — merged by alias into the org library";
10
- readonly pages: "PagePlan[] — { key, name, route, isHome?, template?, routeSource?, sections: { pattern, slots?, binding?, without? }[] }";
11
- readonly globals: "{ header: patternId, footer: patternId }";
4
+ readonly plan: {
5
+ readonly version: "'1'";
6
+ readonly source: "sourceTemplate | sourceAi";
7
+ readonly business: "business";
8
+ readonly design: "design";
9
+ readonly contentTypes: "contentType[]";
10
+ readonly dataSources: "(cmsDataSource | relativeDataSource)[] — merged by alias into the org library";
11
+ readonly pages: "page[]";
12
+ readonly globals: "globals";
13
+ };
14
+ readonly sourceTemplate: {
15
+ readonly kind: "'template'";
16
+ readonly templateId: "string";
17
+ readonly templateVersion: "integer";
18
+ };
19
+ readonly sourceAi: {
20
+ readonly kind: "'ai'";
21
+ readonly runId: "string";
22
+ };
23
+ readonly business: {
24
+ readonly name: "string — the brand: the header, the hero, the copyright line";
25
+ readonly "tagline?": "string — the hero lead and the footer line";
26
+ readonly "facts?": "{ [fact]: string } — facts (email, phone, city): only the customer's own words; a blank fact is a publish-blocking placeholder, never invented";
27
+ };
28
+ readonly design: {
29
+ readonly flavorId: "string — a v2 flavor pack id";
30
+ readonly seed: "string — #rrggbb brand colour";
31
+ readonly pairId: "string — one of the pack's font pairings";
32
+ readonly shape: "string — axes.shapes";
33
+ readonly density: "string — axes.densities";
34
+ readonly motion: "string — axes.motions";
35
+ readonly pillButtons: "boolean";
36
+ readonly mode: "'light' | 'dark' | 'auto'";
37
+ readonly "tint?": "string — axes.tints (DS9 neutral tint)";
38
+ readonly "fonts?": "object — uploaded font families per role (DS5)";
39
+ };
40
+ readonly contentType: {
41
+ readonly name: "string — unique";
42
+ readonly label: "string";
43
+ readonly "previewTextField?": "string — a field of the type: the item's name on cards and the alt text of its bound images";
44
+ readonly "previewImageField?": "string — a field of the type: the item's picture";
45
+ readonly fields: "field[]";
46
+ readonly "seed?": "seed";
47
+ };
48
+ readonly field: {
49
+ readonly name: "string — unique in the type";
50
+ readonly type: "string";
51
+ readonly controller: "string — an IOX input controller";
52
+ readonly "required?": "boolean";
53
+ readonly "note?": "string";
54
+ };
55
+ readonly seed: {
56
+ readonly count: "integer ≥ 0";
57
+ readonly sample: "boolean — items are flagged isSample";
58
+ readonly isPublished: "boolean";
59
+ readonly "[field]?": "the field's value — strings are templates: {n} = the item's 1-based number, {sampleImage:n} / {sampleImage:n+k} = a bundled sample image";
60
+ };
61
+ readonly cmsDataSource: {
62
+ readonly alias: "string — unique; never $… (platform scopes)";
63
+ readonly type: "'cms'";
64
+ readonly mode: "'query' | 'single'";
65
+ readonly contentType: "string — a content type of the plan";
66
+ readonly "limit?": "integer ≥ 1";
67
+ readonly "filterByRouteParam?": "{ field, routeParam } — single only: the route param that picks the item";
68
+ };
69
+ readonly relativeDataSource: {
70
+ readonly alias: "string — unique";
71
+ readonly type: "'relative'";
72
+ readonly of: "string — a single cms source (the subject)";
73
+ readonly op: "'$siblings.next' | '$siblings.prev'";
74
+ readonly within: "string — a query cms source of the same type";
75
+ };
76
+ readonly page: {
77
+ readonly key: "string — unique";
78
+ readonly name: "string — the nav label";
79
+ readonly route: "string — /lowercase-words, :params; unique";
80
+ readonly "isHome?": "boolean — exactly one page, at /";
81
+ readonly "required?": "boolean — a template page the wizard's Pages step cannot leave out";
82
+ readonly "inNav?": "boolean — a Link in the header and footer navigation, in plan order";
83
+ readonly "template?": "string — the content type a template page renders one item of";
84
+ readonly "routeSource?": "string — the single source read by the route param (route.* binds it)";
85
+ readonly sections: "section[]";
86
+ };
87
+ readonly section: {
88
+ readonly pattern: "string — a non-global pattern id";
89
+ readonly "slots?": "{ [slot]: string } — the pattern's declared slots";
90
+ readonly "binding?": "string — the list source a pattern's $binding repeater iterates (exactly when it has one)";
91
+ readonly "without?": "string[] — the pattern's optional parts to leave out";
92
+ };
93
+ readonly globals: {
94
+ readonly header: "string — a header pattern (global: before)";
95
+ readonly footer: "string — a footer pattern (global: after)";
96
+ };
12
97
  };
13
98
  declare const SPEC_SITE_FACTS: {
14
99
  readonly email: "Email address";
@@ -26,6 +111,7 @@ declare const SPEC_SITE_PATTERNS: {
26
111
  readonly tag: "nav";
27
112
  readonly lay: {
28
113
  readonly display: "flex";
114
+ readonly flexWrap: "wrap";
29
115
  readonly justifyContent: "space-between";
30
116
  readonly alignItems: "center";
31
117
  readonly gap: "var(--iox-space-gap)";
@@ -47,18 +133,16 @@ declare const SPEC_SITE_PATTERNS: {
47
133
  readonly lay: {
48
134
  readonly display: "flex";
49
135
  readonly flexWrap: "wrap";
50
- readonly gap: "28px";
136
+ readonly gap: "var(--iox-space-gap)";
51
137
  readonly alignItems: "center";
138
+ readonly width: "auto";
52
139
  };
53
140
  readonly c: readonly [{
54
- readonly t: "Repeater";
55
- readonly src: "$navPages";
56
- readonly c: readonly [{
57
- readonly t: "Link";
58
- readonly es: "link";
59
- readonly bind: "page.name";
60
- readonly href: "$page.route";
61
- }];
141
+ readonly t: "Link";
142
+ readonly es: "link";
143
+ readonly each: "$navPages";
144
+ readonly bind: "page.name";
145
+ readonly href: "$page.route";
62
146
  }, {
63
147
  readonly t: "ButtonBlock";
64
148
  readonly es: "button-primary";
@@ -125,14 +209,11 @@ declare const SPEC_SITE_PATTERNS: {
125
209
  readonly level: "h4";
126
210
  readonly text: "Pages";
127
211
  }, {
128
- readonly t: "Repeater";
129
- readonly src: "$navPages";
130
- readonly c: readonly [{
131
- readonly t: "Link";
132
- readonly es: "link";
133
- readonly bind: "page.name";
134
- readonly href: "$page.route";
135
- }];
212
+ readonly t: "Link";
213
+ readonly es: "link";
214
+ readonly each: "$navPages";
215
+ readonly bind: "page.name";
216
+ readonly href: "$page.route";
136
217
  }];
137
218
  }, {
138
219
  readonly t: "Container";
@@ -168,7 +249,7 @@ declare const SPEC_SITE_PATTERNS: {
168
249
  readonly t: "Text";
169
250
  readonly es: "small";
170
251
  readonly slot: "copyright";
171
- readonly fromBusiness: "copyright";
252
+ readonly text: "© {year} {business.name}";
172
253
  }];
173
254
  readonly lay: {
174
255
  readonly display: "flex";
@@ -580,6 +661,7 @@ declare const SPEC_SITE_PATTERNS: {
580
661
  readonly t: "Image";
581
662
  readonly es: "media";
582
663
  readonly bind: "item";
664
+ readonly alt: "";
583
665
  readonly aspect: "4 / 3";
584
666
  }];
585
667
  }];
@@ -729,26 +811,6 @@ declare const SPEC_SITE_PATTERNS: {
729
811
  readonly c: readonly [{
730
812
  readonly t: "Form";
731
813
  readonly preset: "contact";
732
- readonly c: readonly [{
733
- readonly t: "Field";
734
- readonly control: "text";
735
- readonly label: "Name";
736
- readonly required: true;
737
- }, {
738
- readonly t: "Field";
739
- readonly control: "email";
740
- readonly label: "Email";
741
- readonly required: true;
742
- }, {
743
- readonly t: "Field";
744
- readonly control: "textarea";
745
- readonly label: "Message";
746
- readonly required: true;
747
- }, {
748
- readonly t: "SubmitButton";
749
- readonly es: "button-primary";
750
- readonly text: "Send";
751
- }];
752
814
  }];
753
815
  }];
754
816
  }];
@@ -936,9 +998,40 @@ interface NodeBaseline {
936
998
  }
937
999
 
938
1000
  declare const NODE_BASELINES: Readonly<Record<string, NodeBaseline>>;
1001
+ /** What every style trait saves as when no constructor sets it (buildFullStyleTraits' defaults). */
1002
+ declare const NEUTRAL_STYLE_PROPS: Readonly<Record<string, unknown>>;
1003
+
1004
+ /**
1005
+ * Why each type's constructor sets the style values it sets (W1.1, Q1). Every value a type's config
1006
+ * gives that differs from what the trait saves when nobody sets it (`NEUTRAL_STYLE_PROPS`) is exactly
1007
+ * one of:
1008
+ *
1009
+ * - **Layout** — a size or spacing that exists only so an EMPTY node dropped from the palette is visible
1010
+ * and grabbable (a Container's 200px, a Text's 10px padding). A composed node is never empty and its
1011
+ * pattern owns its box, so the composer RELEASES these to the neutral value unless the pattern
1012
+ * declares one: the nav bar was 200px tall and every lead line indented 10px (W1's screenshot).
1013
+ * - **Behaviour** — what makes the type work: a button's inline-flex centring, a form's column, the 44px
1014
+ * touch target, a slide's width, a spacer's height, a list's marker room. Kept.
1015
+ * - **Look** — the type's pre-design-system paint, which its element style replaces. Kept (the style
1016
+ * writes over it; a type with no style keeps its pre-DS2 look, D5).
1017
+ *
1018
+ * Saved pages are untouched: this is the composer's rule, never the builder's (a palette drop still
1019
+ * gets its 200px). `baseline-roles.spec.ts` fails the moment a config gains a default this table does
1020
+ * not classify — so a new default is a decision, not an accident.
1021
+ */
1022
+ declare enum BaselineRole {
1023
+ Layout = "layout",
1024
+ Behaviour = "behaviour",
1025
+ Look = "look"
1026
+ }
1027
+ declare const BASELINE_ROLES: Readonly<Record<string, Readonly<Record<string, BaselineRole>>>>;
1028
+ /** The style values a type's constructor sets for an empty palette drop, each mapped to the value the
1029
+ * trait saves when nobody sets it — what a composed node of that type starts from. */
1030
+ declare function releasedLayoutDefaults(type: string): Record<string, unknown>;
939
1031
 
940
1032
  /**
941
- * The Site Plan (contract `site.planShape`) — the ONE document the website wizard writes
1033
+ * The Site Plan (contract `site.planShape`, `SPEC_SITE_PLAN_SHAPE`: every key typed and explained, and the
1034
+ * validator reads its key lists from it) — the ONE document the website wizard writes
942
1035
  * (`source.kind: template`) and the AI agent will write (`source.kind: ai`). One validator, one
943
1036
  * composer: everything the wizard proves is proven for the agent.
944
1037
  */
@@ -1035,9 +1128,9 @@ interface PagePlan {
1035
1128
  readonly name: string;
1036
1129
  readonly route: string;
1037
1130
  readonly isHome?: boolean;
1038
- /** Carried by templates (the wizard's Pages step); not part of the documented plan shape. */
1131
+ /** A template page the wizard's Pages step cannot leave out (`planFromTemplate`). */
1039
1132
  readonly required?: boolean;
1040
- /** In the navigation (`$navPages`). Carried by templates; not part of the documented plan shape. */
1133
+ /** A Link in the header and footer navigation (`each: $navPages`), in plan order. */
1041
1134
  readonly inNav?: boolean;
1042
1135
  /** A template page: the content type it renders one item of. */
1043
1136
  readonly template?: string;
@@ -1067,12 +1160,16 @@ interface SitePlan {
1067
1160
  * `t` component type · `es` element style · `scheme` section colour scheme · `tag` a Container's html
1068
1161
  * tag · `level` a Heading's level · `lay` layout-only styleProps · `resp` per-breakpoint styleProps ·
1069
1162
  * `aspect` an Image's aspect ratio · `slot` author-editable text, filled from the plan's section slots ·
1070
- * `fromBusiness` text from the plan's business · `text` the pattern's own words · `ph` a publish-blocking
1071
- * placeholder (a FACT key, `copy` or `image`) · `bind` a binding path on the repeater item (`item.*`,
1072
- * `page.*`), the route item (`route.*`) or its next sibling (`next.*`) · `src` what a Repeater iterates
1073
- * (`$binding`, `$navPages`, `route.<array>`) · `href` a page route (`$page.route` = the nav item's) ·
1163
+ * `fromBusiness` text from the plan's business · `text` the pattern's own words (`{year}` and
1164
+ * `{business.name}` are filled at compose time) · `ph` a publish-blocking placeholder (a FACT key,
1165
+ * `copy` or `image`) · `bind` a binding path on the repeater item (`item.*`), the route item
1166
+ * (`route.*`) or its next sibling (`next.*`) — or, on an `each` node, the page it is stamped for
1167
+ * (`page.*`, written as plain words) · `alt` an Image's alt binding (default: the bound item's preview
1168
+ * text field; `''` = decorative, written empty) · `src` what a Repeater iterates (`$binding`, `route.<array>`) · `each` stamp the node once
1169
+ * per page in the navigation (`$navPages`) · `href` a page route (`$page.route` = the stamped page's) ·
1074
1170
  * `param` the value of the route's `:param` · `optional` a part a page may leave out (`without`) ·
1075
- * `preset` a Form preset · `control` / `label` / `required` a Field's · `c` children.
1171
+ * `preset` a forms-core form preset (its fields, submit and Success/Error words) · `control` / `label` /
1172
+ * `required` a Field's · `c` children.
1076
1173
  */
1077
1174
  interface PatternNode {
1078
1175
  readonly t: string;
@@ -1088,7 +1185,9 @@ interface PatternNode {
1088
1185
  readonly text?: string;
1089
1186
  readonly ph?: string;
1090
1187
  readonly bind?: string;
1188
+ readonly alt?: string;
1091
1189
  readonly src?: string;
1190
+ readonly each?: string;
1092
1191
  readonly href?: string;
1093
1192
  readonly param?: string;
1094
1193
  readonly optional?: string;
@@ -1119,16 +1218,17 @@ declare enum PatternPlaceholder {
1119
1218
  declare enum PatternDirective {
1120
1219
  /** A Repeater's source: the section's `binding`. */
1121
1220
  Binding = "$binding",
1122
- /** A Repeater's source: the plan's pages that are in the navigation. */
1221
+ /** `each`: one plain node per page of the plan that is in the navigation (`inNav`), in plan order —
1222
+ * words and routes written in, editable on the canvas (W1.1). */
1123
1223
  NavPages = "$navPages",
1124
- /** A Link's route: the nav item's route. */
1224
+ /** A Link's route: the route of the page an `each` node is stamped for. */
1125
1225
  PageRoute = "$page.route"
1126
1226
  }
1127
1227
  /** The scopes a `bind` / `param` / `src` path starts with. */
1128
1228
  declare enum BindScope {
1129
1229
  /** The nearest Repeater's row. */
1130
1230
  Item = "item",
1131
- /** The nearest Repeater's row, when that Repeater iterates the navigation's pages. */
1231
+ /** The page an `each: $navPages` node is stamped for — resolved to plain words, never a binding. */
1132
1232
  Page = "page",
1133
1233
  /** The page's route item (`routeSource`). */
1134
1234
  Route = "route",
@@ -1144,6 +1244,10 @@ declare function patternSlots(pattern: SitePattern): string[];
1144
1244
  declare function patternOptionalParts(pattern: SitePattern): string[];
1145
1245
  /** True when the pattern has a Repeater iterating the section's `binding`. */
1146
1246
  declare function patternNeedsBinding(pattern: SitePattern): boolean;
1247
+ /** The scopes whose ITEM a pattern's bound Images take their alt text from (an Image that names no `alt`
1248
+ * of its own): `item` (the row of a `$binding` Repeater — the section's list), `route` or `next`. Each
1249
+ * needs a content type with a `previewTextField` (W1.1, Q7). */
1250
+ declare function altSourceScopes(pattern: SitePattern): Set<string>;
1147
1251
  /** The bind scopes a pattern reads (`route`, `next`, …) — what its page must provide. */
1148
1252
  declare function patternScopes(pattern: SitePattern): Set<string>;
1149
1253
 
@@ -1213,13 +1317,19 @@ interface SerializedNode {
1213
1317
  * wrong — never something to work around. */
1214
1318
  declare class ComposeError extends Error {
1215
1319
  }
1320
+ interface CreateNodeOptions {
1321
+ /** The composer's rule (W1.1): the type's empty-drop sizes (`BaselineRole.Layout`) start neutral, so
1322
+ * the PATTERN owns the box. The builder never does this — a palette drop keeps its 200px. */
1323
+ releaseLayoutDefaults?: boolean;
1324
+ }
1216
1325
  /**
1217
1326
  * A NEW node of `type`, as the builder creates one (ComponentRegistryService.createConfig, serialized):
1218
1327
  * the type's constructor defaults, its text colour released to `inherit` (NEW_NODE_STYLE_DEFAULTS), then
1219
1328
  * ONE element style — `elementStyle` when given, else the type's default for its props (a Heading
1220
- * follows its level). `props` are applied before the style is chosen.
1329
+ * follows its level). `props` are applied before the style is chosen. With `releaseLayoutDefaults` the
1330
+ * constructor's layout sizes are released first, so an element style that owns one still writes it.
1221
1331
  */
1222
- declare function createSerializedNode(id: string, type: string, props?: Record<string, unknown>, elementStyle?: string | null): SerializedNode;
1332
+ declare function createSerializedNode(id: string, type: string, props?: Record<string, unknown>, elementStyle?: string | null, options?: CreateNodeOptions): SerializedNode;
1223
1333
  /** Refuse a style trait the type has no slot for — the builder would silently drop it. */
1224
1334
  declare function assertStyleSlots(type: string, names: ReadonlyArray<string>): void;
1225
1335
  /** Write style traits onto a node (each one a slot the type has). */
@@ -1257,7 +1367,10 @@ declare enum SitePlanIssueCode {
1257
1367
  UnknownSlot = "unknown-slot",
1258
1368
  UnknownOptionalPart = "unknown-optional-part",
1259
1369
  Binding = "binding",
1260
- MissingScope = "missing-scope"
1370
+ MissingScope = "missing-scope",
1371
+ /** A pattern binds an item's image, and the item's content type names no `previewTextField` to be
1372
+ * its alt text. */
1373
+ MissingPreviewField = "missing-preview-field"
1261
1374
  }
1262
1375
  interface SitePlanIssue {
1263
1376
  code: SitePlanIssueCode;
@@ -1273,9 +1386,14 @@ declare class SitePlanError extends Error {
1273
1386
  readonly issues: ReadonlyArray<SitePlanIssue>;
1274
1387
  constructor(issues: ReadonlyArray<SitePlanIssue>);
1275
1388
  }
1276
- /** The alias of the static source the composer writes for the navigation (`$navPages`). A plan source
1277
- * may not take it. */
1278
- declare const NAV_PAGES_ALIAS = "navPages";
1389
+ /** The documented plan shape (contract `site.planShape`): key → type; a key ending in `?` is optional and
1390
+ * a `[name]` key stands for the object's own field names. The key lists below are READ from it, so a
1391
+ * key the plan documents is a key the gate validates — and an undocumented one is refused. */
1392
+ type ShapeEntry = Readonly<Record<string, string>>;
1393
+ /** Every fixed key an object of the shape may carry. */
1394
+ declare function shapeKeys(entry: ShapeEntry): string[];
1395
+ /** The keys an object of the shape must carry. */
1396
+ declare function requiredShapeKeys(entry: ShapeEntry): string[];
1279
1397
  /** The `:param` names of a route, in order. */
1280
1398
  declare function routeParams(route: string): string[];
1281
1399
  /** The plan's `$siblings.next` source of `routeSource` — what a pattern's `next.*` reads. */
@@ -1292,10 +1410,12 @@ declare function assertValidSitePlan(plan: unknown): void;
1292
1410
  /**
1293
1411
  * THE composer — a Site Plan in, ordinary IOX data out (design-system plan §6b, W1). Deterministic and
1294
1412
  * pure: the same plan composes to byte-identical output, with no clock, no randomness and no transport.
1295
- * The wizard and the AI agent both end here; W2's site-build service writes what it returns.
1413
+ * The wizard and the AI agent both end here; W2's site-build service writes what it returns. The one
1414
+ * thing that changes with time — the copyright year — comes in through `ComposeContext`.
1296
1415
  *
1297
1416
  * Every node is created the way the builder creates one (`createSerializedNode`): the type's full
1298
- * constructor defaults, then ONE element style, then the pattern's layout. So a composed page is what
1417
+ * constructor defaults — its empty-drop sizes released, so the pattern owns its box (`BASELINE_ROLES`) —
1418
+ * then ONE element style, then the pattern's layout. So a composed page is what
1299
1419
  * an author would have built by hand from the palette — the builder reloads it and saves it unchanged,
1300
1420
  * and the engine renders it with nothing to resolve. Never a colour, size or font a pattern did not
1301
1421
  * name; never a fact the customer did not give (blank → a placeholder carrying the fact's label).
@@ -1322,14 +1442,29 @@ declare enum ComposeIssueCode {
1322
1442
  /** A slot the plan and the business left empty and the pattern gives no words for — the node keeps
1323
1443
  * its type's default text. */
1324
1444
  UnfilledSlot = "unfilled-slot",
1325
- /** A pattern links to a route the plan has no page for (an optional page was left out). */
1326
- DanglingLink = "dangling-link"
1445
+ /** A link to a page the plan leaves out now opens an email to the business (`mailto:`). */
1446
+ LinkToEmail = "link-to-email",
1447
+ /** A link or button to a page the plan leaves out, with no business email to send to: removed. */
1448
+ LinkRemoved = "link-removed",
1449
+ /** A linked box to a page the plan leaves out, with no business email: it keeps its contents as a
1450
+ * plain Container and loses only the link. */
1451
+ LinkUnlinked = "link-unlinked"
1327
1452
  }
1453
+ /** The issues that are a dangling link (a route the plan has no page for) and what was done about it. */
1454
+ declare const DANGLING_LINK_CODES: ReadonlySet<ComposeIssueCode>;
1328
1455
  interface ComposeIssue {
1329
1456
  code: ComposeIssueCode;
1330
1457
  nodeId: string;
1331
1458
  owner: string;
1459
+ /** In plain words, for the wizard to show. */
1332
1460
  detail: string;
1461
+ /** A dangling link's route. */
1462
+ route?: string;
1463
+ }
1464
+ /** What the composer may not decide for itself — today the clock. */
1465
+ interface ComposeContext {
1466
+ /** The calendar year the copyright line reads (`{year}`), from the caller's clock. */
1467
+ year: number;
1333
1468
  }
1334
1469
  interface ComposedPage {
1335
1470
  key: string;
@@ -1384,14 +1519,11 @@ declare const SAMPLE_IMAGE_REF: RegExp;
1384
1519
  declare function sampleImageRef(index: number): string;
1385
1520
  /** The 1-based index a `{sampleImage:k}` value names, or null. */
1386
1521
  declare function parseSampleImageRef(value: unknown): number | null;
1387
- /** A page route as a base-relative href — the engine's `resolvePageHref` form (`/contact` → `contact`,
1388
- * `/` → `./`), so a link stays inside the org's `<base href>`. */
1389
- declare function baseRelativeHref(route: string): string;
1390
1522
  /**
1391
1523
  * Compose a Site Plan. Throws `SitePlanError` (every issue) when the plan does not validate — the
1392
- * composer never repairs a plan.
1524
+ * composer never repairs a plan. `context` carries what the composer may not read for itself (the year).
1393
1525
  */
1394
- declare function composeSite(plan: SitePlan): ComposedSite;
1526
+ declare function composeSite(plan: SitePlan, context: ComposeContext): ComposedSite;
1395
1527
 
1396
- export { BindScope, ComposeError, ComposeIssueCode, FACT_KEYS, GlobalSlot, NAV_PAGES_ALIAS, NODE_BASELINES, PatternDirective, PatternGlobalPosition, PatternPlaceholder, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, assertStyleSlots, assertValidSitePlan, baseRelativeHref, composeSite, createSerializedNode, nextSourceOf, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, planFromTemplate, routeParams, sampleImageRef, setStyleProps, validateSitePlan, walkSerialized };
1397
- export type { CmsDataSourcePlan, ComposeIssue, ComposedDataSource, ComposedItem, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, DataSourcePlan, FactKey, NodeBaseline, PagePlan, PageSectionPlan, PatternNode, RelativeDataSourcePlan, SerializedBinding, SerializedNode, SerializedResponsive, SiteBusiness, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
1528
+ export { BASELINE_ROLES, BaselineRole, BindScope, ComposeError, ComposeIssueCode, DANGLING_LINK_CODES, FACT_KEYS, GlobalSlot, NEUTRAL_STYLE_PROPS, NODE_BASELINES, PatternDirective, PatternGlobalPosition, PatternPlaceholder, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, altSourceScopes, assertStyleSlots, assertValidSitePlan, composeSite, createSerializedNode, nextSourceOf, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, planFromTemplate, releasedLayoutDefaults, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, validateSitePlan, walkSerialized };
1529
+ export type { CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, DataSourcePlan, FactKey, NodeBaseline, PagePlan, PageSectionPlan, PatternNode, RelativeDataSourcePlan, SerializedBinding, SerializedNode, SerializedResponsive, SiteBusiness, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
package/index.d.ts CHANGED
@@ -446,6 +446,17 @@ declare class ClientRepeaterComponent {
446
446
  static ɵcmp: i0.ɵɵComponentDeclaration<ClientRepeaterComponent, "iox-repeater", never, { "nodeId": { "alias": "nodeId"; "required": false; }; }, {}, never, never, false, never>;
447
447
  }
448
448
 
449
+ /**
450
+ * Where a node links (W1.1). Link and LinkedContainer always had two link types — an external URL or a
451
+ * website PAGE by its route — and ButtonBlock now has the same pair, so a composed "Get in touch" button
452
+ * is a page link the builder shows as `/contact`, not a hand-built URL. Framework-free: the composer
453
+ * (generation-core) writes it, the builder canvas and both engine render paths read it.
454
+ */
455
+ declare enum LinkType {
456
+ External = "external",
457
+ Page = "page"
458
+ }
459
+
449
460
  /**
450
461
  * Directive-host ButtonBlock: the real `<a>`/`<button>` IS the host — no `app-button-block`
451
462
  * wrapper, no inner/outer split. The tag is chosen by whoever CREATES the component (engine
@@ -453,16 +464,21 @@ declare class ClientRepeaterComponent {
453
464
  * The `iox-btn-block` class (UA resets: inline-flex, color/font inherit, no border) matches the
454
465
  * string renderer and lives in the engine's global styles.scss — kept OFF `:host` so styleProps
455
466
  * (`.iox-node-{id}`) can override it. Children render into the host via `childrenHost`.
467
+ * A page link (`linkType: page`) arrives with its `pageRoute` already resolved by the engine (params
468
+ * substituted, base-relative — the Link's resolver); `buttonBlockHref` decides the href for every renderer.
456
469
  */
457
470
  declare class ButtonBlockComponent {
458
471
  tag: 'button' | 'a';
472
+ linkType: LinkType;
459
473
  href: string;
474
+ pageRoute: string;
460
475
  target: '_self' | '_blank';
461
476
  type: 'button' | 'submit' | 'reset';
462
477
  nodeId: string;
463
478
  childrenHost: ViewContainerRef;
479
+ get resolvedHref(): string | null;
464
480
  static ɵfac: i0.ɵɵFactoryDeclaration<ButtonBlockComponent, never>;
465
- static ɵcmp: i0.ɵɵComponentDeclaration<ButtonBlockComponent, "a[iox-button-block],button[iox-button-block]", never, { "tag": { "alias": "tag"; "required": false; }; "href": { "alias": "href"; "required": false; }; "target": { "alias": "target"; "required": false; }; "type": { "alias": "type"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; }, {}, never, never, false, never>;
481
+ static ɵcmp: i0.ɵɵComponentDeclaration<ButtonBlockComponent, "a[iox-button-block],button[iox-button-block]", never, { "tag": { "alias": "tag"; "required": false; }; "linkType": { "alias": "linkType"; "required": false; }; "href": { "alias": "href"; "required": false; }; "pageRoute": { "alias": "pageRoute"; "required": false; }; "target": { "alias": "target"; "required": false; }; "type": { "alias": "type"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; }, {}, never, never, false, never>;
466
482
  }
467
483
 
468
484
  /** The five fixed breakpoints (plan decision D2). Desktop-first: each narrower one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.28.0",
3
+ "version": "5.29.1",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -16,7 +16,8 @@ import { DesignTokenLike, ResolvedMode, resolveSchemeTokenValues } from './token
16
16
  * - `inherit` → the container's ink, a transparent fill → what is under it, translucent fills composited,
17
17
  * a gradient ground (Glass's mesh, Midnight's glow) checked at its worst stop;
18
18
  * - base and hover; a button's icon read through the contract's `inkScopes` (it takes the button's ink),
19
- * its focus ring; the form parts the field baseline paints (`contrastAudit.formParts`).
19
+ * its focus ring; the form parts the field baseline paints (`contrastAudit.formParts`); the form's
20
+ * Success / Error notices, in every scheme and container.
20
21
  *
21
22
  * WCAG 2: 4.5:1 for text, 3:1 for glyphs, UI boundaries and focus indicators (1.4.11), judged on 8-bit
22
23
  * colours. The same audit gates the contract (`colour.mjs auditContrast`, every flavor × tint × a seed
@@ -162,11 +163,11 @@ export function auditDesignContrast(rows: ReadonlyArray<DesignTokenLike>, mode:
162
163
  check(FIELD, `${part.part} thumb`, parseCssColour(resolveIn(values, part.thumb), boxInk), bgs.map(b => compositeOver(paint, b)), part.min);
163
164
  }
164
165
  }
165
- if (box === null && schemeId === DEFAULT_SCHEME) {
166
- for (const n of notices) {
167
- const fill = paintColours(resolveIn(values, ownStyle(n)['backgroundColor']), boxInk);
168
- check(n.id, 'text', parseCssColour(resolveIn(values, ownStyle(n)['color']), boxInk), overAll(fill, bgs), WCAG_AA_TEXT);
169
- }
166
+ // A form's Success / Error notice sits wherever its form does — on the section in every scheme
167
+ // AND inside every container (the Studio's contact form lives in a Card): W1.1.
168
+ for (const n of notices) {
169
+ const fill = paintColours(resolveIn(values, ownStyle(n)['backgroundColor']), boxInk);
170
+ check(n.id, 'text', parseCssColour(resolveIn(values, ownStyle(n)['color']), boxInk), overAll(fill, bgs), WCAG_AA_TEXT);
170
171
  }
171
172
  }
172
173
  }
@@ -1522,6 +1522,23 @@ export const SPEC_ELEMENT_STYLES = [
1522
1522
  "color": "var(--iox-link-hover-color)"
1523
1523
  }
1524
1524
  },
1525
+ {
1526
+ "id": "button-label",
1527
+ "label": "Button label",
1528
+ "group": "Text",
1529
+ "appliesTo": [
1530
+ "Text"
1531
+ ],
1532
+ "base": {
1533
+ "fontFamily": "inherit",
1534
+ "fontSize": "inherit",
1535
+ "fontWeight": "inherit",
1536
+ "lineHeight": "inherit",
1537
+ "letterSpacing": "inherit",
1538
+ "textTransform": "inherit",
1539
+ "color": "inherit"
1540
+ }
1541
+ },
1525
1542
  {
1526
1543
  "id": "section",
1527
1544
  "label": "Section",
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Where a node links (W1.1). Link and LinkedContainer always had two link types — an external URL or a
3
+ * website PAGE by its route — and ButtonBlock now has the same pair, so a composed "Get in touch" button
4
+ * is a page link the builder shows as `/contact`, not a hand-built URL. Framework-free: the composer
5
+ * (generation-core) writes it, the builder canvas and both engine render paths read it.
6
+ */
7
+
8
+ export enum LinkType {
9
+ External = 'external',
10
+ Page = 'page',
11
+ }
12
+
13
+ export interface ButtonBlockLinkProps {
14
+ tag?: unknown;
15
+ linkType?: unknown;
16
+ href?: unknown;
17
+ pageRoute?: unknown;
18
+ }
19
+
20
+ const ANCHOR_TAG = 'a';
21
+
22
+ /**
23
+ * The href a ButtonBlock renders, or `null` for none (it is a `<button>`, or a link with no target).
24
+ * Only an `<a>` links. A page link's route goes through `pageHref` — the engine's `resolvePageHref`
25
+ * (`:params` substituted, base-relative); the builder canvas passes none and shows the route as authored,
26
+ * as its Link does.
27
+ */
28
+ export function buttonBlockHref(props: ButtonBlockLinkProps, pageHref: (route: string) => string = route => route): string | null {
29
+ if (props.tag !== ANCHOR_TAG) return null;
30
+ const href = props.linkType === LinkType.Page ? pageHref(String(props.pageRoute ?? '')) : String(props.href ?? '');
31
+ return href || null;
32
+ }
@@ -17,6 +17,7 @@ export * from './element-styles';
17
17
  export * from './adopt';
18
18
  export * from './node-classes';
19
19
  export * from './empty-image';
20
+ export * from './node-links';
20
21
  export * from './brand';
21
22
  export * from './colour-eval';
22
23
  export * from './brand-neutrals';