@cssints/headless 0.19.0 → 0.20.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.
package/dist/index.d.mts CHANGED
@@ -1030,6 +1030,94 @@ export interface Checkbox {
1030
1030
  * pass `value` + `onValueChange` to own the state.
1031
1031
  */
1032
1032
  export declare const createCheckbox: (options?: CheckboxOptions) => Checkbox;
1033
+ export interface FieldOptions {
1034
+ /** Base of every id the field makes (`<id>-control`, `-label`, `-hint`, `-error`). */
1035
+ readonly id?: string | undefined;
1036
+ readonly required?: boolean | undefined;
1037
+ /**
1038
+ * `true` marks the control invalid; a string does too and is the error message
1039
+ * (unless `error` is set). `false` forces valid: an `error` is then not shown,
1040
+ * so a host can pass `touched() && failed()` and keep the message ready.
1041
+ * Left out, the field is invalid exactly when it has an error message.
1042
+ */
1043
+ readonly invalid?: boolean | string | undefined;
1044
+ /** Help text; absent (or empty) means the hint element is hidden and not referenced. */
1045
+ readonly hint?: string | undefined;
1046
+ /** The error message; absent (or empty) means no error element. */
1047
+ readonly error?: string | undefined;
1048
+ /**
1049
+ * Ids of elements the host owns, appended to `aria-describedby` after the
1050
+ * field's own hint and error: a form-level error that belongs to several
1051
+ * controls ("the passwords do not match" names both). One id or a list;
1052
+ * empty strings are dropped, and the attribute is absent when nothing is left.
1053
+ * The host keeps that element rendered, and decides whether it is hidden.
1054
+ */
1055
+ readonly describedby?: string | readonly string[] | undefined;
1056
+ }
1057
+ /** Spread onto the form control (`input`, `textarea`, `select`, or a custom widget). */
1058
+ export interface FieldControlProps {
1059
+ readonly id: string;
1060
+ /** The ids of the hint and the error that are shown, hint first, then the host's `describedby`; absent when there are none. */
1061
+ readonly "aria-describedby": string | undefined;
1062
+ readonly "aria-invalid": "true" | undefined;
1063
+ /** For a control that is not native (a native one has `required`). */
1064
+ readonly "aria-required": "true" | undefined;
1065
+ readonly required: true | undefined;
1066
+ }
1067
+ export interface FieldLabelProps {
1068
+ readonly id: string;
1069
+ readonly for: string;
1070
+ }
1071
+ /** Spread onto the hint and the error element; render both, always. */
1072
+ export interface FieldMessageProps {
1073
+ readonly id: string;
1074
+ readonly hidden: boolean;
1075
+ }
1076
+ /** The error element also announces itself: `role="alert"` while it is shown. */
1077
+ export interface FieldErrorProps extends FieldMessageProps {
1078
+ readonly role: "alert" | undefined;
1079
+ }
1080
+ export interface Field {
1081
+ readonly invalid: () => boolean;
1082
+ /** The hint, or `undefined` when there is none. */
1083
+ readonly hint: () => string | undefined;
1084
+ /** The error shown: `undefined` unless the field is invalid and has a message. */
1085
+ readonly error: () => string | undefined;
1086
+ /**
1087
+ * The ids `aria-describedby` lists now: hint, then error (never an id that is
1088
+ * hidden), then the host's `describedby`.
1089
+ */
1090
+ readonly describedIds: () => readonly string[];
1091
+ readonly controlProps: FieldControlProps;
1092
+ readonly labelProps: FieldLabelProps;
1093
+ readonly hintProps: FieldMessageProps;
1094
+ readonly errorProps: FieldErrorProps;
1095
+ }
1096
+ /**
1097
+ * The ARIA contract of a form field: a label, a control, a hint and an error.
1098
+ * There is no APG pattern for it (APG has no "form field"; the nearest is the
1099
+ * label and description rules of the accessible name computation), so the
1100
+ * contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
1101
+ * `aria-required`) and HTML (`label[for]`, `required`), and from the
1102
+ * majority of Base UI Field, react-aria TextField and ariakit FormControl,
1103
+ * which list the description and the error message together. It is not checked
1104
+ * against a Quint spec; `test/field.browser.test.tsx` is its oracle.
1105
+ *
1106
+ * Decisions: the error does not replace the hint, both are listed, hint first
1107
+ * (the hint is what tells the user how to fix the error); an error implies
1108
+ * invalid and `invalid: false` overrides it; a hidden message is never
1109
+ * referenced, so `aria-describedby` has no dangling id. The error element is
1110
+ * `role="alert"` only while it is shown (a hidden element announces nothing, and
1111
+ * one that appears is announced); the hint is never an alert.
1112
+ *
1113
+ * `describedby` lists host-owned ids after the field's own: one refusal that
1114
+ * belongs to two controls is one form-level element (with its own `role`), named
1115
+ * by both. The field cannot know whether that element is shown: the host
1116
+ * passes the id only while it is.
1117
+ *
1118
+ * Options are read when an attribute is read: pass getters, never destructure.
1119
+ */
1120
+ export declare const createField: (options?: FieldOptions) => Field;
1033
1121
  export type CheckboxGroupFocus = "none" | "parent" | "c0" | "c1" | "c2";
1034
1122
  export type ParentChecked = "true" | "false" | "mixed";
1035
1123
  export interface CheckboxGroupOptions<T = unknown> {
@@ -1043,6 +1131,22 @@ export interface CheckboxGroupOptions<T = unknown> {
1043
1131
  readonly parent?: boolean;
1044
1132
  /** Remember the last mixed combination for the controller's third activation. */
1045
1133
  readonly remember?: boolean;
1134
+ /** Base of the group's ids (`<id>-control`, `-error`); a unique one when absent. */
1135
+ readonly id?: string;
1136
+ /** The group's name, as `aria-label`: a `role="group"` needs one. */
1137
+ readonly label?: string | undefined;
1138
+ /** The id of the element that names the group (`aria-labelledby`); wins over `label`. */
1139
+ readonly labelledby?: string | undefined;
1140
+ /** Ids of elements the host owns that describe the group, after the group's own error id. */
1141
+ readonly describedby?: string | undefined;
1142
+ /**
1143
+ * `true` marks the group invalid; a string does too and is the error message
1144
+ * (unless `error` is set). `false` forces valid. Left out, the group is invalid
1145
+ * exactly when it has an error message. Same contract as `createField`.
1146
+ */
1147
+ readonly invalid?: boolean | string | undefined;
1148
+ /** The error message; absent (or empty) means no error element. */
1149
+ readonly error?: string | undefined;
1046
1150
  /**
1047
1151
  * A request to change the checked subset, called when the intent differs from
1048
1152
  * the current value. Uncontrolled it is accepted; controlled the host may
@@ -1070,9 +1174,18 @@ export interface CheckboxGroupChildProps {
1070
1174
  readonly onFocus: () => void;
1071
1175
  }
1072
1176
  export interface CheckboxGroupProps {
1177
+ /** The group element: a host's label can name it, and a controller can `aria-controls` it. */
1178
+ readonly id: string;
1073
1179
  readonly role: "group";
1180
+ readonly "aria-label": string | undefined;
1181
+ readonly "aria-labelledby": string | undefined;
1182
+ /** The error id while one is shown, then the host's `describedby`. */
1183
+ readonly "aria-describedby": string | undefined;
1184
+ readonly "aria-invalid": "true" | undefined;
1074
1185
  readonly onKeyDown: (event: KeyboardEvent) => void;
1075
1186
  }
1187
+ /** The error element: render it always, it is `hidden` while there is no error. */
1188
+ export type CheckboxGroupErrorProps = FieldErrorProps;
1076
1189
  export interface CheckboxGroup<T = unknown> {
1077
1190
  readonly children: Accessor<ReadonlyArray<boolean>>;
1078
1191
  readonly memory: Accessor<ReadonlyArray<boolean>>;
@@ -1080,6 +1193,14 @@ export interface CheckboxGroup<T = unknown> {
1080
1193
  readonly focused: Accessor<CheckboxGroupFocus>;
1081
1194
  readonly parentChecked: Accessor<ParentChecked>;
1082
1195
  readonly checkedValues: () => ReadonlyArray<T>;
1196
+ /** The error shown: `undefined` unless the group is invalid and has a message. */
1197
+ readonly error: () => string | undefined;
1198
+ readonly invalid: () => boolean;
1199
+ /**
1200
+ * The ids `aria-describedby` lists now: the error (never while it is hidden),
1201
+ * then the host's `describedby`.
1202
+ */
1203
+ readonly describedIds: () => readonly string[];
1083
1204
  readonly click: (target: CheckboxGroupFocus) => void;
1084
1205
  readonly keySpace: () => void;
1085
1206
  readonly keyTab: () => void;
@@ -1088,6 +1209,7 @@ export interface CheckboxGroup<T = unknown> {
1088
1209
  readonly setRemember: (value: boolean) => void;
1089
1210
  readonly setChildren: (children: ReadonlyArray<boolean>) => void;
1090
1211
  readonly groupProps: CheckboxGroupProps;
1212
+ readonly errorProps: CheckboxGroupErrorProps;
1091
1213
  readonly parentProps: CheckboxGroupParentProps;
1092
1214
  readonly childProps: (index: 0 | 1 | 2) => CheckboxGroupChildProps;
1093
1215
  }
@@ -1103,7 +1225,17 @@ export interface CheckboxGroup<T = unknown> {
1103
1225
  * pass `value` + `onValueChange` to own the checked subset. The checked set is
1104
1226
  * packed internally so it rides the scalar `createControllable`.
1105
1227
  *
1106
- * Behaviour is checked against `specs/apg/checkbox-group.qnt` by the bridge.
1228
+ * The name, the description and the error are `createField`'s contract on the
1229
+ * `role="group"` element (`aria-label` / `aria-labelledby`, `aria-describedby`,
1230
+ * `aria-invalid`, an error element that is `role="alert"` while it is shown), so
1231
+ * a group reads like the fields around it. The group is not a `<label for>`
1232
+ * target: a host names a visible label element's id with `labelledby`, or passes
1233
+ * `label` without one.
1234
+ *
1235
+ * Behaviour is checked against `specs/apg/checkbox-group.qnt` by the bridge. That
1236
+ * model leaves labelling out on purpose ("shift+Tab, accessible names,
1237
+ * fieldset/legend or role=group labelling" in its header), so the field contract
1238
+ * is checked by plain assertions in `test/checkbox-group.browser.test.tsx`.
1107
1239
  */
1108
1240
  export declare const createCheckboxGroup: <T = unknown>(options: CheckboxGroupOptions<T>) => CheckboxGroup<T>;
1109
1241
  /**
@@ -1294,6 +1426,215 @@ export interface DialogModal {
1294
1426
  * are requests the host may ignore.
1295
1427
  */
1296
1428
  export declare const createDialogModal: (options?: DialogModalOptions) => DialogModal;
1429
+ export interface StatusOptions {
1430
+ /** Rendered on `statusProps`; generated when absent. */
1431
+ readonly id?: string | undefined;
1432
+ }
1433
+ /** Spread onto the `role=status` live region. Render it once, always. */
1434
+ export interface StatusProps {
1435
+ readonly id: string;
1436
+ readonly role: "status";
1437
+ readonly "aria-live": "polite";
1438
+ readonly "aria-atomic": "true";
1439
+ }
1440
+ export interface Status {
1441
+ /** The text currently shown, or `undefined` before the first `show`. */
1442
+ readonly message: Accessor<string | undefined>;
1443
+ /** Writes the message, replacing the previous one; nothing is queued. */
1444
+ readonly show: (text: string) => void;
1445
+ readonly clear: () => void;
1446
+ readonly statusProps: StatusProps;
1447
+ }
1448
+ /**
1449
+ * APG Alert's polite sibling: one `role=status` live region whose text the host
1450
+ * replaces ("Link copied", "Saved"). It is not a queue: a second message shows
1451
+ * in place of the first, so a burst reads as the last state, not as N alerts.
1452
+ * The region is rendered once and stays mounted, which is what makes a change
1453
+ * of its text announced.
1454
+ *
1455
+ * A status is never for errors: an error that needs attention now is
1456
+ * `createAlert` (`role=alert`, assertive), and a message with a title, a
1457
+ * description and a timeout is `createToast`.
1458
+ */
1459
+ export declare const createStatus: (options?: StatusOptions) => Status;
1460
+ /** The HTML `autocomplete` tokens worth naming; the attribute also takes an arbitrary token. */
1461
+ export type AutoCompleteToken = "off" | "on" | "name" | "given-name" | "family-name" | "username" | "email" | "tel" | "url" | "organization" | "street-address" | "postal-code" | "current-password" | "new-password" | "one-time-code" | "nickname";
1462
+ export interface TextInputOptions extends FieldOptions {
1463
+ readonly type?: "text" | "email" | "tel" | "url" | "search" | "password" | undefined;
1464
+ /** The HTML token list: the browser's own autofill is the accessible path. */
1465
+ readonly autoComplete?: AutoCompleteToken | undefined;
1466
+ readonly spellCheck?: boolean | undefined;
1467
+ readonly autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined;
1468
+ readonly inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
1469
+ readonly maxLength?: number | undefined;
1470
+ readonly readOnly?: boolean | undefined;
1471
+ readonly disabled?: boolean | undefined;
1472
+ readonly name?: string | undefined;
1473
+ readonly dir?: "ltr" | "rtl" | "auto" | undefined;
1474
+ readonly placeholder?: string | undefined;
1475
+ /** Change and input both land here; the host decides what a keystroke means. */
1476
+ readonly onValueChange?: ((value: string) => void) | undefined;
1477
+ }
1478
+ export interface TextAreaOptions extends TextInputOptions {
1479
+ readonly rows?: number | undefined;
1480
+ readonly wrap?: "soft" | "hard" | "off" | undefined;
1481
+ readonly resize?: "none" | "vertical" | "horizontal" | "both" | undefined;
1482
+ }
1483
+ export interface TextControlProps {
1484
+ readonly id: string;
1485
+ readonly "aria-describedby": string | undefined;
1486
+ readonly "aria-invalid": "true" | undefined;
1487
+ readonly "aria-required": "true" | undefined;
1488
+ readonly required: true | undefined;
1489
+ readonly type: TextInputOptions["type"];
1490
+ readonly autoComplete: AutoCompleteToken | undefined;
1491
+ readonly spellcheck: "true" | "false" | undefined;
1492
+ readonly autocapitalize: TextInputOptions["autoCapitalize"];
1493
+ readonly inputmode: TextInputOptions["inputMode"];
1494
+ readonly maxlength: number | undefined;
1495
+ readonly readOnly: true | undefined;
1496
+ readonly disabled: true | undefined;
1497
+ readonly name: string | undefined;
1498
+ readonly dir: "ltr" | "rtl" | "auto" | undefined;
1499
+ readonly placeholder: string | undefined;
1500
+ readonly onInput: (event: {
1501
+ currentTarget: {
1502
+ value: string;
1503
+ };
1504
+ }) => void;
1505
+ }
1506
+ export interface TextAreaControlProps extends TextControlProps {
1507
+ readonly rows: number | undefined;
1508
+ readonly wrap: TextAreaOptions["wrap"];
1509
+ readonly style: {
1510
+ readonly resize: "none" | "vertical" | "horizontal" | "both";
1511
+ } | undefined;
1512
+ }
1513
+ /** The field and the control, one object: every prop the element needs. */
1514
+ export interface TextInput extends Field {
1515
+ readonly controlProps: TextControlProps;
1516
+ }
1517
+ export interface TextArea extends Field {
1518
+ readonly controlProps: TextAreaControlProps;
1519
+ }
1520
+ /**
1521
+ * A text input with the field contract attached: label, hint, error (announced),
1522
+ * `aria-describedby` and `aria-invalid` come from `createField`; this adds the
1523
+ * props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
1524
+ * disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
1525
+ * no primitive re-implements text editing, which is why there is no key
1526
+ * handling here).
1527
+ *
1528
+ * Both `required` and `aria-required` are emitted: the native attribute is what
1529
+ * the browser enforces, the ARIA one is what a custom widget reading the same
1530
+ * props needs. A host that validates itself passes `noValidate` on the form
1531
+ * rather than dropping the attribute.
1532
+ */
1533
+ export declare const createTextInput: (options?: TextInputOptions) => TextInput;
1534
+ /**
1535
+ * The same contract for a textarea, plus rows, wrap and resize. `resize` is a
1536
+ * style because CSS has no attribute for it; `none` is the value a layout that
1537
+ * owns its own height wants.
1538
+ */
1539
+ export declare const createTextArea: (options?: TextAreaOptions) => TextArea;
1540
+ /** One command the host offers; `group` only picks the heading the host renders. */
1541
+ export interface PaletteCommand {
1542
+ readonly id: string;
1543
+ readonly label: string;
1544
+ readonly group?: string | undefined;
1545
+ readonly keywords?: ReadonlyArray<string> | undefined;
1546
+ readonly run: () => void | Promise<void>;
1547
+ }
1548
+ /** A command that survived the filter, in host order. */
1549
+ export interface PaletteResult {
1550
+ readonly id: string;
1551
+ readonly label: string;
1552
+ readonly group: string | undefined;
1553
+ }
1554
+ /** Every string the host puts on screen; override what you translate. */
1555
+ export interface CommandPaletteLabels {
1556
+ /** The dialog's name; `titleProps` heads it and the listbox is named by it. */
1557
+ readonly title: string;
1558
+ readonly placeholder: string;
1559
+ readonly empty: string;
1560
+ /** The result count the polite region announces, once per query. */
1561
+ readonly results: (count: number) => string;
1562
+ }
1563
+ export interface CommandPaletteOptions {
1564
+ readonly commands: ReadonlyArray<PaletteCommand>;
1565
+ /** Controlled: the host owns the open state and this is the only thing that changes it. */
1566
+ readonly open?: boolean | undefined;
1567
+ /** A request to change the open state, called when the intent differs. */
1568
+ readonly onOpenChange?: ((open: boolean) => void) | undefined;
1569
+ /** The `createHotkeys` chord `attach` binds; `"mod+k"` by default. */
1570
+ readonly hotkey?: string | undefined;
1571
+ readonly labels?: Partial<CommandPaletteLabels> | undefined;
1572
+ }
1573
+ export interface CommandPalette {
1574
+ readonly open: Accessor<boolean>;
1575
+ readonly openPalette: () => void;
1576
+ readonly closePalette: () => void;
1577
+ readonly query: Accessor<string>;
1578
+ readonly setQuery: (text: string) => void;
1579
+ /** Index into `results()`; `-1` = the search field holds the highlight. */
1580
+ readonly active: Accessor<number>;
1581
+ readonly setActive: (index: number) => void;
1582
+ readonly results: Accessor<ReadonlyArray<PaletteResult>>;
1583
+ readonly labels: CommandPaletteLabels;
1584
+ /** The live region's text; render it inside `statusProps`. */
1585
+ readonly status: Accessor<string | undefined>;
1586
+ readonly inputProps: TextControlProps & {
1587
+ readonly role: "searchbox";
1588
+ readonly value: string;
1589
+ readonly "aria-controls": string;
1590
+ readonly "aria-activedescendant": string | undefined;
1591
+ readonly onKeyDown: (event: KeyboardEvent) => void;
1592
+ };
1593
+ readonly listProps: {
1594
+ readonly id: string;
1595
+ readonly role: "listbox";
1596
+ readonly "aria-labelledby": string;
1597
+ /** A pointer click keeps the focus in the search field. */
1598
+ readonly onMouseDown: (event: MouseEvent) => void;
1599
+ };
1600
+ readonly optionProps: (id: string) => {
1601
+ readonly id: string;
1602
+ readonly role: "option";
1603
+ readonly "aria-selected": "true" | "false";
1604
+ readonly tabindex: -1;
1605
+ readonly onClick: () => void;
1606
+ readonly onMouseEnter: () => void;
1607
+ };
1608
+ readonly emptyProps: {
1609
+ readonly hidden: boolean;
1610
+ };
1611
+ readonly statusProps: StatusProps;
1612
+ readonly dialogProps: DialogContentProps;
1613
+ readonly backdropProps: DialogBackdropProps;
1614
+ readonly titleProps: {
1615
+ readonly id: string;
1616
+ };
1617
+ /** Runs the active command and closes the palette; a no-op with no highlight. */
1618
+ readonly runActive: () => void | Promise<void>;
1619
+ /** Binds the hotkey on the document; call once and call the teardown on cleanup. */
1620
+ readonly attach: () => () => void;
1621
+ }
1622
+ /**
1623
+ * A command palette: `createDialogModal` (a modal dialog, announced as one),
1624
+ * `createTextInput` (a `searchbox`, not a combobox — choosing runs a command,
1625
+ * it does not complete the field) and one filtered list. Focus stays in the
1626
+ * field; `aria-activedescendant` names the highlighted option, and ↓ on the
1627
+ * last / ↑ on the first return the highlight to the field. Home/End are menu
1628
+ * keys only while the field is empty, so the caret keeps them while typing.
1629
+ * Filtering is a plain case-insensitive substring match on the label and the
1630
+ * keywords, no fuzzy scoring: a palette is short and stable, and ranking
1631
+ * (recency, scopes) is a policy the host owns.
1632
+ *
1633
+ * The host's: rendering and grouping (the `group` field, headings, the CSS),
1634
+ * async commands (a rejected `run` is the host's to report), and any close
1635
+ * other than Escape / a command. `runActive` closes before it runs.
1636
+ */
1637
+ export declare const createCommandPalette: (options: CommandPaletteOptions) => CommandPalette;
1297
1638
  export interface ConfirmOptions {
1298
1639
  readonly id?: string | undefined;
1299
1640
  /** Called with `true` when a request opens the dialog and `false` when it settles. */
@@ -1684,94 +2025,6 @@ export interface Feed {
1684
2025
  * request; `loadStart` / `loadFail` are adapter lifecycle events.
1685
2026
  */
1686
2027
  export declare const createFeed: (options?: FeedOptions) => Feed;
1687
- export interface FieldOptions {
1688
- /** Base of every id the field makes (`<id>-control`, `-label`, `-hint`, `-error`). */
1689
- readonly id?: string | undefined;
1690
- readonly required?: boolean | undefined;
1691
- /**
1692
- * `true` marks the control invalid; a string does too and is the error message
1693
- * (unless `error` is set). `false` forces valid: an `error` is then not shown,
1694
- * so a host can pass `touched() && failed()` and keep the message ready.
1695
- * Left out, the field is invalid exactly when it has an error message.
1696
- */
1697
- readonly invalid?: boolean | string | undefined;
1698
- /** Help text; absent (or empty) means the hint element is hidden and not referenced. */
1699
- readonly hint?: string | undefined;
1700
- /** The error message; absent (or empty) means no error element. */
1701
- readonly error?: string | undefined;
1702
- /**
1703
- * Ids of elements the host owns, appended to `aria-describedby` after the
1704
- * field's own hint and error: a form-level error that belongs to several
1705
- * controls ("the passwords do not match" names both). One id or a list;
1706
- * empty strings are dropped, and the attribute is absent when nothing is left.
1707
- * The host keeps that element rendered, and decides whether it is hidden.
1708
- */
1709
- readonly describedby?: string | readonly string[] | undefined;
1710
- }
1711
- /** Spread onto the form control (`input`, `textarea`, `select`, or a custom widget). */
1712
- export interface FieldControlProps {
1713
- readonly id: string;
1714
- /** The ids of the hint and the error that are shown, hint first, then the host's `describedby`; absent when there are none. */
1715
- readonly "aria-describedby": string | undefined;
1716
- readonly "aria-invalid": "true" | undefined;
1717
- /** For a control that is not native (a native one has `required`). */
1718
- readonly "aria-required": "true" | undefined;
1719
- readonly required: true | undefined;
1720
- }
1721
- export interface FieldLabelProps {
1722
- readonly id: string;
1723
- readonly for: string;
1724
- }
1725
- /** Spread onto the hint and the error element; render both, always. */
1726
- export interface FieldMessageProps {
1727
- readonly id: string;
1728
- readonly hidden: boolean;
1729
- }
1730
- /** The error element also announces itself: `role="alert"` while it is shown. */
1731
- export interface FieldErrorProps extends FieldMessageProps {
1732
- readonly role: "alert" | undefined;
1733
- }
1734
- export interface Field {
1735
- readonly invalid: () => boolean;
1736
- /** The hint, or `undefined` when there is none. */
1737
- readonly hint: () => string | undefined;
1738
- /** The error shown: `undefined` unless the field is invalid and has a message. */
1739
- readonly error: () => string | undefined;
1740
- /**
1741
- * The ids `aria-describedby` lists now: hint, then error (never an id that is
1742
- * hidden), then the host's `describedby`.
1743
- */
1744
- readonly describedIds: () => readonly string[];
1745
- readonly controlProps: FieldControlProps;
1746
- readonly labelProps: FieldLabelProps;
1747
- readonly hintProps: FieldMessageProps;
1748
- readonly errorProps: FieldErrorProps;
1749
- }
1750
- /**
1751
- * The ARIA contract of a form field: a label, a control, a hint and an error.
1752
- * There is no APG pattern for it (APG has no "form field"; the nearest is the
1753
- * label and description rules of the accessible name computation), so the
1754
- * contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
1755
- * `aria-required`) and HTML (`label[for]`, `required`), and from the
1756
- * majority of Base UI Field, react-aria TextField and ariakit FormControl,
1757
- * which list the description and the error message together. It is not checked
1758
- * against a Quint spec; `test/field.browser.test.tsx` is its oracle.
1759
- *
1760
- * Decisions: the error does not replace the hint, both are listed, hint first
1761
- * (the hint is what tells the user how to fix the error); an error implies
1762
- * invalid and `invalid: false` overrides it; a hidden message is never
1763
- * referenced, so `aria-describedby` has no dangling id. The error element is
1764
- * `role="alert"` only while it is shown (a hidden element announces nothing, and
1765
- * one that appears is announced); the hint is never an alert.
1766
- *
1767
- * `describedby` lists host-owned ids after the field's own: one refusal that
1768
- * belongs to two controls is one form-level element (with its own `role`), named
1769
- * by both. The field cannot know whether that element is shown: the host
1770
- * passes the id only while it is.
1771
- *
1772
- * Options are read when an attribute is read: pass getters, never destructure.
1773
- */
1774
- export declare const createField: (options?: FieldOptions) => Field;
1775
2028
  export interface PaginationLabels {
1776
2029
  /** The navigation landmark's name; defaults to "Pagination". */
1777
2030
  readonly nav?: string | undefined;
@@ -2296,6 +2549,107 @@ export interface Hotkeys {
2296
2549
  * browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
2297
2550
  */
2298
2551
  export declare const createHotkeys: (options?: HotkeysOptions) => Hotkeys;
2552
+ /** How a drag turns the knob: the angle around the centre, a straight slide, or whichever the drag starts as. */
2553
+ export type KnobGesture = "spin" | "slide" | "both";
2554
+ export interface KnobLabels {
2555
+ /** The knob's accessible name (`aria-label`); "Knob" by default. */
2556
+ readonly knob?: string | undefined;
2557
+ }
2558
+ export interface KnobOptions {
2559
+ readonly min?: number | undefined;
2560
+ readonly max?: number | undefined;
2561
+ readonly step?: number | undefined;
2562
+ /** Controlled: the host owns the value and this is the only thing that changes it. */
2563
+ readonly value?: number | undefined;
2564
+ /** Starting value when uncontrolled (the middle of the range by default). */
2565
+ readonly defaultValue?: number | undefined;
2566
+ /**
2567
+ * `false` (default): clamp at `min` / `max`. `true`: a continuous dial — the
2568
+ * value wraps around (past `max` comes back at `min`), so a full turn of the
2569
+ * skin is a full cycle.
2570
+ */
2571
+ readonly wrap?: boolean | undefined;
2572
+ /** Which drag turns the knob; "both" (default) locks per drag from the first movement. */
2573
+ readonly gesture?: KnobGesture | undefined;
2574
+ readonly labels?: KnobLabels | undefined;
2575
+ /** The spoken value (`aria-valuetext`); `String` by default. */
2576
+ readonly format?: ((value: number) => string) | undefined;
2577
+ /** Base for the generated value region id. */
2578
+ readonly id?: string | undefined;
2579
+ /**
2580
+ * A request to change the value, called when the intent differs from the
2581
+ * current value. Uncontrolled it is accepted; controlled the host may ignore it.
2582
+ */
2583
+ readonly onValueChange?: ((value: number) => void) | undefined;
2584
+ }
2585
+ /** Spread onto the knob element, the focusable control. */
2586
+ export interface KnobProps {
2587
+ readonly role: "slider";
2588
+ readonly tabindex: number;
2589
+ readonly "aria-valuemin": string;
2590
+ readonly "aria-valuemax": string;
2591
+ readonly "aria-valuenow": string;
2592
+ readonly "aria-valuetext": string;
2593
+ readonly "aria-label": string;
2594
+ readonly onKeyDown: (event: KeyboardEvent) => void;
2595
+ readonly onPointerDown: (event: PointerEvent) => void;
2596
+ readonly onPointerMove: (event: PointerEvent) => void;
2597
+ readonly onPointerUp: (event: PointerEvent) => void;
2598
+ readonly onPointerCancel: (event: PointerEvent) => void;
2599
+ readonly onWheel: (event: WheelEvent) => void;
2600
+ }
2601
+ /** Spread onto the live value; render it once, always (hide it visually, never with `hidden`). */
2602
+ export interface KnobValueProps {
2603
+ readonly id: string;
2604
+ readonly role: "status";
2605
+ readonly "aria-live": "polite";
2606
+ }
2607
+ export interface Knob {
2608
+ readonly value: Accessor<number>;
2609
+ /** A request, like a key or a drag; `setValue` never bypasses a controlled `value`. */
2610
+ readonly setValue: (value: number) => void;
2611
+ /**
2612
+ * The fraction of a full turn the skin rotates by for `value`, 0..1 (multiply
2613
+ * by 360 for degrees). Clamped to 0..1, or the wrapped fraction when `wrap`.
2614
+ */
2615
+ readonly percentOf: (value: number) => number;
2616
+ /** A request to move by `delta` value units (a key, a wheel, a host button). */
2617
+ readonly stepBy: (delta: number) => void;
2618
+ readonly knobProps: KnobProps;
2619
+ readonly valueProps: KnobValueProps;
2620
+ }
2621
+ /**
2622
+ * A virtual knob after GarageBand's (https://jherrm.github.io/knobs/): one
2623
+ * control turned three ways — spun around its centre, or slid vertically or
2624
+ * horizontally, one gesture locked per drag. Plain Solid signals, no elements,
2625
+ * no styling. **No ARIA role exists for a dial**, so the knob is a `role=slider`
2626
+ * with `aria-valuenow` / `aria-valuetext`; the value the host renders in
2627
+ * `valueProps` is the live announcement (`role=status`), and a numeric field
2628
+ * must sit next to the dial, because a dial is never the only control.
2629
+ *
2630
+ * Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultValue`);
2631
+ * pass `value` + `onValueChange` to own the value. A key, a wheel, a drag and
2632
+ * `setValue`/`stepBy` are requests the host may ignore.
2633
+ *
2634
+ * Gestures (`gesture`, default "both"), decided once on the first meaningful
2635
+ * movement so a drag never switches modes:
2636
+ * - a movement that stays on one axis (the other axis under `SLIDE_VARIANCE` of
2637
+ * it) locks a slide — the reference's "slide in a straight line";
2638
+ * - any other movement is a turn around the centre: the angle the pointer
2639
+ * covers is the value, and the farther from the centre the finer it is;
2640
+ * - `gesture: "spin"` / `"slide"` disable the other.
2641
+ *
2642
+ * A vertical slide turns the knob the way the grabbed side would turn a real
2643
+ * one: down on the right half is clockwise (up in value), down on the left half
2644
+ * counter-clockwise. A horizontal slide uses the same left/right side (the
2645
+ * reference picks top/bottom for this one; one rule for both is simpler to
2646
+ * learn). `wrap` makes the dial continuous instead of clamped.
2647
+ *
2648
+ * ponytail: no momentum/inertia and no `disabled` — the reference has a
2649
+ * deceleration tail, and no host here has asked for one; add inertia in the
2650
+ * host's rAF loop if a look needs it.
2651
+ */
2652
+ export declare const createKnob: (options?: KnobOptions) => Knob;
2299
2653
  export interface LinkOptions {
2300
2654
  readonly href?: string | undefined;
2301
2655
  /** Render a native `<a href>` (the default when `href` is set). */
@@ -2451,8 +2805,12 @@ export declare const createListbox: (options: ListboxOptions) => Listbox;
2451
2805
  * dismiss and Esc; `focusgroup` gives the menubar's roving tabindex. The
2452
2806
  * library owns the menu keys, typeahead, submenus and checkable state.
2453
2807
  *
2454
- * ponytail: uncontrolled; add a `value`/`onValueChange` host pair when a
2455
- * consumer owns open/checked state.
2808
+ * Request/commit (map ticket `rt1p`): the open state, and the checked items of
2809
+ * the checkable variants, are controllable. Uncontrolled by default
2810
+ * (`defaultOpen` / `defaultValue`); pass `open` / `value` (+ `onOpenChange` /
2811
+ * `onCheckedValueChange`) to own them. Every opening and closing intent — the
2812
+ * trigger's click and keys, Escape, an item's activation, Tab, and the
2813
+ * programmatic `openMenu` / `closeMenu` — is a request the host may ignore.
2456
2814
  */
2457
2815
  export type MenuItemKind = "item" | "checkbox" | "radio";
2458
2816
  export interface MenuItemInput {
@@ -2465,6 +2823,9 @@ export type MenuVariant = "plain" | "checkbox-items" | "radio-items" | "mixed";
2465
2823
  export interface MenuOptions {
2466
2824
  readonly items: ReadonlyArray<string | MenuItemInput>;
2467
2825
  readonly variant?: MenuVariant;
2826
+ /** Controlled: the host owns the open state and this is the only thing that opens or closes it. */
2827
+ readonly open?: boolean | undefined;
2828
+ /** Uncontrolled starting state; ignored while `open` is set. */
2468
2829
  readonly defaultOpen?: boolean;
2469
2830
  /**
2470
2831
  * `outside` (menu button): focus can sit outside the widget, so keys are
@@ -2480,7 +2841,7 @@ export interface MenuOptions {
2480
2841
  * Controlled: the host owns the checked items. Packed as
2481
2842
  * `checkboxItem0 * 5 + radioChoice` (1-based), matching `menu-checkable.qnt`'s
2482
2843
  * `checked(wrap, choice)`; pass `undefined` for uncontrolled. The open state
2483
- * stays uncontrolled.
2844
+ * has its own `open` pair.
2484
2845
  */
2485
2846
  readonly value?: number | undefined;
2486
2847
  /** The uncontrolled packed checked state at mount. */
@@ -2490,6 +2851,10 @@ export interface MenuOptions {
2490
2851
  /** Item indices disabled (focusable, not activatable). */
2491
2852
  readonly disabled?: ReadonlyArray<number>;
2492
2853
  readonly id?: string;
2854
+ /**
2855
+ * A request to change the open state, called when the intent differs from the
2856
+ * current one. Uncontrolled it is accepted; controlled the host may ignore it.
2857
+ */
2493
2858
  readonly onOpenChange?: (open: boolean) => void;
2494
2859
  readonly onItemClick?: (index: number) => void;
2495
2860
  readonly onCheckedChange?: (checked: boolean, index: number) => void;
@@ -3190,37 +3555,6 @@ export interface Spinbutton {
3190
3555
  * APG text.
3191
3556
  */
3192
3557
  export declare const createSpinbutton: (options?: SpinbuttonOptions) => Spinbutton;
3193
- export interface StatusOptions {
3194
- /** Rendered on `statusProps`; generated when absent. */
3195
- readonly id?: string | undefined;
3196
- }
3197
- /** Spread onto the `role=status` live region. Render it once, always. */
3198
- export interface StatusProps {
3199
- readonly id: string;
3200
- readonly role: "status";
3201
- readonly "aria-live": "polite";
3202
- readonly "aria-atomic": "true";
3203
- }
3204
- export interface Status {
3205
- /** The text currently shown, or `undefined` before the first `show`. */
3206
- readonly message: Accessor<string | undefined>;
3207
- /** Writes the message, replacing the previous one; nothing is queued. */
3208
- readonly show: (text: string) => void;
3209
- readonly clear: () => void;
3210
- readonly statusProps: StatusProps;
3211
- }
3212
- /**
3213
- * APG Alert's polite sibling: one `role=status` live region whose text the host
3214
- * replaces ("Link copied", "Saved"). It is not a queue: a second message shows
3215
- * in place of the first, so a burst reads as the last state, not as N alerts.
3216
- * The region is rendered once and stays mounted, which is what makes a change
3217
- * of its text announced.
3218
- *
3219
- * A status is never for errors: an error that needs attention now is
3220
- * `createAlert` (`role=alert`, assertive), and a message with a title, a
3221
- * description and a timeout is `createToast`.
3222
- */
3223
- export declare const createStatus: (options?: StatusOptions) => Status;
3224
3558
  export interface SwitchOptions {
3225
3559
  /** Controlled: the host owns the state and this is the only thing that changes it. */
3226
3560
  readonly value?: boolean | undefined;
@@ -3451,86 +3785,6 @@ export interface Table {
3451
3785
  * destructured snapshot. `id` is captured once: it is the base of the caption id.
3452
3786
  */
3453
3787
  export declare const createTable: (options: TableOptions) => Table;
3454
- /** The HTML `autocomplete` tokens worth naming; the attribute also takes an arbitrary token. */
3455
- export type AutoCompleteToken = "off" | "on" | "name" | "given-name" | "family-name" | "username" | "email" | "tel" | "url" | "organization" | "street-address" | "postal-code" | "current-password" | "new-password" | "one-time-code" | "nickname";
3456
- export interface TextInputOptions extends FieldOptions {
3457
- readonly type?: "text" | "email" | "tel" | "url" | "search" | "password" | undefined;
3458
- /** The HTML token list: the browser's own autofill is the accessible path. */
3459
- readonly autoComplete?: AutoCompleteToken | undefined;
3460
- readonly spellCheck?: boolean | undefined;
3461
- readonly autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined;
3462
- readonly inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
3463
- readonly maxLength?: number | undefined;
3464
- readonly readOnly?: boolean | undefined;
3465
- readonly disabled?: boolean | undefined;
3466
- readonly name?: string | undefined;
3467
- readonly dir?: "ltr" | "rtl" | "auto" | undefined;
3468
- readonly placeholder?: string | undefined;
3469
- /** Change and input both land here; the host decides what a keystroke means. */
3470
- readonly onValueChange?: ((value: string) => void) | undefined;
3471
- }
3472
- export interface TextAreaOptions extends TextInputOptions {
3473
- readonly rows?: number | undefined;
3474
- readonly wrap?: "soft" | "hard" | "off" | undefined;
3475
- readonly resize?: "none" | "vertical" | "horizontal" | "both" | undefined;
3476
- }
3477
- export interface TextControlProps {
3478
- readonly id: string;
3479
- readonly "aria-describedby": string | undefined;
3480
- readonly "aria-invalid": "true" | undefined;
3481
- readonly "aria-required": "true" | undefined;
3482
- readonly required: true | undefined;
3483
- readonly type: TextInputOptions["type"];
3484
- readonly autoComplete: AutoCompleteToken | undefined;
3485
- readonly spellcheck: "true" | "false" | undefined;
3486
- readonly autocapitalize: TextInputOptions["autoCapitalize"];
3487
- readonly inputmode: TextInputOptions["inputMode"];
3488
- readonly maxlength: number | undefined;
3489
- readonly readOnly: true | undefined;
3490
- readonly disabled: true | undefined;
3491
- readonly name: string | undefined;
3492
- readonly dir: "ltr" | "rtl" | "auto" | undefined;
3493
- readonly placeholder: string | undefined;
3494
- readonly onInput: (event: {
3495
- currentTarget: {
3496
- value: string;
3497
- };
3498
- }) => void;
3499
- }
3500
- export interface TextAreaControlProps extends TextControlProps {
3501
- readonly rows: number | undefined;
3502
- readonly wrap: TextAreaOptions["wrap"];
3503
- readonly style: {
3504
- readonly resize: "none" | "vertical" | "horizontal" | "both";
3505
- } | undefined;
3506
- }
3507
- /** The field and the control, one object: every prop the element needs. */
3508
- export interface TextInput extends Field {
3509
- readonly controlProps: TextControlProps;
3510
- }
3511
- export interface TextArea extends Field {
3512
- readonly controlProps: TextAreaControlProps;
3513
- }
3514
- /**
3515
- * A text input with the field contract attached: label, hint, error (announced),
3516
- * `aria-describedby` and `aria-invalid` come from `createField`; this adds the
3517
- * props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
3518
- * disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
3519
- * no primitive re-implements text editing, which is why there is no key
3520
- * handling here).
3521
- *
3522
- * Both `required` and `aria-required` are emitted: the native attribute is what
3523
- * the browser enforces, the ARIA one is what a custom widget reading the same
3524
- * props needs. A host that validates itself passes `noValidate` on the form
3525
- * rather than dropping the attribute.
3526
- */
3527
- export declare const createTextInput: (options?: TextInputOptions) => TextInput;
3528
- /**
3529
- * The same contract for a textarea, plus rows, wrap and resize. `resize` is a
3530
- * style because CSS has no attribute for it; `none` is the value a layout that
3531
- * owns its own height wants.
3532
- */
3533
- export declare const createTextArea: (options?: TextAreaOptions) => TextArea;
3534
3788
  export type TabsOrientation = "horizontal" | "vertical";
3535
3789
  export type TabsActivation = "automatic" | "manual";
3536
3790
  /**