@vectoriox/iox-ui 5.27.0 → 5.28.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.
@@ -208,6 +208,18 @@ declare function collectFormSpec(node: FormNodeLike | null | undefined): FormSpe
208
208
  /** Every Field in this subtree, in document order, not descending into a nested Form. */
209
209
  declare function collectFieldNodes(nodes: readonly FormNodeLike[]): FormNodeLike[];
210
210
 
211
+ /**
212
+ * A Field's `optionsText` trait — the author's choices, one per line — as options. ONE parser for the
213
+ * builder canvas, both engine paths and iox-cms: until 2026-10-19 nothing parsed the trait, so a Radio
214
+ * group or a Dropdown from the tray rendered no choices at all on the canvas and on the live site.
215
+ */
216
+ declare function parseFieldOptions(text: unknown): FieldOption[];
217
+ /** The choices a saved Field offers: an explicit `options` list wins, else its `optionsText` parsed. */
218
+ declare function fieldOptionsOf(props: {
219
+ options?: unknown;
220
+ optionsText?: unknown;
221
+ } | null | undefined): FieldOption[];
222
+
211
223
  /** Everything the runtime needs to remember. Immutable — each transition returns a new state. */
212
224
  interface FormRuntimeState {
213
225
  /** Field names the visitor has left (blurred) at least once. */
@@ -343,7 +355,11 @@ type FormTransport = (request: SubmitRequest) => Promise<SubmitResponse>;
343
355
  * The public submit path. Built here rather than interpolated at each call site, so the engine and
344
356
  * any future host cannot disagree about it — and so changing it is one edit.
345
357
  *
346
- * Deliberately relative and WITHOUT a leading slash: the engine mounts under a per-org base href.
358
+ * ⚠️ ROOT-relative (leading slash), never base-relative. A live site is served under a per-org
359
+ * `<base href="/<label>/">`, so a relative `api/…` resolved to `/<label>/api/…` — a path the reverse
360
+ * proxy (`/api` → iox-cms, everything else → the engine) hands to the ENGINE, which answers a POST with
361
+ * 404: no visitor submission ever reached iox-cms from an org path (found 2026-10-19 by
362
+ * e2e:forminsert:real). The org is already in the path segment after `site/`.
347
363
  */
348
364
  declare function submitEndpointPath(orgId: string, formId: string): string;
349
365
  /**
@@ -363,5 +379,5 @@ declare function withAntiSpamFields(values: Record<string, any>, renderedAt: num
363
379
  */
364
380
  declare function interpretSubmitResponse(body: any): SubmitResponse;
365
381
 
366
- export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, firstInvalidField, interpretSubmitResponse, onFieldBlur, onFieldChange, onSubmitAttempt, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
382
+ export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, fieldOptionsOf, firstInvalidField, interpretSubmitResponse, onFieldBlur, onFieldChange, onSubmitAttempt, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
367
383
  export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormResult, FormRuntimeState, FormSpec, FormTransport, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
package/index.d.ts CHANGED
@@ -1189,7 +1189,17 @@ declare class IoxFieldComponent implements OnInit, OnDestroy {
1189
1189
  rangeMax: number | string;
1190
1190
  rangeStep: number | string;
1191
1191
  accept: string;
1192
- options: FieldOption[];
1192
+ /** Explicit choices (bound data, a hand-built node). Wins over `optionsText`. */
1193
+ set options(value: FieldOption[] | null | undefined);
1194
+ get options(): FieldOption[];
1195
+ /** The author's choices, one per line (`value|Label`) — the Field's trait. A SETTER: the builder
1196
+ * assigns traits directly, with no ngOnChanges. */
1197
+ set optionsText(value: string | null | undefined);
1198
+ get optionsText(): string;
1199
+ /** What the radio group / dropdown renders: the shared forms-core reading of the two inputs above. */
1200
+ choices: FieldOption[];
1201
+ private _options;
1202
+ private _optionsText;
1193
1203
  value: any;
1194
1204
  /** Rules beyond `required` — carried on the node, interpreted by forms-core. */
1195
1205
  rules: ValidationRule[];
@@ -1225,6 +1235,7 @@ declare class IoxFieldComponent implements OnInit, OnDestroy {
1225
1235
  /** The message a visitor reads. Translation is the consumer's, via IOX_FORM_MESSAGES. */
1226
1236
  get errorText(): string;
1227
1237
  private toSpec;
1238
+ private refreshChoices;
1228
1239
  /** `file`, `date`, `number`… map straight through; everything else is a text input. */
1229
1240
  get inputType(): string;
1230
1241
  /** Help and error are BOTH announced, in that order, when both are present. */
@@ -1232,7 +1243,7 @@ declare class IoxFieldComponent implements OnInit, OnDestroy {
1232
1243
  emit(value: any): void;
1233
1244
  touch(): void;
1234
1245
  static ɵfac: i0.ɵɵFactoryDeclaration<IoxFieldComponent, [{ optional: true; }, null]>;
1235
- static ɵcmp: i0.ɵɵComponentDeclaration<IoxFieldComponent, "div[iox-field]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "name": { "alias": "name"; "required": false; }; "label": { "alias": "label"; "required": false; }; "control": { "alias": "control"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "help": { "alias": "help"; "required": false; }; "required": { "alias": "required"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "labelHidden": { "alias": "labelHidden"; "required": false; }; "requiredMark": { "alias": "requiredMark"; "required": false; }; "rows": { "alias": "rows"; "required": false; }; "rangeMin": { "alias": "rangeMin"; "required": false; }; "rangeMax": { "alias": "rangeMax"; "required": false; }; "rangeStep": { "alias": "rangeStep"; "required": false; }; "accept": { "alias": "accept"; "required": false; }; "options": { "alias": "options"; "required": false; }; "value": { "alias": "value"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; }; "designErrorText": { "alias": "designErrorText"; "required": false; }; "showValid": { "alias": "showValid"; "required": false; }; }, { "valueChange": "valueChange"; "touched": "touched"; }, never, never, false, never>;
1246
+ static ɵcmp: i0.ɵɵComponentDeclaration<IoxFieldComponent, "div[iox-field]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "name": { "alias": "name"; "required": false; }; "label": { "alias": "label"; "required": false; }; "control": { "alias": "control"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "help": { "alias": "help"; "required": false; }; "required": { "alias": "required"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "labelHidden": { "alias": "labelHidden"; "required": false; }; "requiredMark": { "alias": "requiredMark"; "required": false; }; "rows": { "alias": "rows"; "required": false; }; "rangeMin": { "alias": "rangeMin"; "required": false; }; "rangeMax": { "alias": "rangeMax"; "required": false; }; "rangeStep": { "alias": "rangeStep"; "required": false; }; "accept": { "alias": "accept"; "required": false; }; "options": { "alias": "options"; "required": false; }; "optionsText": { "alias": "optionsText"; "required": false; }; "value": { "alias": "value"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; }; "designErrorText": { "alias": "designErrorText"; "required": false; }; "showValid": { "alias": "showValid"; "required": false; }; }, { "valueChange": "valueChange"; "touched": "touched"; }, never, never, false, never>;
1236
1247
  }
1237
1248
 
1238
1249
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.27.0",
3
+ "version": "5.28.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -1811,7 +1811,8 @@ export const SPEC_FIELD_CLASS_BASELINE = {
1811
1811
  ".iox-es-field .iox-field__control:focus-visible": {
1812
1812
  "border-color": "var(--iox-accent-text)",
1813
1813
  "box-shadow": "var(--iox-field-focus)",
1814
- "outline": "none"
1814
+ "outline": "2px solid transparent",
1815
+ "outline-offset": "2px"
1815
1816
  },
1816
1817
  ".iox-es-field .iox-field__label": {
1817
1818
  "font-family": "var(--iox-font-body)",
@@ -1819,6 +1820,12 @@ export const SPEC_FIELD_CLASS_BASELINE = {
1819
1820
  "font-weight": "var(--iox-field-label-weight)",
1820
1821
  "color": "var(--iox-text)"
1821
1822
  },
1823
+ ".iox-es-field .iox-field__option": {
1824
+ "font-family": "var(--iox-font-body)",
1825
+ "font-size": "var(--iox-size-body)",
1826
+ "color": "var(--iox-text)",
1827
+ "cursor": "pointer"
1828
+ },
1822
1829
  ".iox-es-field .iox-field__help": {
1823
1830
  "font-size": "var(--iox-size-small)",
1824
1831
  "color": "var(--iox-text-muted)"
@@ -1834,11 +1841,121 @@ export const SPEC_FIELD_CLASS_BASELINE = {
1834
1841
  "color": "var(--iox-text-muted)",
1835
1842
  "opacity": "1"
1836
1843
  },
1837
- ".iox-es-field .iox-field__control:where([type='checkbox'], [type='radio'], [type='range'])": {
1844
+ ".iox-es-field .iox-field__control:disabled": {
1845
+ "opacity": "0.55",
1846
+ "cursor": "not-allowed"
1847
+ },
1848
+ ".iox-es-field .iox-field__control:where(.iox-field__control--switch, [type='range'])": {
1838
1849
  "border": "none",
1839
1850
  "padding": "0",
1840
- "background": "transparent",
1841
- "accent-color": "var(--iox-accent-text)"
1851
+ "background": "transparent"
1852
+ },
1853
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio'])": {
1854
+ "appearance": "none",
1855
+ "-webkit-appearance": "none",
1856
+ "display": "inline-grid",
1857
+ "place-content": "center",
1858
+ "flex-shrink": "0",
1859
+ "margin": "0",
1860
+ "padding": "0",
1861
+ "inline-size": "1.25em",
1862
+ "block-size": "1.25em",
1863
+ "vertical-align": "middle",
1864
+ "cursor": "pointer",
1865
+ "background": "var(--iox-surface)",
1866
+ "border-color": "var(--iox-line-strong)",
1867
+ "border-radius": "var(--iox-radius-xs)",
1868
+ "transition": "background var(--iox-dur-fast) var(--iox-ease-standard), border-color var(--iox-dur-fast) var(--iox-ease-standard), box-shadow var(--iox-dur-fast) var(--iox-ease-standard)"
1869
+ },
1870
+ ".iox-es-field .iox-field__control[type='radio']": {
1871
+ "border-radius": "50%"
1872
+ },
1873
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio'])::before": {
1874
+ "content": "''",
1875
+ "inline-size": "0.28em",
1876
+ "block-size": "0.56em",
1877
+ "border-style": "solid",
1878
+ "border-color": "var(--iox-accent-ink)",
1879
+ "border-width": "0 0.14em 0.14em 0",
1880
+ "transform": "translateY(-0.08em) rotate(45deg) scale(0)",
1881
+ "transition": "transform var(--iox-dur-fast) var(--iox-ease-standard)"
1882
+ },
1883
+ ".iox-es-field .iox-field__control[type='radio']::before": {
1884
+ "inline-size": "0",
1885
+ "block-size": "0",
1886
+ "border-width": "0.25em",
1887
+ "border-radius": "50%",
1888
+ "transform": "scale(0)"
1889
+ },
1890
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):hover": {
1891
+ "border-color": "var(--iox-accent-text)"
1892
+ },
1893
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):checked": {
1894
+ "background": "var(--iox-accent)",
1895
+ "border-color": "var(--iox-accent-text)"
1896
+ },
1897
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):checked:hover": {
1898
+ "background": "var(--iox-accent-hover)"
1899
+ },
1900
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):checked::before": {
1901
+ "transform": "translateY(-0.08em) rotate(45deg) scale(1)"
1902
+ },
1903
+ ".iox-es-field .iox-field__control[type='radio']:checked::before": {
1904
+ "transform": "scale(1)"
1905
+ },
1906
+ ".iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):focus-visible": {
1907
+ "box-shadow": "var(--iox-focus-ring)"
1908
+ },
1909
+ ".iox-es-field .iox-field__control:where([type='range'])": {
1910
+ "appearance": "none",
1911
+ "-webkit-appearance": "none",
1912
+ "inline-size": "100%",
1913
+ "block-size": "1.25em",
1914
+ "cursor": "pointer"
1915
+ },
1916
+ ".iox-es-field .iox-field__control:where([type='range'])::-webkit-slider-runnable-track": {
1917
+ "block-size": "0.25em",
1918
+ "border-radius": "var(--iox-radius-pill)",
1919
+ "background": "var(--iox-line-strong)",
1920
+ "outline": "1px solid transparent"
1921
+ },
1922
+ ".iox-es-field .iox-field__control:where([type='range'])::-moz-range-track": {
1923
+ "block-size": "0.25em",
1924
+ "border-radius": "var(--iox-radius-pill)",
1925
+ "background": "var(--iox-line-strong)",
1926
+ "outline": "1px solid transparent"
1927
+ },
1928
+ ".iox-es-field .iox-field__control:where([type='range'])::-webkit-slider-thumb": {
1929
+ "appearance": "none",
1930
+ "-webkit-appearance": "none",
1931
+ "inline-size": "1.25em",
1932
+ "block-size": "1.25em",
1933
+ "border-radius": "50%",
1934
+ "background": "var(--iox-accent-text)",
1935
+ "border": "2px solid var(--iox-bg)",
1936
+ "box-sizing": "border-box",
1937
+ "cursor": "pointer",
1938
+ "margin-block-start": "-0.5em"
1939
+ },
1940
+ ".iox-es-field .iox-field__control:where([type='range'])::-moz-range-thumb": {
1941
+ "appearance": "none",
1942
+ "-webkit-appearance": "none",
1943
+ "inline-size": "1.25em",
1944
+ "block-size": "1.25em",
1945
+ "border-radius": "50%",
1946
+ "background": "var(--iox-accent-text)",
1947
+ "border": "2px solid var(--iox-bg)",
1948
+ "box-sizing": "border-box",
1949
+ "cursor": "pointer"
1950
+ },
1951
+ ".iox-es-field .iox-field__control:where([type='range']):focus-visible": {
1952
+ "box-shadow": "none"
1953
+ },
1954
+ ".iox-es-field .iox-field__control:where([type='range']):focus-visible::-webkit-slider-thumb": {
1955
+ "box-shadow": "var(--iox-focus-ring)"
1956
+ },
1957
+ ".iox-es-field .iox-field__control:where([type='range']):focus-visible::-moz-range-thumb": {
1958
+ "box-shadow": "var(--iox-focus-ring)"
1842
1959
  },
