@cssints/headless 0.18.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. */
@@ -1497,6 +1838,106 @@ export interface Drawer {
1497
1838
  * dismissal a non-modal drawer has); closing returns focus to the trigger.
1498
1839
  */
1499
1840
  export declare const createDrawer: (options?: DrawerOptions) => Drawer;
1841
+ /** Why a file was refused: the `accept` grammar, or `maxSize`. */
1842
+ export type DropZoneRejectReason = "type" | "size";
1843
+ export interface DropZoneReject {
1844
+ readonly file: File;
1845
+ readonly reason: DropZoneRejectReason;
1846
+ }
1847
+ export interface DropZoneLabels {
1848
+ /** The zone's accessible name (`aria-label` on the `role=group` zone). */
1849
+ readonly zone?: string | undefined;
1850
+ /** The browse button's accessible name; render it visibly, or use it for an icon-only button. */
1851
+ readonly browse?: string | undefined;
1852
+ }
1853
+ export interface DropZoneOptions {
1854
+ /** The HTML `accept` grammar: extensions (`.png`) and MIME types, wildcards included (`image/*`). */
1855
+ readonly accept?: string | undefined;
1856
+ readonly multiple?: boolean | undefined;
1857
+ /** Bytes; a larger file is rejected with `"size"`. */
1858
+ readonly maxSize?: number | undefined;
1859
+ readonly disabled?: boolean | undefined;
1860
+ /** The files that passed, one call per batch; not called when the batch has none. */
1861
+ readonly onFiles: (files: File[]) => void;
1862
+ /** Each refused file, once, with the reason the host shows. */
1863
+ readonly onReject?: ((reject: DropZoneReject) => void) | undefined;
1864
+ readonly labels?: DropZoneLabels | undefined;
1865
+ }
1866
+ /** Spread onto the hidden `<input type=file>`; render it, always. */
1867
+ export interface DropZoneInputProps {
1868
+ /** The id the browse button clicks; generated. */
1869
+ readonly id: string;
1870
+ readonly type: "file";
1871
+ readonly accept: string | undefined;
1872
+ readonly multiple: true | undefined;
1873
+ readonly disabled: true | undefined;
1874
+ readonly hidden: true;
1875
+ readonly onChange: (event: {
1876
+ currentTarget: HTMLInputElement;
1877
+ }) => void;
1878
+ }
1879
+ /** Spread onto the drop target (any element). `dragging()` is the styling hook. */
1880
+ export interface DropZoneZoneProps {
1881
+ readonly role: "group";
1882
+ readonly "aria-label": string | undefined;
1883
+ readonly "aria-disabled": "true" | undefined;
1884
+ readonly onDragEnter: (event: DragEvent) => void;
1885
+ readonly onDragOver: (event: DragEvent) => void;
1886
+ readonly onDragLeave: (event: DragEvent) => void;
1887
+ readonly onDrop: (event: DragEvent) => void;
1888
+ }
1889
+ /** Spread onto a real `<button type="button">`: Enter and Space are the platform's. */
1890
+ export interface DropZoneBrowseProps {
1891
+ readonly type: "button";
1892
+ readonly disabled: true | undefined;
1893
+ readonly "aria-disabled": "true" | undefined;
1894
+ readonly "aria-label": string | undefined;
1895
+ readonly onClick: () => void;
1896
+ }
1897
+ /** Spread onto the live region; render it once, always. */
1898
+ export interface DropZoneStatusProps {
1899
+ readonly role: "status";
1900
+ readonly "aria-live": "polite";
1901
+ readonly "aria-atomic": "true";
1902
+ }
1903
+ export interface DropZone {
1904
+ /** A drag is over the zone; `dragleave` on a child does not clear it. */
1905
+ readonly dragging: Accessor<boolean>;
1906
+ /**
1907
+ * A default sentence for the last batch ("2 files, 1 rejected: too large"),
1908
+ * or `undefined` before one arrived. The host may write its own words from
1909
+ * the same counts.
1910
+ */
1911
+ readonly message: Accessor<string | undefined>;
1912
+ readonly inputProps: DropZoneInputProps;
1913
+ readonly zoneProps: DropZoneZoneProps;
1914
+ readonly browseProps: DropZoneBrowseProps;
1915
+ readonly statusProps: DropZoneStatusProps;
1916
+ }
1917
+ /**
1918
+ * A file drop zone. There is no APG pattern and no Quint spec for one: the
1919
+ * contract is HTML's file input plus WAI-ARIA's live region, and
1920
+ * `test/dropzone.browser.test.tsx` is its oracle.
1921
+ *
1922
+ * Decisions:
1923
+ * - **The zone is never drag-only.** `browseProps` spreads onto a real
1924
+ * `<button type="button">` that clicks the hidden input, so the picker is the
1925
+ * keyboard path (Enter and Space are the platform's) and a pointer user can
1926
+ * click as well as drag. The drop handlers add nothing the button cannot do.
1927
+ * - **Validation happens once per file**, in the order type then size, and a
1928
+ * refused file reaches `onReject` with the reason the host shows. `accept` is
1929
+ * the HTML grammar (`.` extension, MIME type, `*`); `maxSize` is bytes.
1930
+ * - **Dropping a folder or a `DataTransfer` with no files is a no-op**: no
1931
+ * `onFiles`, no `onReject`, no message, and the previous announcement stays.
1932
+ * - **`dragleave` counts.** A child's `dragleave` bubbles to the zone, so the
1933
+ * state is a depth counter: it clears only when the drag leaves the zone
1934
+ * itself, or on a drop.
1935
+ * - **The zone is `role=group`** so `labels.zone` is exposed; `dragging()` is
1936
+ * the only drag state a skin reads (no `data-dragging`).
1937
+ * - `disabled` marks the zone `aria-disabled`, disables the input natively and
1938
+ * the browse button, and drops every drag before it becomes a batch.
1939
+ */
1940
+ export declare const createDropZone: (options: DropZoneOptions) => DropZone;
1500
1941
  /**
1501
1942
  * Options are read lazily (getters); pass a component's `props`.
1502
1943
  */
