@vectoriox/iox-ui 5.82.2 → 5.83.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.
Files changed (32) hide show
  1. package/design-core/index.d.ts +200 -0
  2. package/fesm2022/vectoriox-iox-ui-design-core.mjs +215 -1
  3. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  4. package/fesm2022/vectoriox-iox-ui-forms-core.mjs +733 -56
  5. package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +40 -4
  7. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui.mjs +1559 -135
  9. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  10. package/forms-core/index.d.ts +366 -18
  11. package/generation-core/index.d.ts +3 -0
  12. package/index.d.ts +307 -25
  13. package/package.json +1 -1
  14. package/src/design-core/contrast-audit.ts +4 -0
  15. package/src/design-core/flavor-spec.generated.ts +209 -1
  16. package/src/forms-core/channel.ts +85 -0
  17. package/src/forms-core/collect.ts +63 -30
  18. package/src/forms-core/dates.ts +299 -0
  19. package/src/forms-core/field-markup.ts +159 -0
  20. package/src/forms-core/form-presets.ts +51 -2
  21. package/src/forms-core/form.model.ts +34 -2
  22. package/src/forms-core/messages.ts +31 -0
  23. package/src/forms-core/public-api.ts +3 -0
  24. package/src/forms-core/rule-catalog.ts +3 -2
  25. package/src/forms-core/submit-lifecycle.ts +42 -12
  26. package/src/forms-core/validate.ts +90 -4
  27. package/src/generation-core/audit-site.ts +9 -1
  28. package/src/generation-core/compose-site.ts +22 -3
  29. package/src/generation-core/node-baselines.generated.ts +10 -0
  30. package/src/generation-core/plan-wire.ts +2 -2
  31. package/styles/iox-design.css +10 -0
  32. package/styles/iox-forms.css +358 -10