1843
1960
  ".iox-es-field .iox-field__control--switch": {
1844
1961
  "background": "var(--iox-text-muted)",
@@ -2044,9 +2161,40 @@ export const SPEC_CONTRAST_AUDIT = {
2044
2161
  "min": 3
2045
2162
  },
2046
2163
  {
2047
- "part": "checkbox · radio · range",
2164
+ "part": "checkbox · radio edge",
2165
+ "fg": "var(--iox-line-strong)",
2166
+ "min": 3
2167
+ },
2168
+ {
2169
+ "part": "checkbox · radio edge on its fill",
2170
+ "fg": "var(--iox-line-strong)",
2171
+ "on": "var(--iox-surface)",
2172
+ "min": 3
2173
+ },
2174
+ {
2175
+ "part": "checkbox · radio checked",
2176
+ "fg": [
2177
+ "var(--iox-accent-text)",
2178
+ "var(--iox-accent)"
2179
+ ],
2180
+ "any": true,
2181
+ "min": 3
2182
+ },
2183
+ {
2184
+ "part": "check mark · radio dot",
2185
+ "fg": "var(--iox-accent-ink)",
2186
+ "on": "var(--iox-accent)",
2187
+ "min": 3
2188
+ },
2189
+ {
2190
+ "part": "hover edge · range thumb",
2048
2191
  "fg": "var(--iox-accent-text)",
2049
2192
  "min": 3
2193
+ },
2194
+ {
2195
+ "part": "range track",
2196
+ "fg": "var(--iox-line-strong)",
2197
+ "min": 3
2050
2198
  }
2051
2199
  ]
