@vectoriox/iox-ui 5.80.0 → 5.82.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.
@@ -22,10 +22,18 @@ export enum ElementStyleState {
22
22
  /** A composite part that is ON — the active tab (CC4a "Active"): compiles to `[data-iox-on]`, the attribute every
23
23
  * composite runtime writes. */
24
24
  On = 'on',
25
+ /** G4 — compile through the style compiler's COMPONENT_STATE_SELECTOR: `:disabled` / aria-disabled, aria-busy,
26
+ * pressed / checked / selected, the page scrolled. */
27
+ Disabled = 'disabled',
28
+ Busy = 'busy',
29
+ Selected = 'selected',
30
+ Scrolled = 'scrolled',
25
31
  }
26
32
 
27
- export const ELEMENT_STYLE_STATES: ReadonlyArray<ElementStyleState> =
28
- [ElementStyleState.Hover, ElementStyleState.Active, ElementStyleState.Focus, ElementStyleState.Current, ElementStyleState.On];
33
+ export const ELEMENT_STYLE_STATES: ReadonlyArray<ElementStyleState> = [
34
+ ElementStyleState.Hover, ElementStyleState.Active, ElementStyleState.Focus, ElementStyleState.Current, ElementStyleState.On,
35
+ ElementStyleState.Disabled, ElementStyleState.Busy, ElementStyleState.Selected, ElementStyleState.Scrolled,
36
+ ];
29
37
 
