@cssints/headless 0.15.0 → 0.18.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
@@ -1374,7 +1374,9 @@ export declare class ConfirmPendingError extends Error {
1374
1374
  * Nesting: confirms from different `createConfirm` calls may be open together;
1375
1375
  * only the last one opened gets Escape and Tab (and Escape does not reach a
1376
1376
  * dialog it sits in), and the scroll lock is counted: the first takes it, the
1377
- * last to close restores what `overflow` was. One confirm still has one request.
1377
+ * last to close restores what `overflow` was. `createDrawer` claims the same
1378
+ * page-wide stack and lock, so a confirm over a drawer (or the reverse) nests
1379
+ * the same way. One confirm still has one request.
1378
1380
  *
1379
1381
  * One request at a time: a second `confirm()` while one is pending is rejected,
1380
1382
  * not queued. Queued, a double click on "Delete" would ask twice and a stale
@@ -1382,6 +1384,119 @@ export declare class ConfirmPendingError extends Error {
1382
1384
  * answered `false` when the owner is disposed.
1383
1385
  */
1384
1386
  export declare const createConfirm: (options?: ConfirmOptions) => Confirm;
1387
+ /** The edge a drawer occupies. `side()` reads the option at call time. */
1388
+ export type DrawerSide = "left" | "right" | "bottom";
1389
+ export interface DrawerOptions {
1390
+ /** Base for the generated trigger / content / backdrop ids. */
1391
+ readonly id?: string | undefined;
1392
+ /** Controlled: the host owns the open state and this is the only thing that changes it. */
1393
+ readonly open?: boolean | undefined;
1394
+ /** Uncontrolled starting state; ignored while `open` is set. */
1395
+ readonly defaultOpen?: boolean | undefined;
1396
+ /**
1397
+ * The edge the panel is anchored to. @default "right"
1398
+ *
1399
+ * Presentation only: a `bottom` panel is a sheet, `left` / `right` is a side
1400
+ * rail, and the same drawer is one API either way.
1401
+ */
1402
+ readonly side?: DrawerSide | undefined;
1403
+ /**
1404
+ * Whether the panel is modal: `aria-modal`, the Tab cycle, the scroll lock and
1405
+ * the backdrop. @default true
1406
+ *
1407
+ * This is the switch a breakpoint flips: below it a panel is a sheet and
1408
+ * modal, above it a side rail may not be. It is read on every attribute read,
1409
+ * so the host may change it while the drawer is open (crossing the
1410
+ * breakpoint) and the backdrop, `aria-modal` and the lock follow.
1411
+ */
1412
+ readonly modal?: boolean | undefined;
1413
+ /**
1414
+ * The width in px at which the host switches `side` and `modal` (a sheet
1415
+ * below it, a dialog above). **Not read here**: the primitive has no viewport
1416
+ * to measure and no `matchMedia`, so it only documents the pattern, and the
1417
+ * host watches the breakpoint and passes `modal` / `side`. A host that wants
1418
+ * the sheet drag behaviour composes CSS scroll snap around this, as
1419
+ * [docs/bottom-sheet.md](../docs/bottom-sheet.md) lays out.
1420
+ */
1421
+ readonly breakpoint?: number | undefined;
1422
+ /** A backdrop click or an outside `pointerdown` closes the drawer. @default true */
1423
+ readonly dismissable?: boolean | undefined;
1424
+ /** Id of the element naming the panel (`aria-labelledby`); a dialog without a name is unusable. */
1425
+ readonly labelledby?: string | undefined;
1426
+ /** A request to change the open state, called when the intent differs. */
1427
+ readonly onOpenChange?: ((open: boolean) => void) | undefined;
1428
+ }
1429
+ export interface DrawerTriggerProps {
1430
+ readonly id: string;
1431
+ readonly "aria-haspopup": "dialog";
1432
+ readonly "aria-expanded": "true" | "false";
1433
+ readonly "aria-controls": string;
1434
+ readonly onClick: () => void;
1435
+ readonly onKeyDown: (event: KeyboardEvent) => void;
1436
+ }
1437
+ export interface DrawerBackdropProps {
1438
+ readonly id: string;
1439
+ /** Always hidden when the drawer is closed or not modal: a non-modal drawer keeps the page clickable. */
1440
+ readonly hidden: boolean;
1441
+ readonly onClick: () => void;
1442
+ }
1443
+ export interface DrawerContentProps {
1444
+ readonly id: string;
1445
+ readonly role: "dialog";
1446
+ readonly "aria-modal": "true" | undefined;
1447
+ readonly "aria-labelledby": string | undefined;
1448
+ readonly tabindex: -1;
1449
+ readonly hidden: boolean;
1450
+ readonly onKeyDown: (event: KeyboardEvent) => void;
1451
+ }
1452
+ /** A bottom sheet's grab handle. Decorative: there is no drag, so it says nothing. */
1453
+ export interface DrawerHandleProps {
1454
+ readonly "aria-hidden": "true";
1455
+ }
1456
+ export interface Drawer {
1457
+ readonly open: Accessor<boolean>;
1458
+ readonly side: Accessor<DrawerSide>;
1459
+ readonly triggerProps: DrawerTriggerProps;
1460
+ readonly backdropProps: DrawerBackdropProps;
1461
+ readonly contentProps: DrawerContentProps;
1462
+ readonly handleProps: DrawerHandleProps;
1463
+ }
1464
+ /**
1465
+ * The shell for a drawer / bottom sheet: an edge-anchored panel over the
1466
+ * Dialog (Modal) semantics (https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/),
1467
+ * with the same shell as `createConfirm` — focus into the panel on open and back
1468
+ * to the trigger on close, Tab cycling while modal, Escape read from the
1469
+ * document (so it closes wherever focus is), a counted scroll lock and an
1470
+ * outside dismissal.
1471
+ *
1472
+ * **Compose, do not add a sheet primitive.** [docs/bottom-sheet.md](../docs/bottom-sheet.md)
1473
+ * settled it: the semantics are Dialog (Modal), the position, the slide and the
1474
+ * snap points are the host's CSS (native scroll snap), and the panel that both
1475
+ * need is this shell. `side` and `modal` are the whole presentation API: a sheet
1476
+ * is `{ side: "bottom", modal: true }`, a side rail is `{ side: "right" }` and a
1477
+ * non-modal rail is `{ side: "right", modal: false }`.
1478
+ *
1479
+ * **No drag-to-dismiss.** A drag is not a dialog interaction, it is a scroll
1480
+ * snap the platform runs off the main thread, and it belongs to the host's
1481
+ * scroll-snap CSS (see bottom-sheet.md: `scroll-snap-type: y mandatory`, snap
1482
+ * markers, `scrollsnapchange`). Mirroring a native snap in JS would be the
1483
+ * gesture re-implementation that the platform research forbids, and a pointer
1484
+ * gesture would exist on top of the snap that already moved the panel. So the
1485
+ * handle is decoration (`aria-hidden`, no pointer handlers): the ways out are
1486
+ * the backdrop, Escape, the close control the host puts in the panel, and the
1487
+ * trigger. Swipe-to-dismiss is a separate decision with its own oracle.
1488
+ *
1489
+ * The panel is edge-anchored, so there is **no `attach`**: nothing is measured
1490
+ * and no placement is computed. Focus and Tab find the panel through
1491
+ * `contentProps.id`, like `createConfirm` does, so the host only spreads
1492
+ * attributes onto its own elements.
1493
+ *
1494
+ * Not checked against a Quint spec: the dialog is, and this is its shell, so
1495
+ * `test/drawer.browser.test.tsx` is the oracle. A pointerdown outside the panel
1496
+ * closes it when `dismissable` (that is the backdrop click too, and the only
1497
+ * dismissal a non-modal drawer has); closing returns focus to the trigger.
1498
+ */
1499
+ export declare const createDrawer: (options?: DrawerOptions) => Drawer;
1385
1500
  /**
1386
1501
  * Options are read lazily (getters); pass a component's `props`.
1387
1502
  */
@@ -1642,6 +1757,128 @@ export interface Pagination {
1642
1757
  * Target size is the skin's job: 24x24 CSS px at AA, 44x44 at AAA.
1643
1758
  */
1644
1759
  export declare const createPagination: (options: PaginationOptions) => Pagination;
1760
+ /** A point in the viewport's own coordinate space: `0, 0` is its top-left corner. */
1761
+ export interface PanZoomPoint {
1762
+ readonly x: number;
1763
+ readonly y: number;
1764
+ }
1765
+ export interface PanZoomOptions {
1766
+ /** Smallest scale, and where `reset` returns to. Default 1. */
1767
+ readonly min?: number | undefined;
1768
+ /** Largest scale. Default 4. */
1769
+ readonly max?: number | undefined;
1770
+ /** The `+` / `-` / `zoomIn` / `zoomOut` increment. Default 0.25. */
1771
+ readonly step?: number | undefined;
1772
+ /** Starting scale when uncontrolled; ignored while `scale` is set. */
1773
+ readonly defaultScale?: number | undefined;
1774
+ /** Controlled: the host owns the scale and this is the only thing that changes it. */
1775
+ readonly scale?: number | undefined;
1776
+ /**
1777
+ * A request to change the scale, called when the intent differs from the
1778
+ * current value. Uncontrolled it is accepted; controlled the host may ignore it.
1779
+ */
1780
+ readonly onScaleChange?: (scale: number) => void;
1781
+ /** The pan changed (a drag, a pinch, an arrow key, a zoom about a point, a reset). */
1782
+ readonly onPanChange?: (offset: PanZoomPoint) => void;
1783
+ /**
1784
+ * `"clamp"` (default) keeps the scaled image covering the viewport: the offset
1785
+ * never opens a gap. `"free"` lets it be dragged out of view.
1786
+ */
1787
+ readonly bounds?: "clamp" | "free" | undefined;
1788
+ readonly labels?: {
1789
+ readonly viewport?: string;
1790
+ } | undefined;
1791
+ }
1792
+ export interface PanZoomViewportProps {
1793
+ readonly tabindex: number;
1794
+ readonly role: "group";
1795
+ readonly "aria-roledescription": string;
1796
+ readonly "aria-label": string;
1797
+ readonly "aria-describedby": string;
1798
+ readonly onKeyDown: (event: KeyboardEvent) => void;
1799
+ readonly onWheel: (event: WheelEvent) => void;
1800
+ readonly onPointerDown: (event: PointerEvent) => void;
1801
+ readonly onPointerMove: (event: PointerEvent) => void;
1802
+ readonly onPointerUp: (event: PointerEvent) => void;
1803
+ readonly onPointerCancel: (event: PointerEvent) => void;
1804
+ readonly onDblClick: (event: MouseEvent) => void;
1805
+ }
1806
+ /**
1807
+ * Spread onto the image. `style` is a getter returning a fresh object, so Solid
1808
+ * reads it inside the tracking scope; `draggable: false` keeps the native image
1809
+ * drag from swallowing the pan gesture.
1810
+ */
1811
+ export interface PanZoomImageProps {
1812
+ readonly draggable: false;
1813
+ readonly style: {
1814
+ readonly transform: string;
1815
+ };
1816
+ }
1817
+ /** The live value: the current percentage, in a polite status region (`aria-describedby` of the viewport). */
1818
+ export interface PanZoomValueProps {
1819
+ readonly id: string;
1820
+ readonly role: "status";
1821
+ readonly "aria-live": "polite";
1822
+ readonly "aria-atomic": "true";
1823
+ }
1824
+ export interface PanZoom {
1825
+ readonly scale: Accessor<number>;
1826
+ readonly offset: Accessor<PanZoomPoint>;
1827
+ /** Back to `min` and centred. */
1828
+ readonly reset: () => void;
1829
+ readonly zoomIn: () => void;
1830
+ readonly zoomOut: () => void;
1831
+ /** A request; `origin` is the viewport point to keep still (its centre by default). */
1832
+ readonly setScale: (scale: number, origin?: PanZoomPoint) => void;
1833
+ readonly viewportProps: PanZoomViewportProps;
1834
+ readonly imageProps: PanZoomImageProps;
1835
+ readonly valueProps: PanZoomValueProps;
1836
+ /** The human value of `valueProps`: `"150%"`. */
1837
+ readonly value: Accessor<string>;
1838
+ }
1839
+ /**
1840
+ * Pan and zoom for one image in a viewport: scale, offset and the prop objects
1841
+ * for a focusable viewport, the transformed image and a live value.
1842
+ *
1843
+ * **There is no APG pattern for pan-zoom.** The accessible contract is the three
1844
+ * parts this returns: a viewport that takes focus (`role=group`, `tabindex=0`,
1845
+ * `aria-roledescription="zoomable image"`), keyboard operations that stand in
1846
+ * for every pointer gesture (`+` / `=` / `-` zoom, arrows pan, `0` resets), and a
1847
+ * polite live region (`aria-describedby` of the viewport) that carries the
1848
+ * current scale. The pointer gestures are an addition for mouse and touch, never
1849
+ * the only way in. That contract is this module's own: the case it replaces
1850
+ * (kocharin-art-3's `Zoomable`, ~110 lines of pointer maths) had no keyboard path
1851
+ * and no ARIA at all, and `createCanvas` is the wrong model here — an infinite
1852
+ * canvas moves a camera over a world, while this is one image fitted to its
1853
+ * viewport.
1854
+ *
1855
+ * Wheel is zoom-with-a-modifier only: `ctrl` / `meta` (the browser's own pinch
1856
+ * arrives as `ctrl+wheel`) zooms about the cursor; a bare wheel is never
1857
+ * `preventDefault`ed, so the page keeps scrolling. Trackpad gestures differ per
1858
+ * platform and a swallowed wheel is the one thing that is never acceptable.
1859
+ *
1860
+ * Pointer: a drag pans, a second pointer pinches (about its midpoint), a double
1861
+ * click resets. Capture is best-effort (a synthetic pointer cannot be captured).
1862
+ *
1863
+ * `bounds: "clamp"` bounds the offset by half the viewport's scaled overflow, so
1864
+ * the image is expected to fill the viewport at `min` (the usual
1865
+ * `width: 100%; height: 100%` / `object-fit: cover` box). A letterboxed image
1866
+ * smaller than its viewport cannot be held gap-free; give it a viewport of its
1867
+ * own or `bounds: "free"`.
1868
+ *
1869
+ * Request/commit (`rt1p`): uncontrolled by default (`defaultScale`); pass `scale`
1870
+ * + `onScaleChange` to own the value. The offset has no controlled prop, so a
1871
+ * hosting page that rejects a scale request also gets the offset resolved for the
1872
+ * scale it refused — accept the request, or pass `bounds: "free"` and own the
1873
+ * pan elsewhere.
1874
+ *
1875
+ * The host owns the box and the looks: size the viewport, put `touch-action:
1876
+ * none` on it if a touch drag should pan, centre the image
1877
+ * (`transform-origin: 50% 50%` is the default this maths assumes), and hide the
1878
+ * live region visually rather than with `hidden` (a `hidden` live region is not
1879
+ * announced).
1880
+ */
1881
+ export declare const createPanZoom: (options?: PanZoomOptions) => PanZoom;
1645
1882
  export type PopoverPlacement = "bottom-start" | "bottom" | "bottom-end" | "top-start" | "top" | "top-end";
1646
1883
  /** A rect in one space, as `getBoundingClientRect` gives it. */
1647
1884
  export interface PopoverRect {
@@ -1912,6 +2149,53 @@ export declare const packSort: (column: number, dir: GridSortDirection) => numbe
1912
2149
  * snapshot); `rows`/`cols`/`page` are captured once.
1913
2150
  */
1914
2151
  export declare const createGrid: (options?: GridOptions) => Grid;
2152
+ /**
2153
+ * Hotkeys: one registry for the chords every app hand-rolls (the docs app's `/`
2154
+ * and Cmd/Ctrl+K, the CMS's Cmd/Ctrl+S), instead of a `keydown` listener per
2155
+ * screen.
2156
+ *
2157
+ * This is a platform utility, not a widget: there is no APG pattern for a
2158
+ * keyboard registry, so no role, no element, no styling, no Quint spec and no
2159
+ * ported scenario. The oracle is `test/hotkeys.browser.test.tsx` in a real
2160
+ * Chromium.
2161
+ */
2162
+ export interface HotkeyBinding {
2163
+ /**
2164
+ * A chord: the modifiers `mod` (Cmd on macOS, Ctrl elsewhere), `ctrl`, `alt`,
2165
+ * `shift`, `meta`, then exactly one key (`k`, `/`, `?`, `escape`, `enter`,
2166
+ * `arrowup`), joined with `+`; `"mod+k"`, `"shift+?"`, `"/"`.
2167
+ */
2168
+ readonly keys: string;
2169
+ readonly onTrigger: (event: KeyboardEvent) => void;
2170
+ /** Gates a binding whose precondition is off: it falls through to the next one. */
2171
+ readonly when?: (() => boolean) | undefined;
2172
+ /** `event.preventDefault()` when the binding fires; default true. */
2173
+ readonly preventDefault?: boolean | undefined;
2174
+ /** Fire while the focus is in an input, a textarea or a contenteditable; default false. */
2175
+ readonly allowInEditable?: boolean | undefined;
2176
+ /** Only events from inside this element: one panel's `"mod+s"` over a page's. */
2177
+ readonly scope?: HTMLElement | undefined;
2178
+ }
2179
+ export interface HotkeysOptions {
2180
+ /** Whether `mod` is Cmd; injected so a test does not read the runner's OS. */
2181
+ readonly isApple?: boolean | undefined;
2182
+ }
2183
+ export interface Hotkeys {
2184
+ /** Registers `binding`; the returned unbind is idempotent. The last bound wins. */
2185
+ readonly bind: (binding: HotkeyBinding) => () => void;
2186
+ /** Unbinds all of them and removes the listener; a later `bind` is a no-op. */
2187
+ readonly dispose: () => void;
2188
+ }
2189
+ /**
2190
+ * One `keydown` listener in the capture phase, one list of chords. A chord
2191
+ * fires when its modifiers match exactly, its `when` holds, its scope contains
2192
+ * the event's target, and the focus is not in a text field (unless
2193
+ * `allowInEditable`). The most recently bound match is the only one that fires,
2194
+ * so a modal's `"/"` overrides the page's and unbinding it brings the page's
2195
+ * back; `preventDefault` is on by default for the bindings whose chord the
2196
+ * browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
2197
+ */
2198
+ export declare const createHotkeys: (options?: HotkeysOptions) => Hotkeys;
1915
2199
  export interface LinkOptions {
1916
2200
  readonly href?: string | undefined;
1917
2201
  /** Render a native `<a href>` (the default when `href` is set). */
@@ -2486,6 +2770,76 @@ export interface ResizablePanels {
2486
2770
  * DOM is checked against it by the differential walk.
2487
2771
  */
2488
2772
  export declare const createResizablePanels: (options: ResizablePanelsOptions) => ResizablePanels;
2773
+ export interface ScrollspyEntry {
2774
+ readonly id: string;
2775
+ readonly label: string;
2776
+ }
2777
+ export interface ScrollspyOptions {
2778
+ /** The table of contents, in document order. The heading elements carry these ids. */
2779
+ readonly entries: ReadonlyArray<ScrollspyEntry>;
2780
+ /**
2781
+ * How far below the scrollport's top edge a heading counts as current, in px.
2782
+ * A host with a sticky header passes its height. @default 0
2783
+ */
2784
+ readonly offset?: number | undefined;
2785
+ /**
2786
+ * The scroll container; the document by default. Read once, when `attach`
2787
+ * runs, so a host that swaps containers attaches again.
2788
+ */
2789
+ readonly root?: (() => HTMLElement | undefined) | undefined;
2790
+ /** The active id changed. */
2791
+ readonly onChange?: ((id: string | undefined) => void) | undefined;
2792
+ }
2793
+ export interface ScrollspyNavProps {
2794
+ /** The navigation landmark's name; the host overrides it after the spread to translate it. */
2795
+ readonly "aria-label": string;
2796
+ }
2797
+ export interface ScrollspyListProps {
2798
+ readonly role: "list";
2799
+ }
2800
+ export interface ScrollspyItemProps {
2801
+ readonly role: "listitem";
2802
+ }
2803
+ export interface ScrollspyLinkProps {
2804
+ readonly href: string;
2805
+ readonly "aria-current": "location" | undefined;
2806
+ }
2807
+ export interface Scrollspy {
2808
+ /** The current entry's id; `undefined` only when there are no entries. */
2809
+ readonly active: Accessor<string | undefined>;
2810
+ readonly navProps: ScrollspyNavProps;
2811
+ readonly listProps: ScrollspyListProps;
2812
+ readonly itemProps: (id: string) => ScrollspyItemProps;
2813
+ readonly linkProps: (id: string) => ScrollspyLinkProps;
2814
+ /**
2815
+ * DOM wiring: one passive `scroll` listener on the root, which measures the
2816
+ * headings. Call it once mounted and call the teardown it returns on cleanup.
2817
+ */
2818
+ readonly attach: () => () => void;
2819
+ }
2820
+ /**
2821
+ * A scrollspy: a table of contents whose current entry is the last heading that
2822
+ * has reached the top of the scrollport, marked `aria-current="location"` on
2823
+ * its link (and nowhere else: exactly one link is current).
2824
+ *
2825
+ * **No APG pattern exists for a scrollspy**, so there is no Quint spec and no
2826
+ * ported scenarios: the contract is WAI-ARIA (`aria-current="location"`, the
2827
+ * `list` / `listitem` roles, the navigation landmark and its name) and HTML,
2828
+ * and `test/scrollspy.browser.test.tsx` is its oracle.
2829
+ *
2830
+ * **A scroll listener, not an `IntersectionObserver`.** "The last heading at or
2831
+ * above the offset" is a total order over every heading, while an observer only
2832
+ * reports what crossed its band: a band observer has to rebuild the order from
2833
+ * its enter/leave state, and it misreports after a jump (an anchor click, a
2834
+ * restored scroll position). The listener is passive, the measurements are
2835
+ * reads only (no writes, so no layout thrash), and a docs page has a dozen
2836
+ * headings — the same cost as the listener the page already keeps.
2837
+ *
2838
+ * Smooth scrolling is the host's (`scroll-behavior: smooth`): the links are
2839
+ * plain `#id` targets and nothing here moves the scrollport, so no
2840
+ * `prefers-reduced-motion` branch is needed either.
2841
+ */
2842
+ export declare const createScrollspy: (options: ScrollspyOptions) => Scrollspy;
2489
2843
  export type SliderOrientation = "horizontal" | "vertical";
2490
2844
  export type SliderDirection = "ltr" | "rtl";
2491
2845
  export interface SliderOptions {
@@ -2797,6 +3151,182 @@ export interface Switch {
2797
3151
  * pass `value` + `onValueChange` to own the state.
2798
3152
  */
2799
3153
  export declare const createSwitch: (options?: SwitchOptions) => Switch;
3154
+ /**
3155
+ * A data table on Solid 2: sortable column headers and row selection, as state
3156
+ * plus getter-backed attribute objects; no elements, no styling.
3157
+ *
3158
+ * **The host renders a native `<table>`**: `<caption>`, `<thead>`, `<tbody>`,
3159
+ * `<th scope="col">` and `<td>` carry the semantics, so the primitive adds no
3160
+ * `role` and no `aria-rowindex` — only what a native table cannot say itself
3161
+ * (which column is sorted, whether a row is selected, the mixed select-all
3162
+ * state, and the totals a windowed table hides).
3163
+ *
3164
+ * **Sorting is the host's data.** The primitive carries the sort descriptor and
3165
+ * announces it (`aria-sort`); it never sorts. The host maps `sort()` onto its
3166
+ * own array before rendering `rows`.
3167
+ *
3168
+ * Composes the two state models the package already has — `grid-sort.qnt` and
3169
+ * `grid-selection.qnt` — without their navigation: a table is a grid with a
3170
+ * `th[scope]` header row, and the cells keep their own focus.
3171
+ *
3172
+ * Request/commit (map ticket `rt1p`): the sort and the selection are
3173
+ * uncontrolled by default (`defaultSort` / `defaultSelected`) and can be owned
3174
+ * by the host with `sort` + `onSortChange` / `selected` + `onSelectionChange`.
3175
+ * `null` as `sort` is "no column sorted" (in a controlled host); `undefined` is
3176
+ * "uncontrolled". `toggleRow`, `toggleAll` and the header button are requests:
3177
+ * a controlled host may ignore them.
3178
+ */
3179
+ export type TableSortDirection = "ascending" | "descending";
3180
+ export type TableSelectionMode = "none" | "single" | "multiple";
3181
+ /**
3182
+ * `"tri-state"`: ascending -> descending -> unsorted, so a table can be put back
3183
+ * in its own order (the bean's requirement). `"toggle"`: ascending <-> descending,
3184
+ * never unsorted, which is what the APG grid example (and `grid-sort.qnt`) does.
3185
+ */
3186
+ export type TableSortCycle = "tri-state" | "toggle";
3187
+ export interface TableSort {
3188
+ readonly key: string;
3189
+ readonly direction: TableSortDirection;
3190
+ }
3191
+ export interface TableColumn {
3192
+ /** Identity of the column: what `cellProps` and `headerCellProps` are asked for. */
3193
+ readonly key: string;
3194
+ /** Defaults to `true`; `false` makes the header inert (no sort button, no `aria-sort`). */
3195
+ readonly sortable?: boolean | undefined;
3196
+ /**
3197
+ * A layout hint for the host's skin (right-aligned, tabular figures). HTML has
3198
+ * no attribute for it, so the primitive carries nothing.
3199
+ */
3200
+ readonly numeric?: boolean | undefined;
3201
+ /** The `abbr` of the header cell: the short form a screen reader uses. */
3202
+ readonly abbr?: string | undefined;
3203
+ }
3204
+ export interface TableLabels {
3205
+ /** The table's accessible name; a host that renders a `<caption>` leaves it out. */
3206
+ readonly table?: string | undefined;
3207
+ /** Defaults to "Select all rows". */
3208
+ readonly selectAll?: string | undefined;
3209
+ /** Defaults to `Select row <id>`. */
3210
+ readonly selectRow?: ((id: string) => string) | undefined;
3211
+ }
3212
+ export interface TableOptions {
3213
+ readonly columns: ReadonlyArray<TableColumn>;
3214
+ /**
3215
+ * The row ids the host renders, in render order. Ids, not a count: the sort
3216
+ * reorders the host's data and a selection must survive that, while an index
3217
+ * would then name a different row. The row's visible content stays the host's.
3218
+ */
3219
+ readonly rows: ReadonlyArray<string>;
3220
+ readonly selection?: TableSelectionMode | undefined;
3221
+ readonly sortCycle?: TableSortCycle | undefined;
3222
+ /** Controlled: the host owns the sort and this is the only thing that changes it; `null` is "unsorted". */
3223
+ readonly sort?: TableSort | null | undefined;
3224
+ /** Uncontrolled starting sort; ignored while `sort` is set. */
3225
+ readonly defaultSort?: TableSort | undefined;
3226
+ /** A request to re-sort (`undefined` = unsorted); uncontrolled it is accepted. */
3227
+ readonly onSortChange?: ((sort: TableSort | undefined) => void) | undefined;
3228
+ /** Controlled: the host owns the selected row ids and this is the only thing that changes them. */
3229
+ readonly selected?: ReadonlyArray<string> | undefined;
3230
+ /** Uncontrolled starting selection; ignored while `selected` is set. */
3231
+ readonly defaultSelected?: ReadonlyArray<string> | undefined;
3232
+ /** A request to change the selection; uncontrolled it is accepted. */
3233
+ readonly onSelectionChange?: ((selected: ReadonlyArray<string>) => void) | undefined;
3234
+ /**
3235
+ * The total row count when the host renders a window of `rows`
3236
+ * (`aria-rowcount`); absent on a table that renders all of them.
3237
+ */
3238
+ readonly rowCount?: number | undefined;
3239
+ /**
3240
+ * The total column count when the host renders a window of `columns`
3241
+ * (`aria-colcount` and `aria-colindex`); absent on a table that renders all.
3242
+ */
3243
+ readonly columnCount?: number | undefined;
3244
+ readonly labels?: TableLabels | undefined;
3245
+ /** Base for generated ids. */
3246
+ readonly id?: string | undefined;
3247
+ }
3248
+ /** Spread onto `<table>`; the host renders the table, the head and the body. */
3249
+ export interface TableProps {
3250
+ /** `labels.table`; leave it out when the host renders a caption. */
3251
+ readonly "aria-label": string | undefined;
3252
+ /** Only while the host windows its rows. */
3253
+ readonly "aria-rowcount": number | undefined;
3254
+ /** Only while the host windows its columns. */
3255
+ readonly "aria-colcount": number | undefined;
3256
+ }
3257
+ /** Spread onto `<caption>`. */
3258
+ export interface TableCaptionProps {
3259
+ readonly id: string;
3260
+ }
3261
+ /** Spread onto `<th scope="col">` in the header row. */
3262
+ export interface TableHeaderCellProps {
3263
+ readonly scope: "col";
3264
+ /** The sorted column only; an unsorted one carries nothing (ARIA's default). */
3265
+ readonly "aria-sort": TableSortDirection | undefined;
3266
+ readonly abbr: string | undefined;
3267
+ }
3268
+ /** Spread onto the `<button type="button">` inside a sortable `<th>`. */
3269
+ export interface TableHeaderButtonProps {
3270
+ readonly type: "button";
3271
+ readonly onClick: () => void;
3272
+ }
3273
+ /** Spread onto `<tr>` in the body. */
3274
+ export interface TableRowProps {
3275
+ readonly "aria-selected": "true" | "false" | undefined;
3276
+ /** Selects the row; `undefined` when nothing is selectable. */
3277
+ readonly onClick: (() => void) | undefined;
3278
+ }
3279
+ /** Spread onto a `<td>`; `aria-colindex` only while the host windows its columns. */
3280
+ export interface TableCellProps {
3281
+ readonly "aria-colindex": number | undefined;
3282
+ }
3283
+ /**
3284
+ * Spread onto the select-all control: a `role=checkbox` element (not a native
3285
+ * `input`, which spells the mixed state as a DOM property, not an attribute).
3286
+ */
3287
+ export interface TableSelectAllProps {
3288
+ readonly role: "checkbox";
3289
+ readonly "aria-checked": "true" | "false" | "mixed";
3290
+ readonly "aria-label": string;
3291
+ readonly tabindex: 0;
3292
+ readonly onClick: () => void;
3293
+ readonly onKeyDown: (event: KeyboardEvent) => void;
3294
+ }
3295
+ /** Spread onto a row's selection control. */
3296
+ export interface TableRowCheckboxProps {
3297
+ readonly role: "checkbox";
3298
+ readonly "aria-checked": "true" | "false";
3299
+ readonly "aria-label": string;
3300
+ readonly tabindex: 0;
3301
+ /** Selects the row and stops there: the control sits inside the row, whose click would select again. */
3302
+ readonly onClick: (event: MouseEvent) => void;
3303
+ readonly onKeyDown: (event: KeyboardEvent) => void;
3304
+ }
3305
+ export interface Table {
3306
+ readonly selectionMode: Accessor<TableSelectionMode>;
3307
+ /** The sort descriptor, or `undefined` when nothing is sorted. */
3308
+ readonly sort: Accessor<TableSort | undefined>;
3309
+ readonly selected: Accessor<ReadonlyArray<string>>;
3310
+ readonly isSelected: (id: string) => boolean;
3311
+ readonly tableProps: TableProps;
3312
+ readonly captionProps: TableCaptionProps;
3313
+ readonly headerCellProps: (key: string) => TableHeaderCellProps;
3314
+ /** `undefined` for a column that is not sortable — render no button there. */
3315
+ readonly headerButtonProps: (key: string) => TableHeaderButtonProps | undefined;
3316
+ readonly rowProps: (id: string) => TableRowProps;
3317
+ readonly cellProps: (rowId: string, key: string) => TableCellProps;
3318
+ /** `undefined` unless `selection` is `"multiple"`: one control over one selection is meaningless. */
3319
+ readonly selectAllProps: TableSelectAllProps | undefined;
3320
+ /** `undefined` while `selection` is `"none"`. */
3321
+ readonly rowCheckboxProps: (id: string) => TableRowCheckboxProps | undefined;
3322
+ readonly toggleAll: () => void;
3323
+ readonly toggleRow: (id: string) => void;
3324
+ }
3325
+ /**
3326
+ * Every option is read when an attribute is read, so pass props/getters, never a
3327
+ * destructured snapshot. `id` is captured once: it is the base of the caption id.
3328
+ */
3329
+ export declare const createTable: (options: TableOptions) => Table;
2800
3330
  /** The HTML `autocomplete` tokens worth naming; the attribute also takes an arbitrary token. */
2801
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";
2802
3332
  export interface TextInputOptions extends FieldOptions {