@@ -1558,120 +1999,32 @@ export interface Feed {
1558
1999
  readonly ctrlEnd: () => void;
1559
2000
  readonly ctrlHome: () => void;
1560
2001
  /** Scroll/position asked for more: `aria-busy` goes true while in flight. */
1561
- readonly loadStart: () => void;
1562
- /** A batch arrived; a request to grow the loaded count. */
1563
- readonly loadFinish: (count: number) => void;
1564
- readonly loadFail: () => void;
1565
- readonly setSizeMode: (mode: "loaded" | "total") => void;
1566
- readonly setTotalKnown: (known: boolean) => void;
1567
- readonly feedProps: FeedProps;
1568
- readonly articleProps: (posinset: number) => FeedArticleProps;
1569
- readonly beforeProps: FeedSentinelProps;
1570
- readonly afterProps: FeedSentinelProps;
1571
- }
1572
- /**
1573
- * APG Feed (https://www.w3.org/WAI/ARIA/apg/patterns/feed/): state plus prop
1574
- * objects for the feed container, its articles and the two outside focus
1575
- * sentinels. Focus reaching the last loaded article triggers a load; `aria-busy`
1576
- * brackets it; Page Up/Down move by article; Control+End/Home leave the feed.
1577
- * No styling, no elements. Checked against `specs/apg/feed.qnt`.
1578
- *
1579
- * Platform: **library** — focusgroup V2 `feed` is explainer-only, so the load
1580
- * trigger, busy bracketing and Page Up/Down / Control+End/Home keys are ours.
1581
- *
1582
- * Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultValue`);
1583
- * pass `value` + `onValueChange` to own the loaded count. `loadFinish` is the
1584
- * request; `loadStart` / `loadFail` are adapter lifecycle events.
1585
- */
1586
- export declare const createFeed: (options?: FeedOptions) => Feed;
1587
- export interface FieldOptions {
1588
- /** Base of every id the field makes (`<id>-control`, `-label`, `-hint`, `-error`). */
1589
- readonly id?: string | undefined;
1590
- readonly required?: boolean | undefined;
1591
- /**
1592
- * `true` marks the control invalid; a string does too and is the error message
1593
- * (unless `error` is set). `false` forces valid: an `error` is then not shown,
1594
- * so a host can pass `touched() && failed()` and keep the message ready.
1595
- * Left out, the field is invalid exactly when it has an error message.
1596
- */
1597
- readonly invalid?: boolean | string | undefined;
1598
- /** Help text; absent (or empty) means the hint element is hidden and not referenced. */
1599
- readonly hint?: string | undefined;
1600
- /** The error message; absent (or empty) means no error element. */
1601
- readonly error?: string | undefined;
1602
- /**
1603
- * Ids of elements the host owns, appended to `aria-describedby` after the
1604
- * field's own hint and error: a form-level error that belongs to several
1605
- * controls ("the passwords do not match" names both). One id or a list;
1606
- * empty strings are dropped, and the attribute is absent when nothing is left.
1607
- * The host keeps that element rendered, and decides whether it is hidden.
1608
- */
1609
- readonly describedby?: string | readonly string[] | undefined;
1610
- }
1611
- /** Spread onto the form control (`input`, `textarea`, `select`, or a custom widget). */
1612
- export interface FieldControlProps {
1613
- readonly id: string;
1614
- /** The ids of the hint and the error that are shown, hint first, then the host's `describedby`; absent when there are none. */
1615
- readonly "aria-describedby": string | undefined;
1616
- readonly "aria-invalid": "true" | undefined;
1617
- /** For a control that is not native (a native one has `required`). */
1618
- readonly "aria-required": "true" | undefined;
1619
- readonly required: true | undefined;
1620
- }
1621
- export interface FieldLabelProps {
1622
- readonly id: string;
1623
- readonly for: string;
1624
- }
1625
- /** Spread onto the hint and the error element; render both, always. */
1626
- export interface FieldMessageProps {
1627
- readonly id: string;
1628
- readonly hidden: boolean;
1629
- }
1630
- /** The error element also announces itself: `role="alert"` while it is shown. */
1631
- export interface FieldErrorProps extends FieldMessageProps {
1632
- readonly role: "alert" | undefined;
1633
- }
1634
- export interface Field {
1635
- readonly invalid: () => boolean;
1636
- /** The hint, or `undefined` when there is none. */
1637
- readonly hint: () => string | undefined;
1638
- /** The error shown: `undefined` unless the field is invalid and has a message. */
1639
- readonly error: () => string | undefined;
1640
- /**
1641
- * The ids `aria-describedby` lists now: hint, then error (never an id that is
1642
- * hidden), then the host's `describedby`.
1643
- */
1644
- readonly describedIds: () => readonly string[];
1645
- readonly controlProps: FieldControlProps;
1646
- readonly labelProps: FieldLabelProps;
1647
- readonly hintProps: FieldMessageProps;
1648
- readonly errorProps: FieldErrorProps;
1649
- }
1650
- /**
1651
- * The ARIA contract of a form field: a label, a control, a hint and an error.
1652
- * There is no APG pattern for it (APG has no "form field"; the nearest is the
1653
- * label and description rules of the accessible name computation), so the
1654
- * contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
1655
- * `aria-required`) and HTML (`label[for]`, `required`), and from the
1656
- * majority of Base UI Field, react-aria TextField and ariakit FormControl,
1657
- * which list the description and the error message together. It is not checked
1658
- * against a Quint spec; `test/field.browser.test.tsx` is its oracle.
1659
- *
1660
- * Decisions: the error does not replace the hint, both are listed, hint first
1661
- * (the hint is what tells the user how to fix the error); an error implies
1662
- * invalid and `invalid: false` overrides it; a hidden message is never
1663
- * referenced, so `aria-describedby` has no dangling id. The error element is
1664
- * `role="alert"` only while it is shown (a hidden element announces nothing, and
1665
- * one that appears is announced); the hint is never an alert.
2002
+ readonly loadStart: () => void;
2003
+ /** A batch arrived; a request to grow the loaded count. */
2004
+ readonly loadFinish: (count: number) => void;
2005
+ readonly loadFail: () => void;
2006
+ readonly setSizeMode: (mode: "loaded" | "total") => void;
2007
+ readonly setTotalKnown: (known: boolean) => void;
2008
+ readonly feedProps: FeedProps;
2009
+ readonly articleProps: (posinset: number) => FeedArticleProps;
2010
+ readonly beforeProps: FeedSentinelProps;
2011
+ readonly afterProps: FeedSentinelProps;
2012
+ }
2013
+ /**
2014
+ * APG Feed (https://www.w3.org/WAI/ARIA/apg/patterns/feed/): state plus prop
2015
+ * objects for the feed container, its articles and the two outside focus
2016
+ * sentinels. Focus reaching the last loaded article triggers a load; `aria-busy`
2017
+ * brackets it; Page Up/Down move by article; Control+End/Home leave the feed.
2018
+ * No styling, no elements. Checked against `specs/apg/feed.qnt`.
1666
2019
  *
1667
- * `describedby` lists host-owned ids after the field's own: one refusal that
1668
- * belongs to two controls is one form-level element (with its own `role`), named
1669
- * by both. The field cannot know whether that element is shown: the host
1670
- * passes the id only while it is.
2020
+ * Platform: **library** — focusgroup V2 `feed` is explainer-only, so the load
2021
+ * trigger, busy bracketing and Page Up/Down / Control+End/Home keys are ours.
1671
2022
  *
1672
- * Options are read when an attribute is read: pass getters, never destructure.
2023
+ * Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultValue`);
2024
+ * pass `value` + `onValueChange` to own the loaded count. `loadFinish` is the
2025
+ * request; `loadStart` / `loadFail` are adapter lifecycle events.
1673
2026
  */