2052
2200
  } as const;
@@ -15,6 +15,7 @@ import {
15
15
  SubmitAction,
16
16
  ValidationRule,
17
17
  } from './form.model';
18
+ import { fieldOptionsOf } from './options';
18
19
 
19
20
  /** The minimum a node must look like. Structural on purpose: this file knows no renderer. */
20
21
  export interface FormNodeLike {
@@ -117,7 +118,8 @@ function toFieldSpec(node: FormNodeLike, taken: ReadonlySet<string>): FieldSpec
117
118
  if (props['required']) spec.required = true;
118
119
  const rules = normaliseRules(props['rules']);
119
120
  if (rules.length) spec.rules = rules;
120
- if (Array.isArray(props['options']) && props['options'].length) spec.options = props['options'];
121
+ const options = fieldOptionsOf(props);
122
+ if (options.length) spec.options = options;
121
123
  if (props['optionsSource']) spec.optionsSource = props['optionsSource'];
122
124
  if (props['defaultValue'] !== undefined && props['defaultValue'] !== '') {
123
125
  spec.defaultValue = props['defaultValue'];
@@ -0,0 +1,29 @@
1
+ import { FieldOption } from './form.model';
2
+
3
+ /** `value|Label` separates a submitted value from what the visitor reads; a bare line is both. */
4
+ const OPTION_SEPARATOR = '|';
5
+
6
+ /**
7
+ * A Field's `optionsText` trait — the author's choices, one per line — as options. ONE parser for the
8
+ * builder canvas, both engine paths and iox-cms: until 2026-10-19 nothing parsed the trait, so a Radio
9
+ * group or a Dropdown from the tray rendered no choices at all on the canvas and on the live site.
10
+ */
11
+ export function parseFieldOptions(text: unknown): FieldOption[] {
12
+ if (typeof text !== 'string') return [];
13
+ const out: FieldOption[] = [];
14
+ for (const line of text.split(/\r?\n/)) {
15
+ const raw = line.trim();
16
+ if (!raw) continue;
17
+ const at = raw.indexOf(OPTION_SEPARATOR);
18
+ const value = (at === -1 ? raw : raw.slice(0, at)).trim();
19
+ const label = (at === -1 ? raw : raw.slice(at + 1)).trim() || value;
20
+ if (value) out.push({ value, label });
21
+ }
22
+ return out;
23
+ }
24
+
25
+ /** The choices a saved Field offers: an explicit `options` list wins, else its `optionsText` parsed. */
26
+ export function fieldOptionsOf(props: { options?: unknown; optionsText?: unknown } | null | undefined): FieldOption[] {
27
+ if (Array.isArray(props?.options) && props!.options.length) return props!.options as FieldOption[];
28
+ return parseFieldOptions(props?.optionsText);
29
+ }
@@ -14,6 +14,7 @@ export * from './form.model';
14
14
  export * from './messages';
15
15
  export * from './validate';
16
16
  export * from './collect';
17
+ export * from './options';
17
18
  export * from './runtime';
18
19
  export * from './rule-catalog';
19
20
  export * from './transport';
@@ -49,10 +49,14 @@ export type FormTransport = (request: SubmitRequest) => Promise<SubmitResponse>;
49
49
  * The public submit path. Built here rather than interpolated at each call site, so the engine and
50
50
  * any future host cannot disagree about it — and so changing it is one edit.
51
51
  *
52
- * Deliberately relative and WITHOUT a leading slash: the engine mounts under a per-org base href.
52
+ * ⚠️ ROOT-relative (leading slash), never base-relative. A live site is served under a per-org
53
+ * `<base href="/<label>/">`, so a relative `api/…` resolved to `/<label>/api/…` — a path the reverse
54
+ * proxy (`/api` → iox-cms, everything else → the engine) hands to the ENGINE, which answers a POST with
55
+ * 404: no visitor submission ever reached iox-cms from an org path (found 2026-10-19 by
56
+ * e2e:forminsert:real). The org is already in the path segment after `site/`.
53
57
  */
54
58
  export function submitEndpointPath(orgId: string, formId: string): string {
55
- return `api/site/${encodeURIComponent(orgId)}/forms/${encodeURIComponent(formId)}/submit`;
59
+ return `/api/site/${encodeURIComponent(orgId)}/forms/${encodeURIComponent(formId)}/submit`;
56
60
  }
57
61
 
58
62
  /**
@@ -119,7 +119,8 @@
119
119
  .iox-es-field .iox-field__control:focus-visible {
120
120
  border-color: var(--iox-accent-text);
121
121
  box-shadow: var(--iox-field-focus);
122
- outline: none;
122
+ outline: 2px solid transparent;
123
+ outline-offset: 2px;
123
124
  }
124
125
  .iox-es-field .iox-field__label {
125
126
  font-family: var(--iox-font-body);
@@ -127,6 +128,13 @@
127
128
  font-weight: var(--iox-field-label-weight);
128
129
  color: var(--iox-text);
129
130
  }
131
+ /* A radio's choice reads like the label beside it: the look's body type in the look's text. */
132
+ .iox-es-field .iox-field__option {
133
+ font-family: var(--iox-font-body);
134
+ font-size: var(--iox-size-body);
135
+ color: var(--iox-text);
136
+ cursor: pointer;
137
+ }
130
138
  .iox-es-field .iox-field__help {
131
139
  font-size: var(--iox-size-small);
132
140
  color: var(--iox-text-muted);
@@ -139,21 +147,141 @@
139
147
  border-color: var(--iox-danger);
140
148
  }
141
149
  /* DS9 (contrast audit): the placeholder is themed (the UA's #757575 read 1:1 in a Brand section); the
142
- native controls, the focus edge and the switch read `accent-text` — the accent GATED as text on every
150
+ focus edge, the switch and the range thumb read `accent-text` — the accent GATED as text on every
143
151
  ground — never the `accent` fill (Pop's orange on its yellow panel: 2.13:1); the off track is
144
- `text-muted` under a `bg` thumb (the decorative `line` it wore read 1.06:1). WCAG 1.4.11: 3:1. */
152
+ `text-muted` under a `bg` thumb (the decorative `line` it wore read 1.06:1). WCAG 1.4.11: 3:1.
153
+ Focus: the transparent outline is what forced-colors mode paints once it drops the box-shadow ring. */
145
154
  .iox-es-field .iox-field__control::placeholder {
146
155
  color: var(--iox-text-muted);
147
156
  opacity: 1;
148
157
  }
149
- /* Native-appearance controls (checkbox, radio, range — and the switch, a checkbox) draw themselves:
150
- the box baseline must not frame them. `:where()` keeps this at the baseline's specificity, so the
151
- switch rules below still paint the track. */
152
- .iox-es-field .iox-field__control:where([type='checkbox'], [type='radio'], [type='range']) {
158
+ .iox-es-field .iox-field__control:disabled {
159
+ opacity: 0.55;
160
+ cursor: not-allowed;
161
+ }
162
+ /* The switch and the range draw themselves: the box baseline must not frame them. `:where()` keeps this
163
+ at the baseline's specificity, so the switch rules below still paint the track. */
164
+ .iox-es-field .iox-field__control:where(.iox-field__control--switch, [type='range']) {
153
165
  border: none;
154
166
  padding: 0;
155
167
  background: transparent;
156
- accent-color: var(--iox-accent-text);
168
+ }
169
+ /* Checkbox and radio, drawn by the look (2026-10-19 — `accent-color` only TINTED the browser's own box,
170
+ so every flavor showed a browser default). 1.25em of the control's own size (the body step) · the
171
+ field's edge width in `line-strong` (≥ 3:1 on every ground) · the surface fill · `radius-xs` (a radio
172
+ is round) · checked: the `accent` fill, an `accent-text` edge and a mark in `accent-ink` (≥ 4.5:1 on
173
+ the fill). The mark is a BORDER — a rotated corner, a radio's dot a zero-size circle of border — so
174
+ forced-colors mode repaints it in the system ink (a background mark would vanish). Not mirrored in
175
+ RTL: a check mark is not a direction. Every motion reads dur-fast, which reduced motion sets to 0ms. */
176
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']) {
177
+ appearance: none;
178
+ -webkit-appearance: none;
179
+ display: inline-grid;
180
+ place-content: center;
181
+ flex-shrink: 0;
182
+ margin: 0;
183
+ padding: 0;
184
+ inline-size: 1.25em;
185
+ block-size: 1.25em;
186
+ vertical-align: middle;
187
+ cursor: pointer;
188
+ background: var(--iox-surface);
189
+ border-color: var(--iox-line-strong);
190
+ border-radius: var(--iox-radius-xs);
191
+ transition: background var(--iox-dur-fast) var(--iox-ease-standard), border-color var(--iox-dur-fast) var(--iox-ease-standard), box-shadow var(--iox-dur-fast) var(--iox-ease-standard);
192
+ }
193
+ .iox-es-field .iox-field__control[type='radio'] {
194
+ border-radius: 50%;
195
+ }
196
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio'])::before {
197
+ content: '';
198
+ inline-size: 0.28em;
199
+ block-size: 0.56em;
200
+ border-style: solid;
201
+ border-color: var(--iox-accent-ink);
202
+ border-width: 0 0.14em 0.14em 0;
203
+ transform: translateY(-0.08em) rotate(45deg) scale(0);
204
+ transition: transform var(--iox-dur-fast) var(--iox-ease-standard);
205
+ }
206
+ .iox-es-field .iox-field__control[type='radio']::before {
207
+ inline-size: 0;
208
+ block-size: 0;
209
+ border-width: 0.25em;
210
+ border-radius: 50%;
211
+ transform: scale(0);
212
+ }
213
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):hover {
214
+ border-color: var(--iox-accent-text);
215
+ }
216
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):checked {
217
+ background: var(--iox-accent);
218
+ border-color: var(--iox-accent-text);
219
+ }
220
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):checked:hover {
221
+ background: var(--iox-accent-hover);
222
+ }
223
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):checked::before {
224
+ transform: translateY(-0.08em) rotate(45deg) scale(1);
225
+ }
226
+ .iox-es-field .iox-field__control[type='radio']:checked::before {
227
+ transform: scale(1);
228
+ }
229
+ .iox-es-field .iox-field__control:where([type='checkbox']:not(.iox-field__control--switch), [type='radio']):focus-visible {
230
+ box-shadow: var(--iox-focus-ring);
231
+ }
232
+ /* Range: the track is `line-strong`, the thumb `accent-text` on a `bg` ring, the focus ring on the THUMB.
233
+ One rule per vendor pseudo-element — a selector list mixing -webkit- and -moz- is dropped whole by
234
+ the engine that does not know the other. */
235
+ .iox-es-field .iox-field__control:where([type='range']) {
236
+ appearance: none;
237
+ -webkit-appearance: none;
238
+ inline-size: 100%;
239
+ block-size: 1.25em;
240
+ cursor: pointer;
241
+ }
242
+ .iox-es-field .iox-field__control:where([type='range'])::-webkit-slider-runnable-track {
243
+ block-size: 0.25em;
244
+ border-radius: var(--iox-radius-pill);
245
+ background: var(--iox-line-strong);
246
+ outline: 1px solid transparent;
247
+ }
248
+ .iox-es-field .iox-field__control:where([type='range'])::-moz-range-track {
249
+ block-size: 0.25em;
250
+ border-radius: var(--iox-radius-pill);
251
+ background: var(--iox-line-strong);
252
+ outline: 1px solid transparent;
253
+ }
254
+ .iox-es-field .iox-field__control:where([type='range'])::-webkit-slider-thumb {
255
+ appearance: none;
256
+ -webkit-appearance: none;
257
+ inline-size: 1.25em;
258
+ block-size: 1.25em;
259
+ border-radius: 50%;
260
+ background: var(--iox-accent-text);
261
+ border: 2px solid var(--iox-bg);
262
+ box-sizing: border-box;
263
+ cursor: pointer;
264
+ margin-block-start: -0.5em;
265
+ }
266
+ .iox-es-field .iox-field__control:where([type='range'])::-moz-range-thumb {
267
+ appearance: none;
268
+ -webkit-appearance: none;
269
+ inline-size: 1.25em;
270
+ block-size: 1.25em;
271
+ border-radius: 50%;
272
+ background: var(--iox-accent-text);
273
+ border: 2px solid var(--iox-bg);
274
+ box-sizing: border-box;
275
+ cursor: pointer;
276
+ }
277
+ .iox-es-field .iox-field__control:where([type='range']):focus-visible {
278
+ box-shadow: none;
279
+ }
280
+ .iox-es-field .iox-field__control:where([type='range']):focus-visible::-webkit-slider-thumb {
281
+ box-shadow: var(--iox-focus-ring);
282
+ }
283
+ .iox-es-field .iox-field__control:where([type='range']):focus-visible::-moz-range-thumb {
284
+ box-shadow: var(--iox-focus-ring);
157
285
  }
158
286
  .iox-es-field .iox-field__control--switch {
159
287
  background: var(--iox-text-muted);