@vectoriox/iox-ui 5.80.0 → 5.81.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.
@@ -379,6 +379,87 @@ declare function withAntiSpamFields(values: Record<string, any>, renderedAt: num
379
379
  */
380
380
  declare function interpretSubmitResponse(body: any): SubmitResponse;
381
381
 
382
+ /**
383
+ * The submit LIFECYCLE's decisions — what a form shows after the server answers, what it says when a
384
+ * submit fails, what a client needs to validate a form, and how its submit button follows its state.
385
+ *
386
+ * Two runtimes act on these and must agree: `IoxFormComponent` (the engine's component path) and the
387
+ * string-form runtime (every header / footer global and every string-rendered page —
388
+ * vectoriox/iox-client-engine#13). The builder canvas reads `submitButtonView` too, for its Sending state.
389
+ *
390
+ * ⚠️ Framework-free, like the rest of forms-core: no Angular, no DOM.
391
+ */
392
+
393
+ /** Which of the form's four states is showing — written on the form as `data-iox-state`. */
394
+ declare enum FormRenderState {
395
+ Idle = "idle",
396
+ Submitting = "submitting",
397
+ Success = "success",
398
+ Error = "error"
399
+ }
400
+ /** The attribute every renderer writes a form's state on (the canvas switcher, both live paths). */
401
+ declare const FORM_STATE_ATTRIBUTE = "data-iox-state";
402
+ /** What the form does once the server has answered. */
403
+ interface SubmitOutcome {
404
+ state: FormRenderState;
405
+ /** Per-field verdicts to put back on the fields (the form stays Idle so the visitor can fix them). */
406
+ errors?: Record<string, FieldError>;
407
+ /** A success clears the answers, so they cannot be sent twice. */
408
+ reset: boolean;
409
+ redirect?: string;
410
+ }
411
+ /**
412
+ * The server's answer → what the visitor sees. Only a confirmed success is Success; per-field verdicts
413
+ * land on the fields; anything else (a dropped network included) is the designed Error block.
414
+ */
415
+ declare function submitOutcome(response: SubmitResponse | null | undefined): SubmitOutcome;
416
+ /** What the live region says after a refused submit: how many fields need attention, or that it did not send. */
417
+ declare function failureAnnouncement(errors: Readonly<Record<string, unknown>> | null | undefined): string;
418
+ /**
419
+ * The fields a CLIENT needs to validate a form: never its actions (where submissions go — an email
420
+ * address, a webhook) and never its anti-spam settings. A string-rendered form carries this on
421
+ * `FORM_FIELDS_ATTRIBUTE`, the way a string slider carries its inputs.
422
+ */
423
+ interface ClientFormSpec {
424
+ feedback: FeedbackTiming | string;
425
+ fields: FieldSpec[];
426
+ }
427
+ declare const FORM_FIELDS_ATTRIBUTE = "data-iox-form-fields";
428
+ declare function clientFormSpec(spec: Pick<FormSpec, 'feedback' | 'fields'>): ClientFormSpec;
429
+ /** The attribute's value back into a spec, or null when it is missing or not one. */
430
+ declare function parseClientFormSpec(raw: string | null | undefined): ClientFormSpec | null;
431
+ /** How a submit button wears its form's state (vectoriox/iox-ui#42). */
432
+ interface SubmitButtonView {
433
+ busy: boolean;
434
+ disabled: boolean;
435
+ }
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`. */
439
+ declare function submitButtonView(state: FormRenderState | string | null | undefined, disableWhileSubmitting?: boolean): SubmitButtonView;
440
+ /** On a submit button whose author turned "Disable while sending" off. */
441
+ declare const SUBMIT_KEEP_ENABLED_ATTRIBUTE = "data-iox-keep-enabled";
442
+ declare enum SubmitLabelClass {
443
+ Labels = "iox-submit__labels",
444
+ Idle = "iox-submit__idle",
445
+ Busy = "iox-submit__busy"
446
+ }
447
+ /**
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.
451
+ */
452
+ declare function submitButtonLabelsHtml(label: string, loadingLabel: string | null | undefined, escape: (v: string) => string): string;
453
+ declare const FORM_HONEYPOT_CLASS = "iox-form__hp";
454
+ declare const FORM_ANNOUNCER_ATTRIBUTE = "data-iox-form-announcer";
455
+ /**
456
+ * The honeypot and the failure announcer every form holds, as markup — what `IoxFormComponent`'s template
457
+ * renders, for the string renderer. The honeypot is off-screen (some bots skip hidden inputs), out of the
458
+ * Tab order and the accessibility tree; the announcer is present from the start (a live region added at
459
+ * the moment it gains text is not reliably announced).
460
+ */
461
+ declare function formChromeHtml(honeypotField: string): string;
462
+
382
463
  /**
383
464
  * FORM PRESETS — a complete, working form as one insert: its fields, its submit label and the words of
384
465
  * its Success and Error blocks. ONE definition (W1.1): the builder's Forms tray builds a preset from
@@ -462,5 +543,5 @@ interface MisplacedFormContent<N extends NestingNode = NestingNode> {
462
543
  /** Every form-content subtree a tree holds inside an interactive host, in document order. */
463
544
  declare function findFormContentInHosts<N extends NestingNode>(roots: readonly N[] | null | undefined): MisplacedFormContent<N>[];
464
545
 
465
- export { ControlType, DEFAULT_EN_MESSAGES, FIELD_GROUP_NODE_TYPE, FIELD_NODE_TYPE, FORM_CONTENT_TYPES, FORM_ERROR_KEYS, FORM_NODE_TYPE, FORM_PRESETS, FeedbackTiming, FormPresetId, 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, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, fieldOptionsOf, findFormContentInHosts, firstInvalidField, formContentInHost, formPresetById, interpretSubmitResponse, isFormContent, isInteractiveHost, onFieldBlur, onFieldChange, onSubmitAttempt, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
466
- export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormPresetDef, FormPresetField, FormResult, FormRuntimeState, FormSpec, FormTransport, MisplacedFormContent, NestingNode, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
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 };
package/index.d.ts CHANGED
@@ -1135,6 +1135,34 @@ interface SubmitResponse {
1135
1135
  */
1136
1136
  type FormTransport = (request: SubmitRequest) => Promise<SubmitResponse>;
1137
1137
 
1138
+ /**
1139
+ * The submit LIFECYCLE's decisions — what a form shows after the server answers, what it says when a
1140
+ * submit fails, what a client needs to validate a form, and how its submit button follows its state.
1141
+ *
1142
+ * Two runtimes act on these and must agree: `IoxFormComponent` (the engine's component path) and the
1143
+ * string-form runtime (every header / footer global and every string-rendered page —
1144
+ * vectoriox/iox-client-engine#13). The builder canvas reads `submitButtonView` too, for its Sending state.
1145
+ *
1146
+ * ⚠️ Framework-free, like the rest of forms-core: no Angular, no DOM.
1147
+ */
1148
+
1149
+ /** Which of the form's four states is showing — written on the form as `data-iox-state`. */
1150
+ declare enum FormRenderState {
1151
+ Idle = "idle",
1152
+ Submitting = "submitting",
1153
+ Success = "success",
1154
+ Error = "error"
1155
+ }
1156
+ /**
1157
+ * The fields a CLIENT needs to validate a form: never its actions (where submissions go — an email
1158
+ * address, a webhook) and never its anti-spam settings. A string-rendered form carries this on
1159
+ * `FORM_FIELDS_ATTRIBUTE`, the way a string slider carries its inputs.
1160
+ */
1161
+ interface ClientFormSpec {
1162
+ feedback: FeedbackTiming | string;
1163
+ fields: FieldSpec[];
1164
+ }
1165
+
1138
1166
  /**
1139
1167
  * How a message KEY becomes text a visitor reads.
1140
1168
  *
@@ -1205,13 +1233,7 @@ declare class FormRuntimeService {
1205
1233
  declare const IOX_FORM_TRANSPORT: InjectionToken<FormTransport>;
1206
1234
  /** Where a successful submit sends the visitor. Provided by the host; absent on the canvas. */
1207
1235
  declare const IOX_FORM_NAVIGATOR: InjectionToken<(url: string) => void>;
1208
- /** Which of the form's four states the canvas / runtime is showing. */
1209
- declare enum FormRenderState {
1210
- Idle = "idle",
1211
- Submitting = "submitting",
1212
- Success = "success",
1213
- Error = "error"
1214
- }
1236
+
1215
1237
  /**
1216
1238
  * Directive-host Form: the real `<form>` IS the host — no wrapper, no inner/outer split. Children
1217
1239
  * (fields, layout, the submit button, and the two state blocks) render into it via `childrenHost`.
@@ -1238,7 +1260,11 @@ declare class IoxFormComponent implements OnChanges, OnInit {
1238
1260
  formId: string;
1239
1261
  /** Author-facing name — what the submissions inbox groups by. */
1240
1262
  name: string;
1241
- renderState: FormRenderState | string;
1263
+ /** Which state shows. Every change puts the submit buttons in it too (busy / disabled — iox-ui#42), through the
1264
+ * ONE applier the string runtime and the canvas call. */
1265
+ set renderState(state: FormRenderState | string);
1266
+ get renderState(): FormRenderState | string;
1267
+ private _renderState;
1242
1268
  feedback: FeedbackTiming | string;
1243
1269
  /**
1244
1270
  * Move focus to the first field that failed, instead of leaving the visitor to hunt for red
@@ -1287,7 +1313,8 @@ declare class IoxFormComponent implements OnChanges, OnInit {
1287
1313
  * host's, which is the one part that genuinely differs between the engine and anything else.
1288
1314
  */
1289
1315
  private send;
1290
- /** Turn the server's answer into what the visitor sees. Public so a host-owned transport can reuse it. */
1316
+ /** Turn the server's answer into what the visitor sees — the shared `submitOutcome`, which the string runtime
1317
+ * applies too. Public so a host-owned transport can reuse it. */
1291
1318
  applyResponse(response: SubmitResponse): void;
1292
1319
  /** The honeypot input's value, if the host rendered one. Absent reads as empty — never as spam. */
1293
1320
  private honeypotValue;
@@ -1400,20 +1427,21 @@ declare class IoxFieldComponent implements OnInit, OnDestroy {
1400
1427
  *
1401
1428
  * It carries no destination configuration of its own — "what happens with the data" is an ordered
1402
1429
  * action list on the FORM (decision D9), so there is nothing here to wire and nothing to get out of
1403
- * sync. The button owns only its own appearance and its busy state.
1430
+ * sync. The button owns only its own appearance.
1404
1431
  *
1405
- * While submitting it shows `loadingLabel` and is disabled, which is the whole double-submit
1406
- * defence on the client side (the server's rate limit is the real one).
1432
+ * It follows its FORM's state (vectoriox/iox-ui#42): its two labels are both in the markup and the shared
1433
+ * iox-forms.css shows "While sending" while the form's `data-iox-state` is submitting — on the canvas's Sending
1434
+ * state, the component path and the string path alike; busy and disabled are written by the ONE applier
1435
+ * (`syncSubmitButtons`) the form's runtimes call. Before, the button switched only on a `submitting` input
1436
+ * nothing ever wrote. The string renderer writes the same markup (forms-core `submitButtonLabelsHtml`).
1407
1437
  */
1408
1438
  declare class IoxSubmitButtonComponent {
1409
1439
  nodeId: string;
1410
1440
  label: string;
1411
1441
  loadingLabel: string;
1412
1442
  disableWhileSubmitting: boolean;
1413
- /** Driven by the form's lifecycle, not by the author. */
1414
- submitting: boolean;
1415
1443
  static ɵfac: i0.ɵɵFactoryDeclaration<IoxSubmitButtonComponent, never>;
1416
- static ɵcmp: i0.ɵɵComponentDeclaration<IoxSubmitButtonComponent, "button[iox-submit]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "label": { "alias": "label"; "required": false; }; "loadingLabel": { "alias": "loadingLabel"; "required": false; }; "disableWhileSubmitting": { "alias": "disableWhileSubmitting"; "required": false; }; "submitting": { "alias": "submitting"; "required": false; }; }, {}, never, never, false, never>;
1444
+ static ɵcmp: i0.ɵɵComponentDeclaration<IoxSubmitButtonComponent, "button[iox-submit]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "label": { "alias": "label"; "required": false; }; "loadingLabel": { "alias": "loadingLabel"; "required": false; }; "disableWhileSubmitting": { "alias": "disableWhileSubmitting"; "required": false; }; }, {}, never, never, false, never>;
1417
1445
  }
1418
1446
 
1419
1447
  /**
@@ -2011,6 +2039,58 @@ declare const PART_CLASS_NAMES: Record<string, Record<string, string>>;
2011
2039
  * Undefined when the type declares no such part (or no parts at all). */
2012
2040
  declare function partClassName(type: string, partKey: string): string | undefined;
2013
2041
 
2042
+ /**
2043
+ * WHERE a state rule lands — the ONE selector planner both renderers emit state and part rules through
2044
+ * (vectoriox/iox-client-engine#14). Before it the builder and the engine each kept a copy: the builder matched a
2045
+ * structural state (first / last / odd / even child) on the OUTER element, the engine on `.iox-node` — on the
2046
+ * engine's component path the INNER element, the only child of its host, so "last child" matched every item; and
2047
+ * the engine dropped every part state but Wrong / Accepted while the builder applied a part's hover at all times.
2048
+ *
2049
+ * Returns full selectors (with their dots). Framework-free.
2050
+ */
2051
+ /**
2052
+ * Properties that govern how a node sits in its PARENT's layout (flex / grid item, block child) — they live on the
2053
+ * OUTER element: the component's host, or the one element the string renderer and the canvas's collapsed hosts write.
2054
+ * Everything else is "inner". One list for both renderers (it was typed twice).
2055
+ */
2056
+ declare const OUTER_STYLE_PROPS: ReadonlySet<string>;
2057
+ /** Structural pseudo-classes — matched on the element that IS the sibling among siblings (the outer). */
2058
+ declare const STRUCTURAL_STATES: ReadonlyArray<string>;
2059
+ /** States whose pseudo-class lives on the PARENT (`parent-hover` → the parent's `:hover`). */
2060
+ declare const RELATIONAL_STATE_PSEUDO: Readonly<Record<string, string>>;
2061
+ declare function isStructuralState(state: string): boolean;
2062
+ /** What a state rule needs besides the node: its parent (relational) or its scope and trigger (targeted). */
2063
+ interface StateScope {
2064
+ parentId?: string | null;
2065
+ scopeId?: string | null;
2066
+ triggerId?: string | null;
2067
+ }
2068
+ /** A state rule's selectors: `inner` for the node's own element; `outer`, when set, for the OUTER_STYLE_PROPS. */
2069
+ interface StateRulePlan {
2070
+ inner: string;
2071
+ outer?: string;
2072
+ }
2073
+ /**
2074
+ * The selectors of `cssId`'s rule for `state`, or null when the state cannot be scoped yet (a relational state with
2075
+ * no parent, a targeted one with no scope — never an unscoped, page-global rule).
2076
+ *
2077
+ * A structural state is matched on the outer element, and its styles reach the node's own element whether that is
2078
+ * the same element (the string path, a one-element node, the canvas's collapsed hosts) or inside it (a component's
2079
+ * host, a Repeater's row wrapper); its outer props stay on the outer element. Every other state is the node's own.
2080
+ */
2081
+ declare function planStateRule(cssId: string, state: string, scope?: StateScope): StateRulePlan | null;
2082
+ /**
2083
+ * The selector of a PART's rule (a Field's label, control, help, error — `PART_CLASS_NAMES`), optionally in a state:
2084
+ * Wrong / Accepted and a component state on the NODE (that is where the class / attribute is), Hover / Pressed /
2085
+ * Focus on the PART itself. Null for a state a part does not take (structural, relational, targeted).
2086
+ */
2087
+ declare function planPartRule(cssId: string, partClass: string, state?: string | null): string | null;
2088
+ /** Split a style map into the node's own props and its OUTER_STYLE_PROPS. */
2089
+ declare function splitOuterProps(styles: Record<string, any>): {
2090
+ inner: Record<string, any>;
2091
+ outer: Record<string, any>;
2092
+ };
2093
+
2014
2094
  /** Trait names whose value Swiper reads per breakpoint. Anything else is config-wide. */
2015
2095
  declare const SWIPER_RESPONSIVE_TRAITS: readonly string[];
2016
2096
  /** `{ [minWidth]: { slidesPerView, … } }` — Swiper's own `breakpoints` shape. */
@@ -2238,6 +2318,49 @@ interface AnimateActionOptions {
2238
2318
  /** The frames are `buildInteractionKeyframes`' reversed: a reverse pass never splits text, it just fades back. */
2239
2319
  reverse?: boolean;
2240
2320
  }
2321
+ /** The id every interaction run's Animation carries — how a re-trigger finds the runs it interrupts. */
2322
+ declare const INTERACTION_ANIMATION_ID = "iox-interaction";
2323
+ /**
2324
+ * What a run that has ended leaves on its element (vectoriox/iox-client-engine#12, iox-ui#41) — ONE rule for the
2325
+ * canvas and the live site.
2326
+ *
2327
+ * `Release`: the last frame IS the element's natural state (an entrance, an exit played back, a one-shot like a
2328
+ * rotate). The run is cancelled and no frame is written inline, so the stylesheet applies again — the element
2329
+ * style's `:hover` / `:active` transforms included. Before, the last frame (`transform: translateY(0)`) was locked
2330
+ * inline on the site and kept by `fill: both` on the canvas, and an element that had entered never pressed again.
2331
+ *
2332
+ * `Hold`: the last frame HIDES the element (an exit, an entrance played back): it is kept, inline.
2333
+ */
2334
+ declare enum SettleMode {
2335
+ Release = "release",
2336
+ Hold = "hold"
2337
+ }
2338
+ declare function settleModeFor(type: string, reverse?: boolean): SettleMode;
2339
+ interface SettleOptions {
2340
+ /** Properties a runtime never writes inline — the canvas never a transform (one inside the scaled page breaks Lenis). */
2341
+ readonly skip?: ReadonlySet<string>;
2342
+ }
2343
+ /**
2344
+ * Leave `el` as a run that has ended — or one that never plays, under reduced motion — should (`SettleMode`). The
2345
+ * frame is written (Hold) or released (Release) BEFORE `anim` is cancelled, so nothing flashes between the two.
2346
+ */
2347
+ declare function settleInteraction(el: HTMLElement, frames: ReadonlyArray<InteractionKeyframe> | null | undefined, mode: SettleMode, options?: SettleOptions, anim?: Animation | null): void;
2348
+ /**
2349
+ * Interrupt (Emil 52/59/62): the interaction runs in flight on `el` over the same properties are stopped — never
2350
+ * stacked — and the new run starts from what the element shows NOW (its presentation value), not from frame 0: a
2351
+ * hover-out halfway through the hover-in turns back from where it is, a re-trigger continues. Untouched when
2352
+ * nothing is running (the run starts from its own first frame, as before).
2353
+ */
2354
+ declare function framesFromLiveValue(el: HTMLElement, frames: ReadonlyArray<InteractionKeyframe>): InteractionKeyframe[];
2355
+ /**
2356
+ * Play an element action and settle it when it ends — THE call both runtimes make for an animated action: it starts
2357
+ * from the live value (`framesFromLiveValue`, inside `animateAction`) and ends by the ONE settle rule. A run that is
2358
+ * interrupted is never settled: the run that interrupted it owns the element now.
2359
+ */
2360
+ declare function playInteraction(el: HTMLElement, action: {
2361
+ type: string;
2362
+ params?: Readonly<Record<string, unknown>> | null;
2363
+ }, frames: ReadonlyArray<InteractionKeyframe>, timing: KeyframeAnimationOptions, options?: AnimateActionOptions & SettleOptions): Animation;
2241
2364
  /**
2242
2365
  * Start an element action's animation: THE call both runtimes make. Most actions are one `element.animate`; a text
2243
2366
  * reveal plays its pieces and a clip reveal opens from the side the page's direction makes of Start and End, then takes
@@ -3226,5 +3349,69 @@ declare class GalleryLoop {
3226
3349
  private emit;
3227
3350
  }
3228
3351
 
3229
- export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RevealActionType, SCROLL_EASINGS, SELF_PLAYING_TRIGGERS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, animateAction, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, entranceStartsHidden, feelFor, finalKeyframe, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planResponsiveRules, planScroll, planSwiperOptions, playDualLayerTransition, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, viewportTransitionContext, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
3230
- export type { A11yNodeSource, AnimateActionOptions, BoundValueSpec, BreakpointDef, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageScrollStart, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionContext, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, PlannedPageTransition, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteAnimationLike, ScrollPlanContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SliderBootOptions, SliderControlElements, SliderInputs, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionPreviewLayers, TransitionTimelineInput, TransitionViewport, ValueSource, VirtualTraitGroup };
3352
+ /** What a host gives a string-rendered form: the POST (the engine's), where a redirect goes, how a message reads. */
3353
+ interface StringFormOptions {
3354
+ transport?: FormTransport | null;
3355
+ navigate?: ((url: string) => void) | null;
3356
+ messages?: FormMessageResolver;
3357
+ }
3358
+ /**
3359
+ * A form the SSR engine's string renderer wrote — every header / footer global, every string-rendered page —
3360
+ * run with the behaviour `IoxFormComponent` gives a component-path form (vectoriox/iox-client-engine#13): the
3361
+ * forms-core runtime decides when an error shows, a valid submit POSTs through the host's transport with the
3362
+ * anti-spam fields, the form moves idle → submitting → success / error on `data-iox-state` (the shared CSS shows
3363
+ * the designed block), the first problem takes focus and the failure is announced. Nothing posts natively.
3364
+ *
3365
+ * Its fields come from the form itself (`data-iox-form-fields`, the spec minus its destinations), so the
3366
+ * runtime needs no node tree. Idempotent per element.
3367
+ */
3368
+ declare class StringFormRuntime {
3369
+ private readonly form;
3370
+ private readonly spec;
3371
+ private readonly options;
3372
+ private state;
3373
+ private inFlight;
3374
+ private readonly renderedAt;
3375
+ private readonly listeners;
3376
+ private readonly messages;
3377
+ constructor(form: HTMLFormElement, spec: ClientFormSpec, options?: StringFormOptions);
3378
+ destroy(): void;
3379
+ /** The visitor's answers, keyed by the spec's field names. */
3380
+ values(): Record<string, unknown>;
3381
+ private listen;
3382
+ private fieldNameOf;
3383
+ private onEdit;
3384
+ private onLeave;
3385
+ private onSubmit;
3386
+ private send;
3387
+ private apply;
3388
+ private setRenderState;
3389
+ private fullSpec;
3390
+ private fieldElement;
3391
+ private controlsOf;
3392
+ private valueOf;
3393
+ /** Every field's error, aria-invalid, describedby and invalid class — as `IoxFieldComponent` renders them. */
3394
+ private render;
3395
+ private renderError;
3396
+ private announce;
3397
+ private focusFirstInvalid;
3398
+ private honeypotValue;
3399
+ }
3400
+ /** The runtime already running a form element, if any. */
3401
+ declare function stringFormRuntimeOf(form: Element | null | undefined): StringFormRuntime | null;
3402
+ /**
3403
+ * Run a string-rendered form (one carrying `data-iox-form-fields`) — once: a form already running is returned as is.
3404
+ * Null for an element that is not one (a component-path form never carries the attribute; it runs itself).
3405
+ */
3406
+ declare function attachStringForm(form: Element | null | undefined, options?: StringFormOptions): StringFormRuntime | null;
3407
+
3408
+ /**
3409
+ * Put every submit button of `form` in the form's state (vectoriox/iox-ui#42): busy (`aria-busy`) and — unless its
3410
+ * author turned it off — disabled while the form sends; itself again otherwise. The ONE applier: the component path's
3411
+ * form, the string runtime and the canvas's Sending state all call it, from the shared `submitButtonView`. A button
3412
+ * inside a nested form belongs to that form, not this one.
3413
+ */
3414
+ declare function syncSubmitButtons(form: Element | null | undefined, state: FormRenderState | string | null | undefined): void;
3415
+
3416
+ export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_ANIMATION_ID, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, OUTER_STYLE_PROPS, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RELATIONAL_STATE_PSEUDO, RevealActionType, SCROLL_EASINGS, SELF_PLAYING_TRIGGERS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, STRUCTURAL_STATES, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SettleMode, StringFormRuntime, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, animateAction, applyBindingPipes, applyFeel, attachStringForm, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, entranceStartsHidden, feelFor, finalKeyframe, framesFromLiveValue, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isStructuralState, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planPartRule, planResponsiveRules, planScroll, planStateRule, planSwiperOptions, playDualLayerTransition, playInteraction, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, settleInteraction, settleModeFor, sliderControlElements, sliderInputsFromProps, sliderInputsOf, splitOuterProps, stateNamesIn, statePseudoClass, stateSelectorSuffix, stringFormRuntimeOf, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, syncSubmitButtons, targetedStateSelector, viewportTransitionContext, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
3417
+ export type { A11yNodeSource, AnimateActionOptions, BoundValueSpec, BreakpointDef, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageScrollStart, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionContext, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, PlannedPageTransition, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteAnimationLike, ScrollPlanContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SettleOptions, SliderBootOptions, SliderControlElements, SliderInputs, StateRulePlan, StateScope, StateStyleMap, StringFormOptions, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionPreviewLayers, TransitionTimelineInput, TransitionViewport, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.80.0",
3
+ "version": "5.81.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -18,5 +18,6 @@ export * from './options';
18
18
  export * from './runtime';
19
19
  export * from './rule-catalog';
20
20
  export * from './transport';
21
+ export * from './submit-lifecycle';
21
22
  export * from './form-presets';
22
23
  export * from './nesting';
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The submit LIFECYCLE's decisions — what a form shows after the server answers, what it says when a
3
+ * submit fails, what a client needs to validate a form, and how its submit button follows its state.
4
+ *
5
+ * Two runtimes act on these and must agree: `IoxFormComponent` (the engine's component path) and the
6
+ * string-form runtime (every header / footer global and every string-rendered page —
7
+ * vectoriox/iox-client-engine#13). The builder canvas reads `submitButtonView` too, for its Sending state.
8
+ *
9
+ * ⚠️ Framework-free, like the rest of forms-core: no Angular, no DOM.
10
+ */
11
+
12
+ import { FeedbackTiming, FieldSpec, FormSpec } from './form.model';
13
+ import { SubmitResponse } from './transport';
14
+ import { FieldError } from './validate';
15
+
16
+ /** Which of the form's four states is showing — written on the form as `data-iox-state`. */
17
+ export enum FormRenderState {
18
+ Idle = 'idle',
19
+ Submitting = 'submitting',
20
+ Success = 'success',
21
+ Error = 'error',
22
+ }
23
+
24
+ /** The attribute every renderer writes a form's state on (the canvas switcher, both live paths). */
25
+ export const FORM_STATE_ATTRIBUTE = 'data-iox-state';
26
+
27
+ /** What the form does once the server has answered. */
28
+ export interface SubmitOutcome {
29
+ state: FormRenderState;
30
+ /** Per-field verdicts to put back on the fields (the form stays Idle so the visitor can fix them). */
31
+ errors?: Record<string, FieldError>;
32
+ /** A success clears the answers, so they cannot be sent twice. */
33
+ reset: boolean;
34
+ redirect?: string;
35
+ }
36
+
37
+ /**
38
+ * The server's answer → what the visitor sees. Only a confirmed success is Success; per-field verdicts
39
+ * land on the fields; anything else (a dropped network included) is the designed Error block.
40
+ */
41
+ export function submitOutcome(response: SubmitResponse | null | undefined): SubmitOutcome {
42
+ if (response?.ok) return { state: FormRenderState.Success, reset: true, ...(response.redirect ? { redirect: response.redirect } : {}) };
43
+ if (response?.errors && Object.keys(response.errors).length) return { state: FormRenderState.Idle, errors: response.errors, reset: false };
44
+ return { state: FormRenderState.Error, reset: false };
45
+ }
46
+
47
+ /** What the live region says after a refused submit: how many fields need attention, or that it did not send. */
48
+ export function failureAnnouncement(errors: Readonly<Record<string, unknown>> | null | undefined): string {
49
+ const count = errors ? Object.keys(errors).length : 0;
50
+ if (!count) return 'That did not send.';
51
+ return `${count} ${count === 1 ? 'field needs' : 'fields need'} attention.`;
52
+ }
53
+
54
+ /**
55
+ * The fields a CLIENT needs to validate a form: never its actions (where submissions go — an email
56
+ * address, a webhook) and never its anti-spam settings. A string-rendered form carries this on
57
+ * `FORM_FIELDS_ATTRIBUTE`, the way a string slider carries its inputs.
58
+ */
59
+ export interface ClientFormSpec {
60
+ feedback: FeedbackTiming | string;
61
+ fields: FieldSpec[];
62
+ }
63
+
64
+ export const FORM_FIELDS_ATTRIBUTE = 'data-iox-form-fields';
65
+
66
+ export function clientFormSpec(spec: Pick<FormSpec, 'feedback' | 'fields'>): ClientFormSpec {
67
+ return { feedback: spec.feedback ?? FeedbackTiming.Smart, fields: [...(spec.fields ?? [])] };
68
+ }
69
+
70
+ /** The attribute's value back into a spec, or null when it is missing or not one. */
71
+ export function parseClientFormSpec(raw: string | null | undefined): ClientFormSpec | null {
72
+ if (!raw) return null;
73
+ try {
74
+ const parsed = JSON.parse(raw);
75
+ if (!parsed || !Array.isArray(parsed.fields)) return null;
76
+ return { feedback: typeof parsed.feedback === 'string' ? parsed.feedback : FeedbackTiming.Smart, fields: parsed.fields };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /** How a submit button wears its form's state (vectoriox/iox-ui#42). */
83
+ export interface SubmitButtonView {
84
+ busy: boolean;
85
+ disabled: boolean;
86
+ }
87
+
88
+ /** While its form sends, a submit button is busy, and disabled unless its author turned that off — ONE rule for the
89
+ * canvas's Sending state, the component path and the string path. Its "Sending…" label is the shared CSS's, keyed on
90
+ * the same `data-iox-state`. */
91
+ export function submitButtonView(state: FormRenderState | string | null | undefined, disableWhileSubmitting = true): SubmitButtonView {
92
+ const busy = state === FormRenderState.Submitting;
93
+ return { busy, disabled: busy && disableWhileSubmitting !== false };
94
+ }
95
+
96
+ /** On a submit button whose author turned "Disable while sending" off. */
97
+ export const SUBMIT_KEEP_ENABLED_ATTRIBUTE = 'data-iox-keep-enabled';
98
+
99
+ export enum SubmitLabelClass {
100
+ Labels = 'iox-submit__labels',
101
+ Idle = 'iox-submit__idle',
102
+ Busy = 'iox-submit__busy',
103
+ }
104
+
105
+ /**
106
+ * A submit button's words, as markup: its label, or — when it has a "While sending" label — both, stacked in one
107
+ * cell (the button keeps the longer one's width and never jumps). The string renderer's copy of
108
+ * `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
109
+ */
110
+ export function submitButtonLabelsHtml(label: string, loadingLabel: string | null | undefined, escape: (v: string) => string): string {
111
+ if (!loadingLabel) return escape(label);
112
+ return `<span class="${SubmitLabelClass.Labels}"><span class="${SubmitLabelClass.Idle}">${escape(label)}</span>` +
113
+ `<span class="${SubmitLabelClass.Busy}">${escape(loadingLabel)}</span></span>`;
114
+ }
115
+
116
+ export const FORM_HONEYPOT_CLASS = 'iox-form__hp';
117
+ export const FORM_ANNOUNCER_ATTRIBUTE = 'data-iox-form-announcer';
118
+
119
+ /**
120
+ * The honeypot and the failure announcer every form holds, as markup — what `IoxFormComponent`'s template
121
+ * renders, for the string renderer. The honeypot is off-screen (some bots skip hidden inputs), out of the
122
+ * Tab order and the accessibility tree; the announcer is present from the start (a live region added at
123
+ * the moment it gains text is not reliably announced).
124
+ */
125
+ export function formChromeHtml(honeypotField: string): string {
126
+ return `<div class="${FORM_HONEYPOT_CLASS}" aria-hidden="true"><input type="text" name="${honeypotField}" tabindex="-1" autocomplete="off"></div>` +
127
+ `<div class="${FORM_HONEYPOT_CLASS}" ${FORM_ANNOUNCER_ATTRIBUTE} role="status" aria-live="assertive" aria-atomic="true"></div>`;
128
+ }
@@ -49,6 +49,21 @@
49
49
  .iox-field__error { margin: 4px 0 0; }
50
50
  .iox-submit { font: inherit; border: none; cursor: pointer; }
51
51
 
52
+ /* The submit button follows its FORM's state (vectoriox/iox-ui#42). Its two labels (forms-core
53
+ submitButtonLabelsHtml / IoxSubmitButtonComponent) share one grid cell, so the button keeps the longer one's
54
+ width and never jumps; the form's data-iox-state picks the one that shows — the canvas's Sending state, the
55
+ component path and the string path alike. `visibility` (not display) keeps the hidden one out of the
56
+ accessible name. Busy and disabled are written by syncSubmitButtons. */
57
+ .iox-submit__labels { display: inline-grid; }
58
+ .iox-submit__labels > * { grid-area: 1 / 1; }
59
+ .iox-form:not([data-iox-state="submitting"]) .iox-submit__busy,
60
+ .iox-form[data-iox-state="submitting"] .iox-submit__idle { visibility: hidden; }
61
+ .iox-submit[aria-busy="true"] { cursor: progress; }
62
+
63
+ /* The honeypot and the failure announcer (forms-core `formChromeHtml`, IoxFormComponent's template): off-screen
64
+ rather than display:none — some bots skip hidden inputs, and a live region must stay in the tree. */
65
+ .iox-form__hp { position: absolute; inset-inline-start: -9999px; width: 1px; height: 1px; overflow: hidden; }
66
+
52
67
  /* Visually hidden, still announced — a label may be hidden, never deleted. */
53
68
  .iox-field__label--hidden {
54
69
  position: absolute; width: 1px; height: 1px; margin: -1px;