@@ -16,6 +16,11 @@ declare enum ControlType {
16
16
  /** A slider over a numeric range — min/max/step ride the field. */
17
17
  Range = "range",
18
18
  Radio = "radio",
19
+ /** One or several of a few visible choices — chips (the default) or a list of radios / checkboxes (premium N1).
20
+ * Its `multiple` trait picks the value's shape: a string, or a list of strings. */
21
+ Choices = "choices",
22
+ /** A date the visitor picks from the look's calendar (premium N8) — a button that reads the date in words and a
23
+ * hidden input carrying the ISO `YYYY-MM-DD` value. Never the browser's own date box. */
19
24
  Date = "date",
20
25
  Time = "time",
21
26
  File = "file",
@@ -35,7 +40,19 @@ declare enum RuleName {
35
40
  /** A checkbox that must be ticked — terms, consent. */
36
41
  Accepted = "accepted",
37
42
  FileType = "fileType",
38
- FileSize = "fileSize"
43
+ FileSize = "fileSize",
44
+ /** IMPLIED, never authored: an answer of a Choices / Radio / Dropdown must be one of the field's own options
45
+ * (premium N1) — a hand-crafted POST cannot file a value the form never offered. */
46
+ OneOf = "oneOf",
47
+ /** IMPLIED, never authored: a Date's answer is a real `YYYY-MM-DD` day (premium N8). */
48
+ ValidDate = "validDate"
49
+ }
50
+ /** How a Choices field draws its options (premium N1). */
51
+ declare enum ChoiceAppearance {
52
+ /** Each option a chip — the label is the chip; the native input inside it is visually hidden. */
53
+ Chips = "chips",
54
+ /** The options as rows of the look's own radio / checkbox (DS9.1). */
55
+ List = "list"
39
56
  }
40
57
  /** Named regexes so an author picks "Email" rather than writing one. */
41
58
  declare enum PatternPreset {
@@ -81,6 +98,8 @@ interface FieldSpec {
81
98
  options?: FieldOption[];
82
99
  optionsSource?: FieldOptionSource;
83
100
  defaultValue?: any;
101
+ /** A Choices field that takes several answers: its value is a list (premium N1). */
102
+ multiple?: boolean;
84
103
  }
85
104
  /** When a field is validated, once the author has stopped deciding it per field. */
86
105
  declare enum FeedbackTiming {
@@ -117,8 +136,15 @@ interface FormSpec {
117
136
  * validation — the single biggest reason the panel can stay empty and still validate well.
118
137
  */
119
138
  declare const IMPLIED_PATTERN: Readonly<Partial<Record<ControlType, PatternPreset>>>;
120
- /** Controls whose value is a list rather than a scalar. */
121
- declare const MULTI_VALUE_CONTROLS: ReadonlySet<ControlType>;
139
+ /**
140
+ * Whether this field's answer is a LIST rather than one value — a file list, or a Choices field that takes several
141
+ * (premium N1). Per FIELD, not per control: the same Choices control is single or multiple by its `multiple` trait.
142
+ * ONE reading for collect, validate, the runtimes' transport and the server (replaced the control-only
143
+ * `MULTI_VALUE_CONTROLS`, which nothing read).
144
+ */
145
+ declare function isMultiValueField(field: Pick<FieldSpec, 'control' | 'multiple'> | null | undefined): boolean;
146
+ /** The controls whose answers must be one of the field's own options (the implied `oneOf` rule). */
147
+ declare const OPTION_CONTROLS: ReadonlySet<ControlType>;
122
148
 
123
149
  /** Namespace for every forms validation key. Keep in sync with `en.json` / `he.json`. */
124
150
  declare const MESSAGE_NAMESPACE = "forms.validation";
@@ -127,6 +153,24 @@ declare const MESSAGE_NAMESPACE = "forms.validation";
127
153
  * can place the number wherever its grammar needs it, rather than having it baked into the string.
128
154
  */
129
155
  declare const RULE_MESSAGE_KEYS: Readonly<Record<RuleName, string>>;
156
+ /**
157
+ * What "required" says on a field you CHOOSE rather than type into (premium N1 / N8): "Choose an option", "Choose at
158
+ * least one", "Choose a date" — never "This field is required" beside a row of chips.
159
+ */
160
+ declare const CHOICE_MESSAGE_KEYS: Readonly<{
161
+ One: "forms.validation.choose";
162
+ Some: "forms.validation.chooseSome";
163
+ Date: "forms.validation.chooseDate";
164
+ }>;
165
+ /** A Date outside its Earliest / Latest. The calendar refuses those days, so this is a stale page or a hand POST. */
166
+ declare const DATE_MESSAGE_KEYS: Readonly<{
167
+ Min: "forms.validation.dateMin";
168
+ Max: "forms.validation.dateMax";
169
+ }>;
170
+ /** The submit button's own words (premium N2): what it says once its form has sent, when the author wrote none. */
171
+ declare const SUBMIT_MESSAGE_KEYS: Readonly<{
172
+ Sent: "forms.submit.sent";
173
+ }>;
130
174
  /**
131
175
  * A pattern failure deserves better than "Invalid format". Each preset gets its own key so the
132
176
  * message can say what a valid value looks like, which is the difference between a visitor fixing
@@ -158,6 +202,15 @@ interface FormResult {
158
202
  /** Keyed by field name. Empty when valid. */
159
203
  errors: Record<string, FieldError>;
160
204
  }
205
+ /**
206
+ * When "today" is, for a Date's relative bounds (premium N8). A browser leaves it out: its own local day is the
207
+ * visitor's. The SERVER passes `dateSlackDays: 1` — it cannot know the visitor's time zone, so a day either side of
208
+ * its own today is still accepted.
209
+ */
210
+ interface ValidateOptions {
211
+ today?: string;
212
+ dateSlackDays?: number;
213
+ }
161
214
  /**
162
215
  * Validate one field's value.
163
216
  *
@@ -169,12 +222,19 @@ interface FormResult {
169
222
  * number" at someone who simply left it blank — the single most common validation-UX bug.
170
223
  * The FIRST failure wins: a visitor fixes one thing at a time.
171
224
  */
172
- declare function validateField(value: unknown, spec: FieldSpec, form?: Record<string, unknown>): FieldResult;
225
+ declare function validateField(value: unknown, spec: FieldSpec, form?: Record<string, unknown>, options?: ValidateOptions): FieldResult;
173
226
  /**
174
227
  * Validate a whole submission. Every field is checked (not short-circuited at the first bad one) so
175
228
  * the visitor sees every problem at once instead of fixing them one reload at a time.
176
229
  */
177
- declare function validateForm(values: Record<string, unknown>, spec: FormSpec): FormResult;
230
+ declare function validateForm(values: Record<string, unknown>, spec: FormSpec, options?: ValidateOptions): FormResult;
231
+ /**
232
+ * The answers in the shape each field takes (premium N1): a field that takes several answers gets a LIST (one answer
233
+ * posted on its own is a list of one; nothing is an empty list). ONE reading for a host's transport and the server,
234
+ * before validation — `isMultiValueField` decides. A single-answer field is left as posted: two answers to it is the
235
+ * validator's to refuse, never quietly cut to one.
236
+ */
237
+ declare function normaliseAnswers(values: Record<string, unknown> | null | undefined, spec: Pick<FormSpec, 'fields'>): Record<string, unknown>;
178
238
  /** The name of the first field with an error, in spec order — what the browser should focus. */
179
239
  declare function firstInvalidField(spec: FormSpec, errors: Record<string, FieldError>): string | null;
180
240
 
@@ -198,6 +258,27 @@ declare const STORE_ACTION: SubmitAction;
198
258
  declare function deriveFieldName(label: string): string;
199
259
  /** Make `name` unique within `taken`, appending 2, 3, … — two "Email" fields must not collide. */
200
260
  declare function uniqueFieldName(name: string, taken: ReadonlySet<string>): string;
261
+ /** A Field's own key, before the form makes it unique: the author's override, else derived from the label. */
262
+ declare function baseFieldName(props: {
263
+ name?: unknown;
264
+ label?: unknown;
265
+ } | null | undefined): string;
266
+ /**
267
+ * THE naming rule (vectoriox/iox-ui#43): a form's fields, in document order, each its base name made unique against
268
+ * the names before it — `email`, `email2`. The spec (`collectFormSpec`), the component path's runtime and the engine's
269
+ * string renderer all name a field from here, so the input's `name` and the saved spec's key can never differ.
270
+ */
271
+ declare function uniqueFieldNames(baseNames: ReadonlyArray<string>): string[];
272
+ /** The submitted name of every Field of a Form, in document order — `uniqueFieldNames` over its fields. */
273
+ declare function formFieldNames(node: FormNodeLike | null | undefined): string[];
274
+ /** Each Field node of a Form → the name it submits under (`formFieldNames`, by node). */
275
+ declare function formFieldNameMap<N extends FormNodeLike>(node: N | null | undefined): Map<FormNodeLike, string>;
276
+ /**
277
+ * A Field's spec from its props and the name its form gave it — ONE reading for the saved spec and the component
278
+ * path's runtime (it was written twice: `IoxFieldComponent.toSpec` never carried the options source, the default or a
279
+ * Date's bounds). A Date's Earliest / Latest (`earliest` / `latest`) become its `min` / `max` rules (premium N8).
280
+ */
281
+ declare function fieldSpecOf(props: Record<string, any> | null | undefined, name: string): FieldSpec;
201
282
  /**
202
283
  * Walk a Form subtree and produce its spec.
203
284
  *
@@ -428,28 +509,45 @@ declare const FORM_FIELDS_ATTRIBUTE = "data-iox-form-fields";
428
509
  declare function clientFormSpec(spec: Pick<FormSpec, 'feedback' | 'fields'>): ClientFormSpec;
429
510
  /** The attribute's value back into a spec, or null when it is missing or not one. */
430
511
  declare function parseClientFormSpec(raw: string | null | undefined): ClientFormSpec | null;
431
- /** How a submit button wears its form's state (vectoriox/iox-ui#42). */
512
+ /** How a submit button wears its form's state (vectoriox/iox-ui#42, premium N2). */
432
513
  interface SubmitButtonView {
433
514
  busy: boolean;
434
515
  disabled: boolean;
435
516
  }
436
- /** While its form sends, a submit button is busy, and disabled unless its author turned that off — ONE rule for the
437
- * canvas's Sending state, the component path and the string path. Its "Sending…" label is the shared CSS's, keyed on
438
- * the same `data-iox-state`. */
517
+ /**
518
+ * While its form sends, a submit button is busy, and disabled unless its author turned that off; once the form has
519
+ * SENT it stays disabled, reading "Sent" with its check — nothing sends a lead twice (premium N2, the success in place).
520
+ * ONE rule for the canvas's Sending / Success states, the component path and the string path. Its labels are the shared
521
+ * CSS's, keyed on the same `data-iox-state`.
522
+ */
439
523
  declare function submitButtonView(state: FormRenderState | string | null | undefined, disableWhileSubmitting?: boolean): SubmitButtonView;
440
524
  /** On a submit button whose author turned "Disable while sending" off. */
441
525
  declare const SUBMIT_KEEP_ENABLED_ATTRIBUTE = "data-iox-keep-enabled";
442
526
  declare enum SubmitLabelClass {
443
527
  Labels = "iox-submit__labels",
444
528
  Idle = "iox-submit__idle",
445
- Busy = "iox-submit__busy"
529
+ Busy = "iox-submit__busy",
530
+ Sent = "iox-submit__sent",
531
+ Check = "iox-submit__check"
532
+ }
533
+ /** The check the Sent label draws once (premium N2 — the rare-tier beat, Emil 135/136): one stroke, `pathLength` 100 so
534
+ * the shared CSS draws it by `stroke-dashoffset` whatever its real length. */
535
+ declare const SUBMIT_CHECK_SVG: string;
536
+ declare function sentWord(lang: string | null | undefined): string;
537
+ /** A submit button's three labels: what it says at rest, while its form sends, and once it has sent. */
538
+ interface SubmitLabels {
539
+ idle: string;
540
+ /** "While sending" — none: the button keeps its idle words while sending. */
541
+ busy?: string | null;
542
+ /** "Sent" — none: the catalog's word, translated by the host (`SUBMIT_MESSAGE_KEYS.Sent`). */
543
+ sent: string;
446
544
  }
447
545
  /**
448
- * A submit button's words, as markup: its label, or — when it has a "While sending" label — both, stacked in one
449
- * cell (the button keeps the longer one's width and never jumps). The string renderer's copy of
450
- * `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
546
+ * A submit button's words, as markup: its three labels stacked in one grid cell (the button keeps the longest one's
547
+ * width and never jumps), each shown by the form's state through the shared CSS — swapped in place behind a 2px blur
548
+ * (Emil 87/139). The string renderer's copy of `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
451
549
  */
452
- declare function submitButtonLabelsHtml(label: string, loadingLabel: string | null | undefined, escape: (v: string) => string): string;
550
+ declare function submitButtonLabelsHtml(labels: SubmitLabels, escape: (v: string) => string): string;
453
551
  declare const FORM_HONEYPOT_CLASS = "iox-form__hp";
454
552
  declare const FORM_ANNOUNCER_ATTRIBUTE = "data-iox-form-announcer";
455
553
  /**
@@ -460,6 +558,44 @@ declare const FORM_ANNOUNCER_ATTRIBUTE = "data-iox-form-announcer";
460
558
  */
461
559
  declare function formChromeHtml(honeypotField: string): string;
462
560
 
561
+ /**
562
+ * THE SECOND CHANNEL (premium components N2) — "or message us on WhatsApp", the button a form keeps beside its submit,
563
+ * before AND after it sends.
564
+ *
565
+ * It is an ordinary node in the form's actions row (a ButtonBlock that links) carrying the ROLE `form.channel`, so the
566
+ * shared CSS keeps it on screen when the form succeeds and the Publishable gate can find it. Its address is the
567
+ * OWNER's fact: the builder builds it from what the author types (a number, an address, a link) and never invents one —
568
+ * inserted empty, it blocks publishing until it is filled (generation-core `auditSite`, `empty-channel`).
569
+ *
570
+ * Framework-free, like the rest of forms-core.
571
+ */
572
+ /** The role the channel's node carries (`iox-role-form-channel`). */
573
+ declare const FORM_CHANNEL_ROLE = "form.channel";
574
+ /** The role of the row that holds the submit button and the channel (`iox-role-form-actions`). */
575
+ declare const FORM_ACTIONS_ROLE = "form.actions";
576
+ declare enum ChannelKind {
577
+ WhatsApp = "whatsapp",
578
+ Phone = "phone",
579
+ Email = "email",
580
+ Link = "link"
581
+ }
582
+ declare const CHANNEL_KINDS: ReadonlyArray<ChannelKind>;
583
+ /**
584
+ * The address a channel links to, built from what the author typed — or `''` when it gives nothing a visitor could
585
+ * use (no digits, not an email). Never a guess: a WhatsApp number keeps exactly the digits typed (the panel asks for
586
+ * the country code), a link without a scheme gets `https://`.
587
+ */
588
+ declare function channelHref(kind: ChannelKind | string, input: unknown): string;
589
+ /** A channel's address back into what the panel shows: its kind and the value the author typed. */
590
+ declare function parseChannelHref(href: unknown): {
591
+ kind: ChannelKind;
592
+ value: string;
593
+ };
594
+ /** True when the channel goes somewhere a visitor can use — the gate's "filled". */
595
+ declare function isChannelFilled(href: unknown): boolean;
596
+ /** A WhatsApp or web channel opens in a new tab; a call or an email hands over to the device. */
597
+ declare function channelOpensNewTab(kind: ChannelKind | string): boolean;
598
+
463
599
  /**
464
600
  * FORM PRESETS — a complete, working form as one insert: its fields, its submit label and the words of
465
601
  * its Success and Error blocks. ONE definition (W1.1): the builder's Forms tray builds a preset from
@@ -467,11 +603,15 @@ declare function formChromeHtml(honeypotField: string): string;
467
603
  * into a composed page, so a generated contact form is the real one — never a second, drifting copy.
468
604
  * Data only: framework-free, like the rest of forms-core.
469
605
  */
606
+
470
607
  declare enum FormPresetId {
471
608
  Contact = "contact",
472
609
  Newsletter = "newsletter",
473
610
  Application = "application",
474
- Booking = "booking"
611
+ Booking = "booking",
612
+ /** Premium N2: modelled on Doron's iox.studio brief — name, a phone that is also WhatsApp, the business, what they
613
+ * need and a budget as chips, Send with WhatsApp beside it. */
614
+ Lead = "lead"
475
615
  }
476
616
  interface FormPresetField {
477
617
  label: string;
@@ -479,9 +619,21 @@ interface FormPresetField {
479
619
  required?: boolean;
480
620
  help?: string;
481
621
  placeholder?: string;
482
- /** Choices for select/radio, `value|Label` per line — the Field's `optionsText` trait. */
622
+ /** Choices for select/radio/choices, `value|Label` per line — the Field's `optionsText` trait. */
483
623
  optionsText?: string;
484
624
  rows?: number;
625
+ /** A Choices field that takes several answers (N1). */
626
+ multiple?: boolean;
627
+ /** A Choices field's appearance — `chips` (the default) or `list` (N1). */
628
+ appearance?: string;
629
+ /** A Date's first and last day — '' any · an integer: days from today · an ISO day (N8, `dates.ts`). */
630
+ earliest?: string;
631
+ latest?: string;
632
+ }
633
+ /** The second way to reach the studio, beside Send (N2). Its address is the studio's own and starts EMPTY. */
634
+ interface FormPresetChannel {
635
+ kind: `${ChannelKind}`;
636
+ label: string;
485
637
  }
486
638
  interface FormPresetDef {
487
639
  id: `${FormPresetId}`;
@@ -495,7 +647,13 @@ interface FormPresetDef {
495
647
  submitLabel: string;
496
648
  successText: string;
497
649
  errorText: string;
650
+ channel?: FormPresetChannel;
651
+ /** `false`: the AI plan wire does not offer it yet (the lead preset ships in the builder in M3; generation takes it
652
+ * up in premium M8 — the wire's enum is model-facing and does not change before then). */
653
+ inPlan?: false;
498
654
  }
655
+ /** The presets a generated plan may name (the plan wire's `form` enum) — every preset not held back by `inPlan`. */
656
+ declare function planFormPresetIds(): string[];
499
657
  declare const FORM_PRESETS: readonly FormPresetDef[];
500
658
  /** The preset with this id, or undefined. */
501
659
  declare function formPresetById(id: string | null | undefined): FormPresetDef | undefined;
@@ -543,5 +701,195 @@ interface MisplacedFormContent<N extends NestingNode = NestingNode> {
543
701
  /** Every form-content subtree a tree holds inside an interactive host, in document order. */
544
702
  declare function findFormContentInHosts<N extends NestingNode>(roots: readonly N[] | null | undefined): MisplacedFormContent<N>[];
545
703
 
546
- export { ControlType, DEFAULT_EN_MESSAGES, FIELD_GROUP_NODE_TYPE, FIELD_NODE_TYPE, FORM_ANNOUNCER_ATTRIBUTE, FORM_CONTENT_TYPES, FORM_ERROR_KEYS, FORM_FIELDS_ATTRIBUTE, FORM_HONEYPOT_CLASS, FORM_NODE_TYPE, FORM_PRESETS, FORM_STATE_ATTRIBUTE, FeedbackTiming, FormPresetId, FormRenderState, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, INTERACTIVE_HOST_TAGS, InteractiveTag, 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, SUBMIT_BUTTON_NODE_TYPE, SUBMIT_KEEP_ENABLED_ATTRIBUTE, SubmitLabelClass, addableRules, applyServerErrors, clientFormSpec, collectFieldNodes, collectFormSpec, deriveFieldName, failureAnnouncement, fieldOptionsOf, findFormContentInHosts, firstInvalidField, formChromeHtml, formContentInHost, formPresetById, interpretSubmitResponse, isFormContent, isInteractiveHost, onFieldBlur, onFieldChange, onSubmitAttempt, parseClientFormSpec, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitButtonLabelsHtml, submitButtonView, submitEndpointPath, submitOutcome, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
547
- export type { ClientFormSpec, FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormPresetDef, FormPresetField, FormResult, FormRuntimeState, FormSpec, FormTransport, MisplacedFormContent, NestingNode, RuleDescriptor, SubmitAction, SubmitButtonView, SubmitOutcome, SubmitRequest, SubmitResponse, ValidationRule };
704
+ /**
705
+ * DATES — the date picker's pure half (premium components N8) and the date rules' one reading.
706
+ *
707
+ * A Date field's value is an ISO day, `YYYY-MM-DD`, compared as a string (the format sorts as the calendar does).
708
+ * Its `min` / `max` rules take an ISO day OR an offset in days from today (`{ offsetDays: 0 }` = today), so "from
709
+ * today on" stays true tomorrow without a re-save. ONE reading for the validator (client AND iox-cms), the calendar
710
+ * (which days it refuses) and the builder's Earliest / Latest controls.
711
+ *
712
+ * The calendar model (six rows of seven, roving focus, the APG keys) lives here too, so the DOM runtime only draws
713
+ * what this decides — the canvas, the engine's component path and its string path draw the SAME month.
714
+ *
715
+ * ⚠️ Framework-free, like the rest of forms-core. Local-time `Date` objects are used only to walk the calendar; a
716
+ * day leaves this file as an ISO string.
717
+ */
718
+
719
+ /** Always six rows, so the panel never changes size between months (APG; Emil 74 — one object, one size). */
720
+ declare const CALENDAR_ROWS = 6;
721
+ /** The ISO day of a local `Date`. */
722
+ declare function isoOfDate(d: Date): string;
723
+ /** A local `Date` at midnight for an ISO day, or null when it is not one (2026-02-30 is not). */
724
+ declare function dateOfIso(iso: unknown): Date | null;
725
+ /** True for a real calendar day written `YYYY-MM-DD`. */
726
+ declare function isIsoDate(value: unknown): boolean;
727
+ /** Today, as the device that runs this sees it (the visitor's local day in a browser; the server's on iox-cms). */
728
+ declare function todayIso(now?: Date): string;
729
+ /** `iso` moved by `days` (negative goes back). */
730
+ declare function addDaysIso(iso: string, days: number): string;
731
+ /** `iso` moved by `months`, its day clamped to the target month's last (31 Jan + 1 month = 28/29 Feb). */
732
+ declare function addMonthsIso(iso: string, months: number): string;
733
+ /** `iso` held inside [min, max] (either may be absent). */
734
+ declare function clampIso(iso: string, min?: string | null, max?: string | null): string;
735
+ /** A date rule's parameter: an ISO day, or a number of days from today. */
736
+ interface DateBoundParams {
737
+ date?: string;
738
+ offsetDays?: number;
739
+ }
740
+ /**
741
+ * A Field's `earliest` / `latest` trait → the rule parameter. The trait is one string so the panel and a saved page
742
+ * stay simple: `''` = any day · an integer = that many days from today (`'0'` = today, `'30'` = a month ahead) · an
743
+ * ISO day. Anything else reads as no bound.
744
+ */
745
+ declare function parseDateBound(raw: unknown): DateBoundParams | null;
746
+ /** The `min` / `max` rules a Date field's Earliest / Latest traits give it (none for "any"). */
747
+ declare function dateBoundRules(earliest: unknown, latest: unknown): ValidationRule[];
748
+ /** True when a rule's params are a DATE bound (an ISO day or an offset), never a number bound. */
749
+ declare function isDateBoundParams(params: Record<string, any> | null | undefined): boolean;
750
+ /** The ISO day a bound names today, or null when it names none. */
751
+ declare function resolveDateBound(params: Record<string, any> | null | undefined, today: string): string | null;
752
+ /** The first and last day a Date field accepts today — the calendar's refused days and the validator's range. */
753
+ declare function fieldDateRange(field: Pick<FieldSpec, 'rules'> | null | undefined, today: string): {
754
+ min: string | null;
755
+ max: string | null;
756
+ };
757
+ /** True when the field is a date picker (the control that reads these rules as days). */
758
+ declare function isDateField(field: Pick<FieldSpec, 'control'> | null | undefined): boolean;
759
+ /**
760
+ * The first day of the week for a locale, 0 = Sunday … 6 = Saturday. `Intl.Locale(…).getWeekInfo()` (or the older
761
+ * `weekInfo` getter) when the runtime has it, else a small region table; a bare `en` reads as the US.
762
+ */
763
+ declare function weekStartFor(locale: string | null | undefined): number;
764
+ /** One cell of the month grid. */
765
+ interface CalendarDay {
766
+ iso: string;
767
+ day: number;
768
+ today: boolean;
769
+ selected: boolean;
770
+ /** Outside Earliest / Latest: shown, announced as unavailable, never picked. */
771
+ disabled: boolean;
772
+ /** The roving tab stop — exactly one day of the month has it. */
773
+ focus: boolean;
774
+ }
775
+ interface CalendarMonth {
776
+ year: number;
777
+ /** 0-based, as `Date` counts. */
778
+ month: number;
779
+ /** `CALENDAR_ROWS` × 7; null where the month has no day. */
780
+ weeks: Array<Array<CalendarDay | null>>;
781
+ /** The weekday of each column, 0 = Sunday — the header row reads these. */
782
+ weekdays: number[];
783
+ canPrev: boolean;
784
+ canNext: boolean;
785
+ }
786
+ interface CalendarOptions {
787
+ weekStart: number;
788
+ today: string;
789
+ min?: string | null;
790
+ max?: string | null;
791
+ selected?: string | null;
792
+ }
793
+ /** The month `focus` sits in, as six rows of seven — the ONE grid every renderer draws. */
794
+ declare function calendarMonth(focus: string, options: CalendarOptions): CalendarMonth;
795
+ /** The keys the grid answers (APG date picker dialog). */
796
+ declare enum CalendarKey {
797
+ Left = "ArrowLeft",
798
+ Right = "ArrowRight",
799
+ Up = "ArrowUp",
800
+ Down = "ArrowDown",
801
+ Home = "Home",
802
+ End = "End",
803
+ PageUp = "PageUp",
804
+ PageDown = "PageDown"
805
+ }
806
+ /**
807
+ * Where a key moves the focused day, held inside [min, max] — or null for a key the grid does not answer (Enter,
808
+ * Space and Escape are the runtime's: they pick or close, they never move).
809
+ */
810
+ declare function calendarKeyTarget(key: string, focus: string, options: {
811
+ weekStart: number;
812
+ rtl?: boolean;
813
+ shift?: boolean;
814
+ min?: string | null;
815
+ max?: string | null;
816
+ }): string | null;
817
+ /** The date in words, in the site's language: "Thu, 20 November" — the year only when it is not this year's. */
818
+ declare function formatDateInWords(iso: string, locale: string | null | undefined, today: string): string;
819
+ /** The calendar's own words, by the site's language — the media-core `videoWords` precedent (a string-rendered
820
+ * site has no translation service; the document's `lang` picks). */
821
+ interface DatePickerWords {
822
+ choose: string;
823
+ previousMonth: string;
824
+ nextMonth: string;
825
+ close: string;
826
+ }
827
+ declare const DATE_PICKER_WORDS: Readonly<Record<'en' | 'he', DatePickerWords>>;
828
+ /** The words for a language tag (`he-IL` → Hebrew); English for anything else. */
829
+ declare function datePickerWords(lang: string | null | undefined): DatePickerWords;
830
+
831
+ /**
832
+ * A Field as MARKUP — the string renderer's copy of `IoxFieldComponent`'s template, in ONE place (it lived in the
833
+ * engine's `layout-renderer.service.ts`, a second copy beside the component). The engine's string path writes it; the
834
+ * component mirrors it element for element, so hydration replaces like with like and the shared CSS (iox-forms.css,
835
+ * the contract's field baseline) reaches both.
836
+ *
837
+ * Choices (premium N1): a `<fieldset>` whose `<legend>` is the label, its options native radios (one answer — the
838
+ * arrow keys move and pick, with no script) or checkboxes (several), each inside a `<label>` that IS the chip; the
839
+ * input is visually hidden, never removed. Date (N8): a `<button>` that says the date in words and opens the look's
840
+ * calendar, with a hidden input carrying the ISO day — the raw SSR is the final markup (nothing shifts on hydrate).
841
+ *
842
+ * Framework-free, like the rest of forms-core: `escape` is the caller's.
843
+ */
844
+
845
+ /** The classes the markup carries — the runtimes and the shared CSS key on these, never on a literal. */
846
+ declare enum FieldClass {
847
+ Root = "iox-field",
848
+ Label = "iox-field__label",
849
+ LabelHidden = "iox-field__label--hidden",
850
+ Required = "iox-field__required",
851
+ Control = "iox-field__control",
852
+ Help = "iox-field__help",
853
+ Error = "iox-field__error",
854
+ Group = "iox-field__group",
855
+ Option = "iox-field__option",
856
+ Choices = "iox-field__choices",
857
+ ChoicesChips = "iox-field__choices--chips",
858
+ ChoicesList = "iox-field__choices--list",
859
+ Chips = "iox-field__chips",
860
+ Chip = "iox-field__chip",
861
+ ChipInput = "iox-field__control--chip",
862
+ DateButton = "iox-field__control--date",
863
+ DateValue = "iox-field__date-value",
864
+ DateIcon = "iox-field__date-icon",
865
+ DateInput = "iox-field__date-input"
866
+ }
867
+ /** On the date button: the raw Earliest / Latest (`'0'`, an ISO day), resolved by the visitor's own today. */
868
+ declare const DATE_MIN_ATTRIBUTE = "data-iox-date-min";
869
+ declare const DATE_MAX_ATTRIBUTE = "data-iox-date-max";
870
+ /** On the date's words while no day is chosen (the placeholder's colour keys on it). */
871
+ declare const DATE_EMPTY_ATTRIBUTE = "data-iox-empty";
872
+ /** The calendar glyph inside the date button: one path set, drawn in currentColor, never announced. */
873
+ declare const DATE_ICON_SVG: string;
874
+ /** The `<input type>` of a control that renders as a plain input. */
875
+ declare function inputTypeOf(control: string): string;
876
+ /** A Choices field's appearance; anything unknown reads as chips (the default). */
877
+ declare function choiceAppearanceOf(props: Record<string, any> | null | undefined): ChoiceAppearance;
878
+ interface FieldMarkupOptions {
879
+ /** The name the field submits under — its FORM's unique name for it (`formFieldNames`, vectoriox/iox-ui#43). */
880
+ name: string;
881
+ /** The control's id; the label, help and error ids derive from it. */
882
+ id: string;
883
+ /** The node's classes (`iox-node-…`, the element-style marker…). */
884
+ cls: string;
885
+ /** Anything else the host element carries, written verbatim (an entrance's style). */
886
+ attrs?: string;
887
+ /** The site's language (the date's words). */
888
+ lang?: string | null;
889
+ escape: (value: string) => string;
890
+ }
891
+ /** A Field's whole element, as the string renderer writes it. */
892
+ declare function fieldHtml(props: Record<string, any> | null | undefined, options: FieldMarkupOptions): string;
893
+
894
+ export { CALENDAR_ROWS, CHANNEL_KINDS, CHOICE_MESSAGE_KEYS, CalendarKey, ChannelKind, ChoiceAppearance, ControlType, DATE_EMPTY_ATTRIBUTE, DATE_ICON_SVG, DATE_MAX_ATTRIBUTE, DATE_MESSAGE_KEYS, DATE_MIN_ATTRIBUTE, DATE_PICKER_WORDS, DEFAULT_EN_MESSAGES, FIELD_GROUP_NODE_TYPE, FIELD_NODE_TYPE, FORM_ACTIONS_ROLE, FORM_ANNOUNCER_ATTRIBUTE, FORM_CHANNEL_ROLE, FORM_CONTENT_TYPES, FORM_ERROR_KEYS, FORM_FIELDS_ATTRIBUTE, FORM_HONEYPOT_CLASS, FORM_NODE_TYPE, FORM_PRESETS, FORM_STATE_ATTRIBUTE, FeedbackTiming, FieldClass, FormPresetId, FormRenderState, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, INTERACTIVE_HOST_TAGS, InteractiveTag, MESSAGE_NAMESPACE, OPTION_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, SUBMIT_BUTTON_NODE_TYPE, SUBMIT_CHECK_SVG, SUBMIT_KEEP_ENABLED_ATTRIBUTE, SUBMIT_MESSAGE_KEYS, SubmitLabelClass, addDaysIso, addMonthsIso, addableRules, applyServerErrors, baseFieldName, calendarKeyTarget, calendarMonth, channelHref, channelOpensNewTab, choiceAppearanceOf, clampIso, clientFormSpec, collectFieldNodes, collectFormSpec, dateBoundRules, dateOfIso, datePickerWords, deriveFieldName, failureAnnouncement, fieldDateRange, fieldHtml, fieldOptionsOf, fieldSpecOf, findFormContentInHosts, firstInvalidField, formChromeHtml, formContentInHost, formFieldNameMap, formFieldNames, formPresetById, formatDateInWords, inputTypeOf, interpretSubmitResponse, isChannelFilled, isDateBoundParams, isDateField, isFormContent, isInteractiveHost, isIsoDate, isMultiValueField, isoOfDate, normaliseAnswers, onFieldBlur, onFieldChange, onSubmitAttempt, parseChannelHref, parseClientFormSpec, parseDateBound, parseFieldOptions, planFormPresetIds, resetFormState, resolveDateBound, rulesFor, sentWord, shouldShowError, submitButtonLabelsHtml, submitButtonView, submitEndpointPath, submitOutcome, todayIso, uniqueFieldName, uniqueFieldNames, validateField, validateForm, weekStartFor, withAntiSpamFields };
895
+ export type { CalendarDay, CalendarMonth, CalendarOptions, ClientFormSpec, DateBoundParams, DatePickerWords, FieldError, FieldMarkupOptions, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormPresetChannel, FormPresetDef, FormPresetField, FormResult, FormRuntimeState, FormSpec, FormTransport, MisplacedFormContent, NestingNode, RuleDescriptor, SubmitAction, SubmitButtonView, SubmitLabels, SubmitOutcome, SubmitRequest, SubmitResponse, ValidateOptions, ValidationRule };
@@ -6929,6 +6929,9 @@ declare enum SiteFindingCode {
6929
6929
  NoHeader = "no-header",
6930
6930
  NoPhoneMenu = "no-phone-menu",
6931
6931
  FormWithoutId = "form-without-id",
6932
+ /** Premium N2: a form's second way to reach you (WhatsApp, a phone, an email) has no address yet — the studio's own
6933
+ * fact, never written for it. */
6934
+ EmptyChannel = "empty-channel",
6932
6935
  EmailNeedsMailer = "email-needs-mailer",
6933
6936
  TrackerWithoutBanner = "tracker-without-banner",
6934
6937
  NoTitle = "no-title",