30
38
  export interface ElementStyle {
31
39
  readonly id: string;
@@ -38,6 +46,10 @@ export interface ElementStyle {
38
46
  readonly focus?: Readonly<Record<string, string>>;
39
47
  readonly current?: Readonly<Record<string, string>>;
40
48
  readonly on?: Readonly<Record<string, string>>;
49
+ readonly disabled?: Readonly<Record<string, string>>;
50
+ readonly busy?: Readonly<Record<string, string>>;
51
+ readonly selected?: Readonly<Record<string, string>>;
52
+ readonly scrolled?: Readonly<Record<string, string>>;
41
53
  }
42
54
 
43
55
  export const ELEMENT_STYLES: ReadonlyArray<ElementStyle> = SPEC_ELEMENT_STYLES;
@@ -21,13 +21,24 @@ export interface StaggerSpec {
21
21
  readonly cap?: number;
22
22
  }
23
23
 
24
- /** The delay of the `index`-th item: `stagger` ms each, the last `cap` waiting no longer than the one before. */
24
+ /** Each gap is this much of the one before (G8 — Emil 66: a uniform stagger is a metronome; the delays shrink). */
25
+ export const STAGGER_SHRINK = 0.75;
26
+ /** Only the first items stagger; the rest arrive with the sixth (Emil 67). */
27
+ export const STAGGER_MAX_ITEMS = 6;
28
+
29
+ /**
30
+ * The delay of the `index`-th item (G8): the first gap is `stagger` ms and every next one is `STAGGER_SHRINK` of the
31
+ * one before, so the group lands together instead of ticking; items from the `cap`-th on (never past the sixth) arrive
32
+ * with it. 80 ms → 0, 80, 140, 185, 219, 244. Static groups (their delays written at stamp time) and a Repeater's rows
33
+ * (`entranceDelay`) read the same curve.
34
+ */
25
35
  export function staggerDelay(spec: StaggerSpec, index = 0): number {
26
36
  const stagger = Number(spec.stagger ?? 0);
27
37
  if (!(stagger > 0)) return 0;
28
- const cap = Math.max(1, Math.floor(Number(spec.cap ?? 1)) || 1);
38
+ const cap = Math.min(STAGGER_MAX_ITEMS, Math.max(1, Math.floor(Number(spec.cap ?? 1)) || 1));
29
39
  const i = Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0;
30
- return Math.min(i, cap - 1) * stagger;
40
+ const steps = Math.min(i, cap - 1);
41
+ return Math.round(stagger * (1 - STAGGER_SHRINK ** steps) / (1 - STAGGER_SHRINK));
31
42
  }
32
43
 
33
44
  /** What an action carries, as both runtimes read it. */
@@ -1340,7 +1340,7 @@ export const SPEC_AXES = {
1340
1340
  "ease-spring": "cubic-bezier(0.2, 0, 0, 1)",
1341
1341
  "dur-spring": "140ms",
1342
1342
  "hover-lift": "-2px",
1343
- "press-scale": "1"
1343
+ "press-scale": "0.97"
1344
1344
  },
1345
1345
  "standard": {
1346
1346
  "label": "Standard",
@@ -1362,7 +1362,7 @@ export const SPEC_AXES = {
1362
1362
  "ease-spring": "cubic-bezier(0.33, 0, 0.1, 1)",
1363
1363
  "dur-spring": "400ms",
1364
1364
  "hover-lift": "0px",
1365
- "press-scale": "1"
1365
+ "press-scale": "0.98"
1366
1366
  },
1367
1367
  "lively": {
1368
1368
  "label": "Lively",
@@ -2618,7 +2618,13 @@ export const SPEC_A11Y = {
2618
2618
  "reducedTransparency": {
2619
2619
  "surface-glass": "var(--iox-surface)",
2620
2620
  "surface-glass-strong": "var(--iox-surface)",
2621
- "blur-surface": "none"
2621
+ "blur-surface": "none",
2622
+ "button-secondary-backdrop": "none",
2623
+ "card-backdrop": "none",
2624
+ "nav-backdrop": "none",
2625
+ "dialog-backdrop": "none",
2626
+ "tabbar-backdrop": "none",
2627
+ "segmented-track-backdrop": "none"
2622
2628
  }
2623
2629
  } as const;
2624
2630
 
@@ -6558,7 +6564,7 @@ export const SPEC_FLAVOR_PACKS = [
6558
6564
  "tier": "semantic",
6559
6565
  "type": "number",
6560
6566
  "group": "motion",
6561
- "value": "1"
6567
+ "value": "0.98"
6562
6568
  },
6563
6569
  {
6564
6570
  "name": "button-font",
@@ -13817,7 +13823,7 @@ export const SPEC_FLAVOR_PACKS = [
13817
13823
  "tier": "semantic",
13818
13824
  "type": "number",
13819
13825
  "group": "motion",
13820
- "value": "1"
13826
+ "value": "0.97"
13821
13827
  },
13822
13828
  {
13823
13829
  "name": "button-font",
@@ -13957,7 +13963,7 @@ export const SPEC_FLAVOR_PACKS = [
13957
13963
  "tier": "component",
13958
13964
  "type": "number",
13959
13965
  "group": "components",
13960
- "value": "1",
13966
+ "value": "0.97",
13961
13967
  "overridden": true
13962
13968
  },
13963
13969
  {
@@ -12,6 +12,7 @@ export * from './flavor-spec.generated';
12
12
  export * from './design-token.model';
13
13
  export * from './css-names';
14
14
  export * from './token-css';
15
+ export * from './theme-color';
15
16
  export * from './flavor';
16
17
  export * from './element-styles';
17
18
  export * from './adopt';
@@ -0,0 +1,41 @@
1
+ import { DesignMode } from './design-token.model';
2
+ import { DesignTokenLike } from './token-css';
3
+
4
+ /** The token the browser's own chrome takes its colour from: the page's ground. */
5
+ export const THEME_COLOR_TOKEN = 'bg';
6
+
7
+ const VAR_REF = /^var\(\s*--iox-([\w-]+)\s*(?:,\s*([^()]+))?\)$/;
8
+ /** A colour the browser's chrome can wear: opaque hex, rgb() or hsl() — never a gradient, a mix or a translucent one. */
9
+ const SOLID_COLOUR = /^(#([0-9a-f]{3}|[0-9a-f]{6})|rgb\(\s*[\d.]+\s*,?\s*[\d.]+\s*,?\s*[\d.]+\s*\)|hsl\([^()/]+\))$/i;
10
+
11
+ export interface ThemeColors {
12
+ light?: string;
13
+ dark?: string;
14
+ }
15
+
16
+ function resolveIn(map: ReadonlyMap<string, string>, value: string | undefined, depth = 0): string | undefined {
17
+ if (!value || depth > 12) return undefined;
18
+ const ref = VAR_REF.exec(value.trim());
19
+ if (!ref) return value.trim();
20
+ return resolveIn(map, map.get(ref[1]) ?? ref[2]?.trim(), depth + 1);
21
+ }
22
+
23
+ function colourFor(rows: ReadonlyArray<DesignTokenLike>, dark: boolean): string | undefined {
24
+ const map = new Map(rows.map(r => [r.name, dark ? (r.values?.dark ?? r.value) : r.value] as [string, string]));
25
+ const colour = resolveIn(map, map.get(THEME_COLOR_TOKEN));
26
+ return colour && SOLID_COLOUR.test(colour) ? colour : undefined;
27
+ }
28
+
29
+ /**
30
+ * The `theme-color` the browser's chrome takes per colour scheme (premium components G15 — mobile-native M10): the
31
+ * page's ground in each mode the site shows, resolved through the org's rows. A light or dark site answers its one
32
+ * mode; `auto` answers both (the engine writes them with `media="(prefers-color-scheme: …)"`). A value that is not a
33
+ * solid colour answers nothing — the browser keeps its own.
34
+ */
35
+ export function themeColors(rows: ReadonlyArray<DesignTokenLike> | null | undefined, mode: DesignMode | string | null | undefined = 'auto'): ThemeColors {
36
+ const list = rows ?? [];
37
+ const out: ThemeColors = {};
38
+ if (mode !== 'dark') { const light = colourFor(list, false); if (light) out.light = light; }
39
+ if (mode !== 'light') { const dark = colourFor(list, true); if (dark) out.dark = dark; }
40
+ return out;
41
+ }
@@ -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
+ }