1674
- export declare const createField: (options?: FieldOptions) => Field;
2027
+ export declare const createFeed: (options?: FeedOptions) => Feed;
1675
2028
  export interface PaginationLabels {
1676
2029
  /** The navigation landmark's name; defaults to "Pagination". */
1677
2030
  readonly nav?: string | undefined;
@@ -2196,6 +2549,107 @@ export interface Hotkeys {
2196
2549
  * browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
2197
2550
  */
2198
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;
2199
2653
  export interface LinkOptions {
2200
2654
  readonly href?: string | undefined;
2201
2655
  /** Render a native `<a href>` (the default when `href` is set). */
@@ -2351,8 +2805,12 @@ export declare const createListbox: (options: ListboxOptions) => Listbox;
2351
2805
  * dismiss and Esc; `focusgroup` gives the menubar's roving tabindex. The
2352
2806
  * library owns the menu keys, typeahead, submenus and checkable state.
2353
2807
  *
2354
- * ponytail: uncontrolled; add a `value`/`onValueChange` host pair when a
2355
- * 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.
2356
2814
  */
2357
2815
  export type MenuItemKind = "item" | "checkbox" | "radio";
2358
2816
  export interface MenuItemInput {
@@ -2365,6 +2823,9 @@ export type MenuVariant = "plain" | "checkbox-items" | "radio-items" | "mixed";
2365
2823
  export interface MenuOptions {
2366
2824
  readonly items: ReadonlyArray<string | MenuItemInput>;
2367
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. */
2368
2829
  readonly defaultOpen?: boolean;
2369
2830
  /**
2370
2831
  * `outside` (menu button): focus can sit outside the widget, so keys are
@@ -2380,7 +2841,7 @@ export interface MenuOptions {
2380
2841
  * Controlled: the host owns the checked items. Packed as
2381
2842
  * `checkboxItem0 * 5 + radioChoice` (1-based), matching `menu-checkable.qnt`'s
2382
2843
  * `checked(wrap, choice)`; pass `undefined` for uncontrolled. The open state
2383
- * stays uncontrolled.
2844
+ * has its own `open` pair.
2384
2845
  */
2385
2846
  readonly value?: number | undefined;
2386
2847
  /** The uncontrolled packed checked state at mount. */
@@ -2390,6 +2851,10 @@ export interface MenuOptions {
2390
2851
  /** Item indices disabled (focusable, not activatable). */
2391
2852
  readonly disabled?: ReadonlyArray<number>;
2392
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
+ */
2393
2858
  readonly onOpenChange?: (open: boolean) => void;
2394
2859
  readonly onItemClick?: (index: number) => void;
2395
2860
  readonly onCheckedChange?: (checked: boolean, index: number) => void;
@@ -2854,6 +3319,11 @@ export interface SliderOptions {
2854
3319
  readonly defaultValue?: number;
2855
3320
  readonly orientation?: SliderOrientation;
2856
3321
  readonly direction?: SliderDirection;
3322
+ /**
3323
+ * aria-disabled: the thumb stays the tab stop (the APG pattern's focus
3324
+ * contract) but a key or a rail press changes nothing.
3325
+ */
3326
+ readonly disabled?: boolean;
2857
3327
  /** Base for the generated thumb id. */
2858
3328
  readonly id?: string;
2859
3329
  /**
@@ -2870,6 +3340,7 @@ export interface SliderThumbProps {
2870
3340
  readonly "aria-valuemax": string;
2871
3341
  readonly "aria-valuetext": string;
2872
3342
  readonly "aria-orientation": SliderOrientation;
3343
+ readonly "aria-disabled": "true" | undefined;
2873
3344
  readonly tabindex: number;
2874
3345
  readonly onFocus: () => void;
2875
3346
  readonly onBlur: () => void;
@@ -2907,6 +3378,11 @@ export interface Slider {
2907
3378
  * pass `value` + `onValueChange` to own the value. A key or rail press is a
2908
3379
  * request the host may ignore.
2909
3380
  *
3381
+ * `disabled` is a widget-level extension of the pattern (the spec lists it
3382
+ * under its deliberate omissions): the thumb keeps its tab stop and announces
3383
+ * `aria-disabled`, and every key and rail press is dropped before it becomes a
3384
+ * request.
3385
+ *
2910
3386
  * ponytail: dragging (pointer capture/move) is not modelled — the spec
2911
3387
  * deliberately only fixes a discrete rail press.
2912
3388
  */
@@ -2991,6 +3467,11 @@ export interface SpinbuttonOptions {
2991
3467
  readonly defaultValue?: number;
2992
3468
  /** Wrap around at the bounds instead of clamping. */
2993
3469
  readonly wrap?: boolean;
3470
+ /**
3471
+ * aria-disabled plus the native `disabled` on the text field: keys, typing
3472
+ * and both steppers change nothing, and both steppers announce `aria-disabled`.
3473
+ */
3474
+ readonly disabled?: boolean;
2994
3475
  /** Base for the generated input / button ids. */
2995
3476
  readonly id?: string;
2996
3477
  /**
@@ -3007,6 +3488,8 @@ export interface SpinbuttonInputProps {
3007
3488
  readonly "aria-valuemax": string;
3008
3489
  readonly "aria-valuetext": string;
3009
3490
  readonly "aria-invalid": "true" | undefined;
3491
+ readonly "aria-disabled": "true" | undefined;
3492
+ readonly disabled: boolean;
3010
3493
  readonly tabindex: number;
3011
3494
  readonly value: string;
3012
3495
  readonly onFocus: () => void;
@@ -3062,41 +3545,16 @@ export interface Spinbutton {
3062
3545
  * stepper buttons, Enter and blur are requests the host may ignore; typing and
3063
3546
  * Escape change no committed value.
3064
3547
  *
3548
+ * `disabled` is a widget-level extension of the pattern (the spec models the
3549
+ * steppers' bound only, and lists this under its deliberate omissions): the
3550
+ * field announces `aria-disabled` and carries the native `disabled`, every key,
3551
+ * typed draft and both steppers are dropped before they become a request, and
3552
+ * neither stepper offers itself.
3553
+ *
3065
3554
  * ponytail: commit/revert on Enter/blur/Escape is the common convention, not
3066
3555
  * APG text.
3067
3556
  */
3068
3557
  export declare const createSpinbutton: (options?: SpinbuttonOptions) => Spinbutton;
3069
- export interface StatusOptions {
3070
- /** Rendered on `statusProps`; generated when absent. */
3071
- readonly id?: string | undefined;
3072
- }
3073
- /** Spread onto the `role=status` live region. Render it once, always. */
3074
- export interface StatusProps {
3075
- readonly id: string;
3076
- readonly role: "status";
3077
- readonly "aria-live": "polite";
3078
- readonly "aria-atomic": "true";
3079
- }
3080
- export interface Status {
3081
- /** The text currently shown, or `undefined` before the first `show`. */
3082
- readonly message: Accessor<string | undefined>;
3083
- /** Writes the message, replacing the previous one; nothing is queued. */
3084
- readonly show: (text: string) => void;
3085
- readonly clear: () => void;
3086
- readonly statusProps: StatusProps;
3087
- }
3088
- /**
3089
- * APG Alert's polite sibling: one `role=status` live region whose text the host
3090
- * replaces ("Link copied", "Saved"). It is not a queue: a second message shows
3091
- * in place of the first, so a burst reads as the last state, not as N alerts.
3092
- * The region is rendered once and stays mounted, which is what makes a change
3093
- * of its text announced.
3094
- *
3095
- * A status is never for errors: an error that needs attention now is
3096
- * `createAlert` (`role=alert`, assertive), and a message with a title, a
3097
- * description and a timeout is `createToast`.
3098
- */
3099
- export declare const createStatus: (options?: StatusOptions) => Status;
3100
3558
  export interface SwitchOptions {
3101
3559
  /** Controlled: the host owns the state and this is the only thing that changes it. */
3102
3560
  readonly value?: boolean | undefined;
@@ -3327,86 +3785,6 @@ export interface Table {
3327
3785
  * destructured snapshot. `id` is captured once: it is the base of the caption id.
3328
3786
  */
3329
3787
  export declare const createTable: (options: TableOptions) => Table;
3330
- /** The HTML `autocomplete` tokens worth naming; the attribute also takes an arbitrary token. */
3331
- 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";
3332
- export interface TextInputOptions extends FieldOptions {
3333
- readonly type?: "text" | "email" | "tel" | "url" | "search" | "password" | undefined;
3334
- /** The HTML token list: the browser's own autofill is the accessible path. */
3335
- readonly autoComplete?: AutoCompleteToken | undefined;
3336
- readonly spellCheck?: boolean | undefined;
3337
- readonly autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined;
3338
- readonly inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
3339
- readonly maxLength?: number | undefined;
3340
- readonly readOnly?: boolean | undefined;
3341
- readonly disabled?: boolean | undefined;
3342
- readonly name?: string | undefined;
3343
- readonly dir?: "ltr" | "rtl" | "auto" | undefined;
3344
- readonly placeholder?: string | undefined;
3345
- /** Change and input both land here; the host decides what a keystroke means. */
3346
- readonly onValueChange?: ((value: string) => void) | undefined;
3347
- }
3348
- export interface TextAreaOptions extends TextInputOptions {
3349
- readonly rows?: number | undefined;
3350
- readonly wrap?: "soft" | "hard" | "off" | undefined;
3351
- readonly resize?: "none" | "vertical" | "horizontal" | "both" | undefined;
3352
- }
3353
- export interface TextControlProps {
3354
- readonly id: string;
3355
- readonly "aria-describedby": string | undefined;
3356
- readonly "aria-invalid": "true" | undefined;
3357
- readonly "aria-required": "true" | undefined;
3358
- readonly required: true | undefined;
3359
- readonly type: TextInputOptions["type"];
3360
- readonly autoComplete: AutoCompleteToken | undefined;
3361
- readonly spellcheck: "true" | "false" | undefined;
3362
- readonly autocapitalize: TextInputOptions["autoCapitalize"];
3363
- readonly inputmode: TextInputOptions["inputMode"];
3364
- readonly maxlength: number | undefined;
3365
- readonly readOnly: true | undefined;
3366
- readonly disabled: true | undefined;
3367
- readonly name: string | undefined;
3368
- readonly dir: "ltr" | "rtl" | "auto" | undefined;
3369
- readonly placeholder: string | undefined;
3370
- readonly onInput: (event: {
3371
- currentTarget: {
3372
- value: string;
3373
- };
3374
- }) => void;
3375
- }
3376
- export interface TextAreaControlProps extends TextControlProps {
3377
- readonly rows: number | undefined;
3378
- readonly wrap: TextAreaOptions["wrap"];
3379
- readonly style: {
3380
- readonly resize: "none" | "vertical" | "horizontal" | "both";
3381
- } | undefined;
3382
- }
3383
- /** The field and the control, one object: every prop the element needs. */
3384
- export interface TextInput extends Field {
3385
- readonly controlProps: TextControlProps;
3386
- }
3387
- export interface TextArea extends Field {
3388
- readonly controlProps: TextAreaControlProps;
3389
- }
3390
- /**
3391
- * A text input with the field contract attached: label, hint, error (announced),
3392
- * `aria-describedby` and `aria-invalid` come from `createField`; this adds the
3393
- * props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
3394
- * disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
3395
- * no primitive re-implements text editing, which is why there is no key
3396
- * handling here).
3397
- *
3398
- * Both `required` and `aria-required` are emitted: the native attribute is what
3399
- * the browser enforces, the ARIA one is what a custom widget reading the same
3400
- * props needs. A host that validates itself passes `noValidate` on the form
3401
- * rather than dropping the attribute.
3402
- */
3403
- export declare const createTextInput: (options?: TextInputOptions) => TextInput;
3404
- /**
3405
- * The same contract for a textarea, plus rows, wrap and resize. `resize` is a
3406
- * style because CSS has no attribute for it; `none` is the value a layout that
3407
- * owns its own height wants.
3408
- */
3409
- export declare const createTextArea: (options?: TextAreaOptions) => TextArea;
3410
3788
  export type TabsOrientation = "horizontal" | "vertical";
3411
3789
  export type TabsActivation = "automatic" | "manual";
3412
3790
  /**
@@ -3433,6 +3811,26 @@ export interface TabsOptions {
3433
3811
  * current value. Uncontrolled it is accepted; controlled the host may ignore it.
3434
3812
  */
3435
3813
  readonly onValueChange?: (index: number) => void;
3814
+ /**
3815
+ * Close buttons and the Delete key (default false): the host renders
3816
+ * `closeProps` and removes the tab from `onClose`. The last tab is never
3817
+ * asked to close.
3818
+ */
3819
+ readonly closable?: boolean | undefined;
3820
+ /**
3821
+ * Inline rename (default false): a double click on a tab starts it, Enter /
3822
+ * blur commit and Escape reverts; the host renames from `onRename`.
3823
+ */
3824
+ readonly renamable?: boolean | undefined;
3825
+ /**
3826
+ * A tab's label: the close / rename button names and the starting draft of a
3827
+ * rename, read lazily (a getter is fine) so a rename follows.
3828
+ */
3829
+ readonly label?: ((index: number) => string | undefined) | undefined;
3830
+ /** A request to remove a tab; the primitive does not own the list. */
3831
+ readonly onClose?: ((index: number) => void) | undefined;
3832
+ /** A request to rename a tab with a non-empty label. */
3833
+ readonly onRename?: ((index: number, label: string) => void) | undefined;
3436
3834
  }
3437
3835
  /** Spread onto the element with `role=tab` (a native `<button>`). */
3438
3836
  export interface TabProps {
@@ -3460,21 +3858,63 @@ export interface TabpanelProps {
3460
3858
  readonly tabindex: 0 | undefined;
3461
3859
  readonly hidden: boolean;
3462
3860
  }
3861
+ /**
3862
+ * Spread onto the tab's close button. `tabindex=-1` keeps it out of the
3863
+ * tablist's tab order (a tablist's stops are its tabs); the Delete key is the
3864
+ * keyboard path.
3865
+ */
3866
+ export interface TabCloseProps {
3867
+ readonly "aria-label": string;
3868
+ readonly tabindex: -1;
3869
+ readonly onClick: () => void;
3870
+ }
3871
+ /** Spread onto the inline `<input>` a renaming tab renders instead of its label. */
3872
+ export interface TabRenameInputProps {
3873
+ readonly type: "text";
3874
+ readonly value: string;
3875
+ readonly "aria-label": string;
3876
+ readonly onInput: (event: InputEvent) => void;
3877
+ readonly onKeyDown: (event: KeyboardEvent) => void;
3878
+ readonly onBlur: () => void;
3879
+ }
3880
+ /** The rename part of one tab: put `onDblClick` on the tab, `inputProps` in it. */
3881
+ export interface TabRenameProps {
3882
+ readonly onDblClick: () => void;
3883
+ /** Whether this tab is the one being renamed. */
3884
+ readonly editing: Accessor<boolean>;
3885
+ /** Start renaming `index` (e.g. a tab the host has just added). */
3886
+ readonly begin: (index: number) => void;
3887
+ /** Commit `label` to the tab being edited; an empty label cancels. */
3888
+ readonly commit: (label: string) => void;
3889
+ readonly cancel: () => void;
3890
+ readonly inputProps: TabRenameInputProps;
3891
+ }
3463
3892
  export interface Tabs {
3893
+ /** The tab count; read from `items` on every read, so a host that shrinks its list stays in step. */
3464
3894
  readonly items: number;
3465
3895
  readonly selected: Accessor<number>;
3466
3896
  /** Tab holding (or last holding) DOM focus; -1 when focus is outside. */
3467
3897
  readonly focused: Accessor<number>;
3898
+ /** The tab being renamed inline, `null` when none. */
3899
+ readonly editing: Accessor<number | null>;
3468
3900
  /** Programmatic selection request; ignores disabled tabs. */
3469
3901
  readonly select: (index: number) => void;
3470
3902
  /** Programmatic selection request (may point at a disabled tab, like base-ui). */
3471
3903
  readonly setSelected: (index: number) => void;
3472
3904
  /** Programmatic focus (does not select). */
3473
3905
  readonly focusTab: (index: number) => void;
3906
+ /**
3907
+ * The tab that takes focus once `onClose(index)`'s removal lands: the tab
3908
+ * that slid into `index`, or the previous one when the last tab closed. Call
3909
+ * it before the host removes the tab.
3910
+ */
3911
+ readonly focusAfterClose: (index: number) => number;
3474
3912
  readonly keyDown: (key: string) => void;
3475
3913
  readonly tablistProps: TablistProps;
3476
3914
  readonly tabProps: (index: number) => TabProps;
3477
3915
  readonly tabpanelProps: (index: number) => TabpanelProps;
3916
+ readonly closeProps: (index: number) => TabCloseProps;
3917
+ readonly renameProps: (index: number) => TabRenameProps;
3478
3918
  }
3479
3919
  /**
3480
3920
  * APG Tabs (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): state plus prop
@@ -3489,6 +3929,14 @@ export interface Tabs {
3489
3929
  * Request/commit (map ticket `rt1p`): uncontrolled by default
3490
3930
  * (`defaultSelected`); pass `value` + `onValueChange` to own the selection.
3491
3931
  * Programmatic methods (`select`, `setSelected`, `keyDown`) are requests too.
3932
+ *
3933
+ * Close and rename are an **extension** (bean `cssints-gdv2`): the APG spec
3934
+ * models a `deletable` flavour but neither a callback nor a rename, and this
3935
+ * primitive owns no list. `closable` / `renamable` turn on the props; `onClose`
3936
+ * / `onRename` ask the host, which removes or renames. Focus after a close goes
3937
+ * to the tab that slid into the closed one's place, or to the previous tab when
3938
+ * the last one closed; closing the last remaining tab is refused, as the spec
3939
+ * refuses an empty tablist.
3492
3940
  */
3493
3941
  export declare const createTabs: (options?: TabsOptions) => Tabs;
3494
3942
  /**
@@ -3506,10 +3954,16 @@ export declare const createTabs: (options?: TabsOptions) => Tabs;
3506
3954
  * may ignore. The zoom window (`viewStart`/`viewEnd`) is not the controlled
3507
3955
  * value and stays uncontrolled.
3508
3956
  *
3509
- * ponytail: the fixed 5-span tree is a demo fixture; a real consumer owns the
3510
- * span list (layout is theirs). Roving tabindex only — a consumer must call
3511
- * `focus()` on the row whose `tabindex` is 0 after arrow navigation (the tests'
3512
- * fixture does this with an effect).
3957
+ * The fixed 5-span tree is the demo fixture (the differential walk's oracle).
3958
+ * A real consumer passes `rows`: span data on one shared axis, laid out by
3959
+ * `barProps` as percentages of the rows' extent. No roles are borrowed from
3960
+ * APG: a trace waterfall is a list of spans in time, so the rows mode is
3961
+ * `role="list"` / `role="listitem"` (with `aria-level` for the depth) and the
3962
+ * current row is `aria-current="true"`, which every element may carry.
3963
+ *
3964
+ * Roving tabindex only — a consumer must call `focus()` on the row whose
3965
+ * `tabindex` is 0 after arrow navigation (the tests' fixture does this with an
3966
+ * effect; the rows mode's span rows are reached by tabbing to the current one).
3513
3967
  */
3514
3968
  /** One span row: a fraction of the parent span on a shared 0..1 axis. */
3515
3969
  export interface TimelineRow {
@@ -3592,12 +4046,90 @@ export interface Timeline {
3592
4046
  readonly rowProps: (id: number) => TimelineRowProps;
3593
4047
  readonly rulerProps: TimelineRulerProps;
3594
4048
  }
3595
- /**
3596
- * Headless span timeline on Solid 2. The exported accessors are signals; the
3597
- * prop objects are getter-backed, so spreading them keeps every attribute
3598
- * reactive.
3599
- */
3600
- export declare const createTimeline: (options?: TimelineOptions) => Timeline;
4049
+ /** One span of a real trace: a point on a shared axis, labelled, at a depth. */
4050
+ export interface TimelineSpan {
4051
+ readonly id: string;
4052
+ readonly label: string;
4053
+ /** Offset on the axis, in the axis' unit; `end` is the same axis, not a duration. */
4054
+ readonly start: number;
4055
+ readonly end: number;
4056
+ /** Nesting level, 0 for a root span. */
4057
+ readonly depth?: number;
4058
+ readonly status?: "ok" | "error" | "unset";
4059
+ }
4060
+ export interface TimelineAxis {
4061
+ /** The unit the numbers are in; the ruler labels its ticks with it. */
4062
+ readonly unit?: "ms" | "us" | "s";
4063
+ }
4064
+ /** The rows mode: real span data, read lazily like every other option. */
4065
+ export interface TimelineRowsOptions {
4066
+ readonly rows: ReadonlyArray<TimelineSpan>;
4067
+ readonly axis?: TimelineAxis | undefined;
4068
+ /** Controlled: the host owns the selection. */
4069
+ readonly selected?: string | undefined;
4070
+ /** A request to change the selection the host may ignore. */
4071
+ readonly onSelect?: ((id: string) => void) | undefined;
4072
+ }
4073
+ export interface TimelineTick {
4074
+ readonly value: number;
4075
+ readonly left: number;
4076
+ readonly label: string;
4077
+ }
4078
+ export interface TimelineListProps {
4079
+ readonly role: "list";
4080
+ readonly "aria-label": string;
4081
+ }
4082
+ export interface TimelineSpanRowProps {
4083
+ readonly role: "listitem";
4084
+ /** `depth + 1`: the nesting a flat list cannot show in the DOM. */
4085
+ readonly "aria-level": number;
4086
+ readonly "aria-current": "true" | undefined;
4087
+ /** A styling hook: the status the span ended with. */
4088
+ readonly "data-status": "ok" | "error" | "unset";
4089
+ readonly tabindex: number;
4090
+ readonly onClick: () => void;
4091
+ readonly onKeyDown: (event: KeyboardEvent) => void;
4092
+ }
4093
+ export interface TimelineBarProps {
4094
+ readonly "aria-hidden": "true";
4095
+ /** `left` / `width` are percentages of the extent; `--depth` is the row's depth. */
4096
+ readonly style: {
4097
+ readonly left: string;
4098
+ readonly width: string;
4099
+ readonly "--depth": string;
4100
+ };
4101
+ }
4102
+ export interface TimelineLabelProps {
4103
+ /** `--depth`, so the host indents the label (`padding-inline-start: calc(var(--depth) * 1rem)`) instead of nesting it. */
4104
+ readonly style: {
4105
+ readonly "--depth": string;
4106
+ };
4107
+ }
4108
+ export interface TimelineRulerRowsProps {
4109
+ readonly role: "presentation";
4110
+ readonly "aria-hidden": "true";
4111
+ /** Ticks of a nice step across the extent; render them as children. */
4112
+ readonly ticks: ReadonlyArray<TimelineTick>;
4113
+ }
4114
+ export interface TimelineRows {
4115
+ /** `{ start, end }` across the rows; `{ 0, 0 }` when there are none. */
4116
+ readonly extent: () => {
4117
+ readonly start: number;
4118
+ readonly end: number;
4119
+ };
4120
+ readonly depthOf: (id: string) => number;
4121
+ readonly listProps: TimelineListProps;
4122
+ readonly rowProps: (id: string) => TimelineSpanRowProps;
4123
+ readonly barProps: (id: string) => TimelineBarProps;
4124
+ readonly labelProps: (id: string) => TimelineLabelProps;
4125
+ readonly rulerProps: TimelineRulerRowsProps;
4126
+ /** Programmatic selection request; a controlled host may ignore it. */
4127
+ readonly select: (id: string) => void;
4128
+ readonly selectedId: Accessor<string | undefined>;
4129
+ }
4130
+ /** The demo five-span tree (`rows` absent) or real span data (`rows` given). */
4131
+ export declare function createTimeline(options: TimelineRowsOptions): TimelineRows;
4132
+ export declare function createTimeline(options?: TimelineOptions): Timeline;
3601
4133
  /** A toast's urgency; `error` is the only tone that announces assertively. */
3602
4134
  export type ToastTone = "info" | "success" | "error";
3603
4135
  export interface ToastOptions {