@cssints/headless 0.17.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/README.md CHANGED
@@ -65,7 +65,8 @@ A primitive that has a seam honours an optional `prefs` entry (a `Partial<Prefs>
65
65
  - **menu / menubar** — `typeaheadDelayMs` (the typeahead buffer timeout).
66
66
  - **tooltip** — `tooltipShowDelayMs` / `tooltipHideDelayMs`, exposed as `showDelayMs()` / `hideDelayMs()` for the host to wire its `setTimeout` from (the spec keeps timers as events).
67
67
 
68
- Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, confirm, dialog-modal, disclosure, feed, field, grid, link, listbox, meter, player, popover, radio, resizable-panels, slider, slider-multithumb, spinbutton, switch, table, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
68
+ Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, confirm, dialog-modal, disclosure, feed, field, grid, link, listbox, meter, player, popover, radio, resizable-panels, scrollspy, slider, slider-multithumb, spinbutton, switch, table, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
69
+ Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, confirm, dialog-modal, disclosure, feed, field, grid, link, listbox, meter, panzoom, player, popover, radio, resizable-panels, slider, slider-multithumb, spinbutton, switch, table, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
69
70
 
70
71
  `motionDurationMs` is **not consumed**: canvas tweens are tick-driven, not millisecond-driven, and the look layer owns durations. It stays in `Prefs` only so a host's preference record fits the type.
71
72
 
@@ -89,6 +90,41 @@ Membership, sizes and z-order are **not modelled** in `pointer.qnt` (its node se
89
90
 
90
91
  **Nested canvases** are two instances the host composes: a `#canvas` node holds a second `createCanvas`, and the inner viewport stops propagation of its own pointer, wheel and key events. The innermost canvas under the pointer owns the event, so a press or zoom inside it never starts an outer gesture or moves the outer camera. A nested canvas is a single tab stop in its parent.
91
92
 
93
+ ## Pan and zoom
94
+
95
+ `createPanZoom` is one image fitted to a viewport: the scale, the offset and the prop objects for the viewport, the image and a live value. **There is no APG pattern for pan-zoom**, so there is no Quint spec and no ported scenarios: the contract is this module's own and `test/panzoom.browser.test.tsx` is its oracle. The case it replaces (kocharin-art-3's `Zoomable`: ~110 lines of pointer maths) had no keyboard path and no ARIA at all.
96
+
97
+ **The accessible contract is three parts, and the pointer is a bonus:** a viewport that takes focus (`role=group`, `tabindex=0`, `aria-roledescription="zoomable image"`, `aria-describedby` the value), keyboard operations that stand in for every gesture (`+` / `=` / `-` zoom by `step`, arrows pan by 24 px, Shift ×4, `0` resets), and a polite live region (`role=status`, `aria-live=polite`, the current `"150%"`). `valueProps` deliberately carries no `hidden`: a `hidden` live region is not announced, so the host hides it visually.
98
+
99
+ **Not `createCanvas`.** The canvas is a camera over an infinite world with nodes, culling, snap and a z-order; a picture is one image in a box. There is no world, no node set and no reason to carry the camera machine.
100
+
101
+ ```tsx
102
+ const panzoom = createPanZoom({ min: 1, max: 4, step: 0.25 });
103
+
104
+ <div {...panzoom.viewportProps} class="viewport">
105
+ <img {...panzoom.imageProps} src={src} alt={alt} />
106
+ <span {...panzoom.valueProps} class="visually-hidden">
107
+ {panzoom.value()}
108
+ </span>
109
+ </div>;
110
+ ```
111
+
112
+ **Gestures.** A drag pans, a second pointer pinches about its midpoint, a double click resets. Pinch is two pointers and the distance between them, not a platform gesture, so it works where `PointerEvent` does. **Wheel is zoom only with `ctrl` / `meta`** — the browser's own pinch arrives as `ctrl+wheel` — and a bare wheel is never `preventDefault`ed, so the page keeps scrolling; a swallowed wheel is the one thing that is never acceptable on a trackpad.
113
+
114
+ **`bounds: "clamp"`** (the default) bounds the offset by half the viewport's scaled overflow, so the image is expected to fill the viewport at `min` (`width: 100%; height: 100%` / `object-fit: cover`). A letterboxed image smaller than its viewport cannot be held gap-free: give it a viewport of its own or `bounds: "free"`.
115
+
116
+ Left to the host: the box (the viewport's size and `overflow: hidden`, `touch-action: none` if a touch drag should pan), the look (a transition on the transform, `transform-origin: 50% 50%` — the default this maths assumes), and the image itself. Not modelled: inertia, rotation, and a keyboard pan at scale `min` (the clamp means there is nothing to pan, so the arrows are inert).
117
+
118
+ ```tsx
119
+ const panzoom = createPanZoom({
120
+ scale, // controlled: the host owns the scale
121
+ onScaleChange: setScale,
122
+ onPanChange: (offset) => save({ x: offset.x, y: offset.y }),
123
+ });
124
+ ```
125
+
126
+ Controlled is `scale` + `onScaleChange`; the offset has no controlled prop, so a host that ignores a scale request also gets the offset resolved for the scale it refused.
127
+
92
128
  ## Resizable panels
93
129
 
94
130
  `createResizablePanels` is a group of N panes and N-1 separators: react-resizable-panels' layout on the APG window splitter's keyboard. Its behaviour is `packages/apg-specs/specs/patterns/resizable-panels.qnt` — not an APG pattern, so it lives under `patterns/` — and the DOM is checked against it by the differential walk. The spec is the three-pane instance (10/80 bounds, 10% steps, pane 0 collapsible at 0); the primitive takes N panes and any bounds.
@@ -382,6 +418,74 @@ const drawer = createDrawer({ side: "right", labelledby: "nav-title" });
382
418
  - **No drag-to-dismiss.** A drag is not a dialog interaction: it is a scroll snap the platform runs off the main thread, and it is the host's CSS (`scroll-snap-type: y mandatory`, snap markers, `scrollsnapchange`; see [docs/bottom-sheet.md](docs/bottom-sheet.md)). Re-implementing a native snap in pointer events would be the gesture duplication that research forbids, and the handle would drag on top of the snap that already moved the panel. So `handleProps` is decoration (`aria-hidden`, no pointer handlers) and the ways out are the backdrop, Escape, a close control the host puts in the panel, and the trigger. Swipe-to-dismiss, if it is ever wanted, is a separate decision with its own oracle.
383
419
  - **No `attach`.** The panel is edge-anchored: nothing is measured and no placement is computed, so focus and the Tab cycle find the panel through `contentProps.id` and the host only spreads attributes onto its own elements. Give the panel a name: pass `labelledby`, or set `aria-labelledby` / `aria-label` after the spread.
384
420
 
421
+ ## Hotkeys
422
+
423
+ `createHotkeys` is a **platform utility, not a widget**: one registry for the chords every app hand-rolls (the docs app's `/` and Cmd/Ctrl+K, the CMS's Cmd/Ctrl+S). **No APG pattern exists for a keyboard registry**, so there is no role, no element, no styling and no Quint spec; `test/hotkeys.browser.test.tsx` is the oracle.
424
+
425
+ ```tsx
426
+ const hotkeys = createHotkeys();
427
+
428
+ onSettled(() => {
429
+ hotkeys.bind({
430
+ keys: "mod+k",
431
+ onTrigger: () => searchEl.focus(),
432
+ });
433
+ hotkeys.bind({
434
+ keys: "/",
435
+ when: () => !dialog().open,
436
+ onTrigger: () => searchEl.focus(),
437
+ });
438
+ hotkeys.bind({
439
+ keys: "mod+s",
440
+ scope: editorEl,
441
+ onTrigger: () => save(),
442
+ });
443
+ return () => hotkeys.dispose();
444
+ });
445
+ ```
446
+
447
+ - **One listener.** `bind` adds a chord to one `keydown` listener on the document in the capture phase (a `scope` only filters the event's target, it does not add a listener), and the last binding to go is the one that removes it; `dispose` unbinds everything and a later `bind` is a no-op. The unbind `bind` returns is idempotent.
448
+ - **Modifiers match exactly.** `mod` is Cmd on macOS and Ctrl elsewhere (`isApple` injects the answer, so a test does not read the runner's OS); `ctrl`, `alt`, `shift` and `meta` are as written. A chord that does not declare Shift does not fire with Shift held. A shifted character implies Shift: `"?"` fires from Shift+/ even without `"shift+?"`, and `"/"` does not fire from it.
449
+ - **A text field keeps its keys.** A binding does not fire while the focus is in an `input`, a `textarea` or a `contenteditable` (an inherited one included) unless it sets `allowInEditable`, which is what Escape wants: it closes a dialog wherever the focus is.
450
+ - **Last bound wins.** Only the most recently bound match fires, so a modal's `"/"` shadows the page's and unbinding it brings the page's back; a `when` that is false makes the next match fire instead, so the gate can close without unbinding.
451
+ - **`preventDefault` is on** for a binding that fires (Cmd/Ctrl+S and Cmd/Ctrl+K would otherwise reach the browser); pass `false` to let the platform act too.
452
+
453
+ Collisions with the browser's own chords: `preventDefault` reaches the page's default action, not the browser's. Cmd/Ctrl+S (Save Page), Cmd/Ctrl+K (the browser's search, in Chrome and Firefox), Cmd/Ctrl+P and `/` (Firefox's quick find) can be cancelled by a page, and a host that binds one is choosing to take it from the browser; Cmd/Ctrl+T, Cmd/Ctrl+W, Cmd/Ctrl+N, Cmd/Ctrl+L and F11 are reserved and cannot be intercepted, so a chord built on them never fires. `keys` takes any of them the same and says nothing.
454
+
455
+ ## Scrollspy
456
+
457
+ `createScrollspy` is a table of contents whose current entry is the last heading to reach the top of the scrollport. **No APG pattern exists for a scrollspy** (the APG has no table-of-contents pattern), so there is no Quint spec and no ported scenarios: the contract is WAI-ARIA — `aria-current="location"` on the current link, exactly one link at a time, `role="list"` / `role="listitem"` for the list, and a named navigation landmark — plus HTML (`#id` targets). `test/scrollspy.browser.test.tsx` is its oracle.
458
+
459
+ ```tsx
460
+ const toc = createScrollspy({
461
+ entries, // { id, label } in document order: the ids are the headings'
462
+ offset: 96, // a sticky header's height
463
+ onChange: (id) => track(id),
464
+ });
465
+
466
+ onSettled(() => toc.attach()); // returns the teardown
467
+
468
+ <nav {...toc.navProps}>
469
+ <ul {...toc.listProps}>
470
+ <For each={entries}>
471
+ {(entry) => (
472
+ <li {...toc.itemProps(entry.id)}>
473
+ <a {...toc.linkProps(entry.id)}>{entry.label}</a>
474
+ </li>
475
+ )}
476
+ </For>
477
+ </ul>
478
+ </nav>;
479
+ ```
480
+
481
+ - **A scroll listener, not an `IntersectionObserver`.** "The last heading at or above the offset" is a total order over every heading, and an observer only reports what crossed its band: a band observer has to rebuild the order from its enter/leave state and misreports after a jump (an anchor click, a restored scroll position). The listener is passive, the measurements are reads only (no writes, so no layout thrash), and a docs page has a dozen headings — the same cost as the listener the page already keeps.
482
+ - **`attach()`** adds that one listener to `root()` (the document by default; read once, so attach again after swapping containers), calls it once to sync a page opened mid-scroll, and returns the teardown. Headings are looked up by id on every scroll, so they may render after `attach`; an id with no element is skipped.
483
+ - **`offset`** is px below the scrollport's top edge, not the viewport's, so an inner container plus a sticky header works: `offset` is the header's height inside that container.
484
+ - **Before any scroll the first entry is current**, and it stays current while nothing has passed the line. `active()` is `undefined` only when there are no entries.
485
+ - **Exactly one `aria-current="location"`**, on the link, never on the heading: the heading's own `id` is the target. Skin it with `a[aria-current="location"]`.
486
+ - **Smooth scrolling is the host's.** The links are plain `#id` targets, the primitive never moves the scrollport, so `scroll-behavior: smooth` (and any `prefers-reduced-motion` override) belongs to the page's CSS; there is no scroll helper to reduce.
487
+ - **"On this page" is English.** Spread `navProps` first and override it after: `<nav {...toc.navProps} aria-label={t("onThisPage")}>`.
488
+
385
489
  ## Commands
386
490
 
387
491
  ```sh
package/dist/index.d.mts CHANGED
@@ -1757,6 +1757,128 @@ export interface Pagination {
1757
1757
  * Target size is the skin's job: 24x24 CSS px at AA, 44x44 at AAA.
1758
1758
  */
1759
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;
1760
1882
  export type PopoverPlacement = "bottom-start" | "bottom" | "bottom-end" | "top-start" | "top" | "top-end";
1761
1883
  /** A rect in one space, as `getBoundingClientRect` gives it. */
1762
1884
  export interface PopoverRect {
@@ -2027,6 +2149,53 @@ export declare const packSort: (column: number, dir: GridSortDirection) => numbe
2027
2149
  * snapshot); `rows`/`cols`/`page` are captured once.
2028
2150
  */
2029
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;
2030
2199
  export interface LinkOptions {
2031
2200
  readonly href?: string | undefined;
2032
2201
  /** Render a native `<a href>` (the default when `href` is set). */
@@ -2601,6 +2770,76 @@ export interface ResizablePanels {
2601
2770
  * DOM is checked against it by the differential walk.
2602
2771
  */
2603
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;
2604
2843
  export type SliderOrientation = "horizontal" | "vertical";
2605
2844
  export type SliderDirection = "ltr" | "rtl";
2606
2845
  export interface SliderOptions {
package/dist/index.mjs CHANGED
@@ -3266,6 +3266,304 @@ const createPagination = (options) => {
3266
3266
  }
3267
3267
  };
3268
3268
  };
3269
+ // src/panzoom.ts
3270
+ /** Arrow-key pan distance, in CSS px; Shift multiplies it. */
3271
+ const PAN_STEP = 24;
3272
+ const PAN_SHIFT = 4;
3273
+ /** A ctrl/meta wheel notch. */
3274
+ const WHEEL_FACTOR = 1.1;
3275
+ const axis = (value, limit) => Math.min(limit, Math.max(-limit, value));
3276
+ /**
3277
+ * Pan and zoom for one image in a viewport: scale, offset and the prop objects
3278
+ * for a focusable viewport, the transformed image and a live value.
3279
+ *
3280
+ * **There is no APG pattern for pan-zoom.** The accessible contract is the three
3281
+ * parts this returns: a viewport that takes focus (`role=group`, `tabindex=0`,
3282
+ * `aria-roledescription="zoomable image"`), keyboard operations that stand in
3283
+ * for every pointer gesture (`+` / `=` / `-` zoom, arrows pan, `0` resets), and a
3284
+ * polite live region (`aria-describedby` of the viewport) that carries the
3285
+ * current scale. The pointer gestures are an addition for mouse and touch, never
3286
+ * the only way in. That contract is this module's own: the case it replaces
3287
+ * (kocharin-art-3's `Zoomable`, ~110 lines of pointer maths) had no keyboard path
3288
+ * and no ARIA at all, and `createCanvas` is the wrong model here — an infinite
3289
+ * canvas moves a camera over a world, while this is one image fitted to its
3290
+ * viewport.
3291
+ *
3292
+ * Wheel is zoom-with-a-modifier only: `ctrl` / `meta` (the browser's own pinch
3293
+ * arrives as `ctrl+wheel`) zooms about the cursor; a bare wheel is never
3294
+ * `preventDefault`ed, so the page keeps scrolling. Trackpad gestures differ per
3295
+ * platform and a swallowed wheel is the one thing that is never acceptable.
3296
+ *
3297
+ * Pointer: a drag pans, a second pointer pinches (about its midpoint), a double
3298
+ * click resets. Capture is best-effort (a synthetic pointer cannot be captured).
3299
+ *
3300
+ * `bounds: "clamp"` bounds the offset by half the viewport's scaled overflow, so
3301
+ * the image is expected to fill the viewport at `min` (the usual
3302
+ * `width: 100%; height: 100%` / `object-fit: cover` box). A letterboxed image
3303
+ * smaller than its viewport cannot be held gap-free; give it a viewport of its
3304
+ * own or `bounds: "free"`.
3305
+ *
3306
+ * Request/commit (`rt1p`): uncontrolled by default (`defaultScale`); pass `scale`
3307
+ * + `onScaleChange` to own the value. The offset has no controlled prop, so a
3308
+ * hosting page that rejects a scale request also gets the offset resolved for the
3309
+ * scale it refused — accept the request, or pass `bounds: "free"` and own the
3310
+ * pan elsewhere.
3311
+ *
3312
+ * The host owns the box and the looks: size the viewport, put `touch-action:
3313
+ * none` on it if a touch drag should pan, centre the image
3314
+ * (`transform-origin: 50% 50%` is the default this maths assumes), and hide the
3315
+ * live region visually rather than with `hidden` (a `hidden` live region is not
3316
+ * announced).
3317
+ */
3318
+ const createPanZoom = (options = {}) => {
3319
+ const min = options.min ?? 1;
3320
+ const max = options.max ?? 4;
3321
+ const step = options.step ?? .25;
3322
+ const bounds = () => options.bounds ?? "clamp";
3323
+ const valueId = createUniqueId();
3324
+ const clamp = (next) => Math.min(max, Math.max(min, next));
3325
+ const [scale, requestScale] = createControllable({
3326
+ value: () => options.scale,
3327
+ initial: clamp(options.defaultScale ?? min),
3328
+ onChange: () => options.onScaleChange
3329
+ });
3330
+ const [version, setVersion] = createSignal(0);
3331
+ let last = {
3332
+ x: 0,
3333
+ y: 0
3334
+ };
3335
+ const offset = () => {
3336
+ version();
3337
+ return last;
3338
+ };
3339
+ const setOffset = (next) => {
3340
+ last = next;
3341
+ setVersion((count) => count + 1);
3342
+ };
3343
+ let box = {
3344
+ left: 0,
3345
+ top: 0,
3346
+ width: 0,
3347
+ height: 0
3348
+ };
3349
+ const measure = (event) => {
3350
+ if (!(event.currentTarget instanceof Element)) return;
3351
+ const rect = event.currentTarget.getBoundingClientRect();
3352
+ box = {
3353
+ left: rect.left,
3354
+ top: rect.top,
3355
+ width: rect.width,
3356
+ height: rect.height
3357
+ };
3358
+ };
3359
+ const limits = (at) => ({
3360
+ x: Math.max(0, box.width * (at - 1) / 2),
3361
+ y: Math.max(0, box.height * (at - 1) / 2)
3362
+ });
3363
+ const move = (next, at = scale()) => {
3364
+ const allowed = limits(at);
3365
+ const target = bounds() === "free" ? next : {
3366
+ x: axis(next.x, allowed.x),
3367
+ y: axis(next.y, allowed.y)
3368
+ };
3369
+ const previous = offset();
3370
+ if (target.x === previous.x && target.y === previous.y) return;
3371
+ setOffset(target);
3372
+ options.onPanChange?.(target);
3373
+ };
3374
+ const setScale = (next, origin) => {
3375
+ const before = scale();
3376
+ const target = clamp(next);
3377
+ if (target === before) return;
3378
+ requestScale(target);
3379
+ const at = origin ?? {
3380
+ x: box.width / 2,
3381
+ y: box.height / 2
3382
+ };
3383
+ const o = {
3384
+ x: at.x - box.width / 2,
3385
+ y: at.y - box.height / 2
3386
+ };
3387
+ const ratio = before > 0 ? target / before : 1;
3388
+ const current = offset();
3389
+ move({
3390
+ x: o.x - (o.x - current.x) * ratio,
3391
+ y: o.y - (o.y - current.y) * ratio
3392
+ }, target);
3393
+ };
3394
+ const zoomIn = () => setScale(scale() + step);
3395
+ const zoomOut = () => setScale(scale() - step);
3396
+ const reset = () => {
3397
+ requestScale(min);
3398
+ move({
3399
+ x: 0,
3400
+ y: 0
3401
+ }, min);
3402
+ };
3403
+ const pointers = /* @__PURE__ */ new Map();
3404
+ let drag;
3405
+ let pinch;
3406
+ const distance = () => {
3407
+ const [a, b] = [...pointers.values()];
3408
+ return a === void 0 || b === void 0 ? 0 : Math.hypot(a.x - b.x, a.y - b.y);
3409
+ };
3410
+ const midpoint = () => {
3411
+ const [a, b] = [...pointers.values()];
3412
+ if (a === void 0 || b === void 0) return {
3413
+ x: box.left + box.width / 2,
3414
+ y: box.top + box.height / 2
3415
+ };
3416
+ return {
3417
+ x: (a.x + b.x) / 2,
3418
+ y: (a.y + b.y) / 2
3419
+ };
3420
+ };
3421
+ const local = (point) => ({
3422
+ x: point.x - box.left,
3423
+ y: point.y - box.top
3424
+ });
3425
+ const pointerDown = (event) => {
3426
+ measure(event);
3427
+ if (event.button !== 0) return;
3428
+ try {
3429
+ if (event.currentTarget instanceof Element) event.currentTarget.setPointerCapture(event.pointerId);
3430
+ } catch {}
3431
+ const at = {
3432
+ x: event.clientX,
3433
+ y: event.clientY
3434
+ };
3435
+ pointers.set(event.pointerId, at);
3436
+ drag = {
3437
+ at,
3438
+ from: offset()
3439
+ };
3440
+ if (pointers.size === 2) pinch = {
3441
+ distance: distance(),
3442
+ scale: scale()
3443
+ };
3444
+ };
3445
+ const pointerMove = (event) => {
3446
+ if (!pointers.has(event.pointerId)) return;
3447
+ pointers.set(event.pointerId, {
3448
+ x: event.clientX,
3449
+ y: event.clientY
3450
+ });
3451
+ if (pointers.size === 2 && pinch !== void 0) {
3452
+ if (pinch.distance > 0) setScale(pinch.scale * (distance() / pinch.distance), local(midpoint()));
3453
+ return;
3454
+ }
3455
+ if (drag !== void 0) move({
3456
+ x: drag.from.x + event.clientX - drag.at.x,
3457
+ y: drag.from.y + event.clientY - drag.at.y
3458
+ });
3459
+ };
3460
+ const pointerUp = (event) => {
3461
+ pointers.delete(event.pointerId);
3462
+ pinch = void 0;
3463
+ const rest = [...pointers.values()][0];
3464
+ drag = rest === void 0 ? void 0 : {
3465
+ at: rest,
3466
+ from: offset()
3467
+ };
3468
+ };
3469
+ const wheel = (event) => {
3470
+ if (!event.ctrlKey && !event.metaKey) return;
3471
+ event.preventDefault();
3472
+ measure(event);
3473
+ const factor = event.deltaY < 0 ? WHEEL_FACTOR : 1 / WHEEL_FACTOR;
3474
+ setScale(scale() * factor, {
3475
+ x: event.clientX - box.left,
3476
+ y: event.clientY - box.top
3477
+ });
3478
+ };
3479
+ const keyDown = (event) => {
3480
+ measure(event);
3481
+ const by = PAN_STEP * (event.shiftKey ? PAN_SHIFT : 1);
3482
+ const current = offset();
3483
+ switch (event.key) {
3484
+ case "+":
3485
+ case "=":
3486
+ zoomIn();
3487
+ break;
3488
+ case "-":
3489
+ case "_":
3490
+ zoomOut();
3491
+ break;
3492
+ case "0":
3493
+ reset();
3494
+ break;
3495
+ case "ArrowLeft":
3496
+ move({
3497
+ x: current.x - by,
3498
+ y: current.y
3499
+ });
3500
+ break;
3501
+ case "ArrowRight":
3502
+ move({
3503
+ x: current.x + by,
3504
+ y: current.y
3505
+ });
3506
+ break;
3507
+ case "ArrowUp":
3508
+ move({
3509
+ x: current.x,
3510
+ y: current.y - by
3511
+ });
3512
+ break;
3513
+ case "ArrowDown":
3514
+ move({
3515
+ x: current.x,
3516
+ y: current.y + by
3517
+ });
3518
+ break;
3519
+ default: return;
3520
+ }
3521
+ event.preventDefault();
3522
+ };
3523
+ return {
3524
+ scale,
3525
+ offset,
3526
+ reset,
3527
+ zoomIn,
3528
+ zoomOut,
3529
+ setScale,
3530
+ value: () => `${Math.round(scale() * 100)}%`,
3531
+ viewportProps: {
3532
+ tabindex: 0,
3533
+ role: "group",
3534
+ "aria-roledescription": "zoomable image",
3535
+ get "aria-label"() {
3536
+ return options.labels?.viewport ?? "Image";
3537
+ },
3538
+ get "aria-describedby"() {
3539
+ return valueId;
3540
+ },
3541
+ onKeyDown: keyDown,
3542
+ onWheel: wheel,
3543
+ onPointerDown: pointerDown,
3544
+ onPointerMove: pointerMove,
3545
+ onPointerUp: pointerUp,
3546
+ onPointerCancel: pointerUp,
3547
+ onDblClick: (event) => {
3548
+ measure(event);
3549
+ reset();
3550
+ }
3551
+ },
3552
+ imageProps: {
3553
+ draggable: false,
3554
+ get style() {
3555
+ const at = offset();
3556
+ return { transform: `translate(${at.x}px, ${at.y}px) scale(${scale()})` };
3557
+ }
3558
+ },
3559
+ valueProps: {
3560
+ id: valueId,
3561
+ role: "status",
3562
+ "aria-live": "polite",
3563
+ "aria-atomic": "true"
3564
+ }
3565
+ };
3566
+ };
3269
3567
  // src/popover.ts
3270
3568
  const FLIPPED = {
3271
3569
  "bottom-start": "top-start",
@@ -3833,6 +4131,109 @@ const createGrid = (options = {}) => {
3833
4131
  }
3834
4132
  };
3835
4133
  };
4134
+ // src/hotkeys.ts
4135
+ const MODIFIERS = /* @__PURE__ */ new Set([
4136
+ "mod",
4137
+ "ctrl",
4138
+ "alt",
4139
+ "shift",
4140
+ "meta"
4141
+ ]);
4142
+ const SHIFTED = /* @__PURE__ */ new Set("~!@#$%^&*()_+{}|:\"<>?");
4143
+ const detectsApple = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
4144
+ const parse = (keys, isApple) => {
4145
+ const parts = keys.toLowerCase().split("+");
4146
+ const key = parts.pop() ?? "";
4147
+ if (key === "" || MODIFIERS.has(key)) throw new TypeError(`hotkeys: "${keys}" ends with no key`);
4148
+ const chord = {
4149
+ ctrl: false,
4150
+ meta: false,
4151
+ alt: false,
4152
+ shift: SHIFTED.has(key),
4153
+ key
4154
+ };
4155
+ for (const part of parts) switch (part) {
4156
+ case "mod":
4157
+ if (isApple) chord.meta = true;
4158
+ else chord.ctrl = true;
4159
+ break;
4160
+ case "ctrl":
4161
+ chord.ctrl = true;
4162
+ break;
4163
+ case "meta":
4164
+ chord.meta = true;
4165
+ break;
4166
+ case "alt":
4167
+ chord.alt = true;
4168
+ break;
4169
+ case "shift":
4170
+ chord.shift = true;
4171
+ break;
4172
+ default: throw new TypeError(`hotkeys: "${part}" in "${keys}" is not a modifier`);
4173
+ }
4174
+ return chord;
4175
+ };
4176
+ const matches = (chord, event) => event.key.toLowerCase() === chord.key && event.ctrlKey === chord.ctrl && event.metaKey === chord.meta && event.altKey === chord.alt && event.shiftKey === chord.shift;
4177
+ const editable = (target) => target instanceof HTMLElement && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable);
4178
+ const inScope = (scope, target) => scope === void 0 || target instanceof Node && scope.contains(target);
4179
+ /**
4180
+ * One `keydown` listener in the capture phase, one list of chords. A chord
4181
+ * fires when its modifiers match exactly, its `when` holds, its scope contains
4182
+ * the event's target, and the focus is not in a text field (unless
4183
+ * `allowInEditable`). The most recently bound match is the only one that fires,
4184
+ * so a modal's `"/"` overrides the page's and unbinding it brings the page's
4185
+ * back; `preventDefault` is on by default for the bindings whose chord the
4186
+ * browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
4187
+ */
4188
+ const createHotkeys = (options = {}) => {
4189
+ const isApple = options.isApple ?? detectsApple();
4190
+ const bindings = [];
4191
+ let listening = false;
4192
+ let disposed = false;
4193
+ const onKeyDown = (event) => {
4194
+ for (let i = bindings.length - 1; i >= 0; i--) {
4195
+ const entry = bindings[i];
4196
+ if (entry === void 0) continue;
4197
+ const { chord, binding } = entry;
4198
+ if (!matches(chord, event) || binding.when?.() === false) continue;
4199
+ if (!inScope(binding.scope, event.target)) continue;
4200
+ if (binding.allowInEditable !== true && editable(event.target)) continue;
4201
+ if (binding.preventDefault !== false) event.preventDefault();
4202
+ binding.onTrigger(event);
4203
+ return;
4204
+ }
4205
+ };
4206
+ const stop = () => {
4207
+ if (!listening) return;
4208
+ listening = false;
4209
+ document.removeEventListener("keydown", onKeyDown, true);
4210
+ };
4211
+ return {
4212
+ bind: (binding) => {
4213
+ if (disposed) return () => {};
4214
+ const entry = {
4215
+ chord: parse(binding.keys, isApple),
4216
+ binding
4217
+ };
4218
+ bindings.push(entry);
4219
+ if (!listening) {
4220
+ listening = true;
4221
+ document.addEventListener("keydown", onKeyDown, true);
4222
+ }
4223
+ return () => {
4224
+ const at = bindings.indexOf(entry);
4225
+ if (at === -1) return;
4226
+ bindings.splice(at, 1);
4227
+ if (bindings.length === 0) stop();
4228
+ };
4229
+ },
4230
+ dispose: () => {
4231
+ disposed = true;
4232
+ bindings.length = 0;
4233
+ stop();
4234
+ }
4235
+ };
4236
+ };
3836
4237
  // src/link.ts
3837
4238
  /**
3838
4239
  * APG Link (https://www.w3.org/WAI/ARIA/apg/patterns/link/): a native `<a href>`
@@ -5449,6 +5850,69 @@ const createResizablePanels = (options) => {
5449
5850
  separatorProps
5450
5851
  };
5451
5852
  };
5853
+ // src/scrollspy.ts
5854
+ /**
5855
+ * A scrollspy: a table of contents whose current entry is the last heading that
5856
+ * has reached the top of the scrollport, marked `aria-current="location"` on
5857
+ * its link (and nowhere else: exactly one link is current).
5858
+ *
5859
+ * **No APG pattern exists for a scrollspy**, so there is no Quint spec and no
5860
+ * ported scenarios: the contract is WAI-ARIA (`aria-current="location"`, the
5861
+ * `list` / `listitem` roles, the navigation landmark and its name) and HTML,
5862
+ * and `test/scrollspy.browser.test.tsx` is its oracle.
5863
+ *
5864
+ * **A scroll listener, not an `IntersectionObserver`.** "The last heading at or
5865
+ * above the offset" is a total order over every heading, while an observer only
5866
+ * reports what crossed its band: a band observer has to rebuild the order from
5867
+ * its enter/leave state, and it misreports after a jump (an anchor click, a
5868
+ * restored scroll position). The listener is passive, the measurements are
5869
+ * reads only (no writes, so no layout thrash), and a docs page has a dozen
5870
+ * headings — the same cost as the listener the page already keeps.
5871
+ *
5872
+ * Smooth scrolling is the host's (`scroll-behavior: smooth`): the links are
5873
+ * plain `#id` targets and nothing here moves the scrollport, so no
5874
+ * `prefers-reduced-motion` branch is needed either.
5875
+ */
5876
+ const createScrollspy = (options) => {
5877
+ const offset = options.offset ?? 0;
5878
+ const first = options.entries[0]?.id;
5879
+ const [active, setActive] = createSignal(first);
5880
+ const at = () => {
5881
+ const container = options.root?.()?.getBoundingClientRect().top ?? 0;
5882
+ let id = first;
5883
+ for (const entry of options.entries) {
5884
+ const element = document.getElementById(entry.id);
5885
+ if (element === null) continue;
5886
+ if (element.getBoundingClientRect().top - container <= offset) id = entry.id;
5887
+ }
5888
+ return id;
5889
+ };
5890
+ const commit = () => {
5891
+ const next = at();
5892
+ if (next === active()) return;
5893
+ setActive(next);
5894
+ options.onChange?.(next);
5895
+ };
5896
+ return {
5897
+ active,
5898
+ navProps: { "aria-label": "On this page" },
5899
+ listProps: { role: "list" },
5900
+ itemProps: () => ({ role: "listitem" }),
5901
+ linkProps: (id) => ({
5902
+ href: `#${id}`,
5903
+ get "aria-current"() {
5904
+ return id === active() ? "location" : void 0;
5905
+ }
5906
+ }),
5907
+ attach: () => {
5908
+ const target = options.root?.() ?? document;
5909
+ const listener = () => commit();
5910
+ target.addEventListener("scroll", listener, { passive: true });
5911
+ commit();
5912
+ return () => target.removeEventListener("scroll", listener);
5913
+ }
5914
+ };
5915
+ };
5452
5916
  // src/slider.ts
5453
5917
  const valueTextFor$1 = (value) => value <= 3 ? "low" : value <= 7 ? "medium" : "high";
5454
5918
  /**
@@ -8361,4 +8825,4 @@ const createRearrangeableListbox = (options) => {
8361
8825
  })
8362
8826
  };
8363
8827
  };
8364
- export { ConfirmPendingError, DEFAULT_PREFS, DRAG_PX, GRID, NO_POS, SNAP_PX, TONE, TREEVIEW_NODES, TWEEN_TICKS, WORLD_BOUND, YES_POS, ZMAX, ZMIN, ZONE, cellPart, createAccordion, createAlert, createAlertDialog, createBreadcrumb, createButton, createCanvas, createCarousel, createCheckbox, createCheckboxGroup, createCombobox, createConfirm, createDialogModal, createDisclosure, createDrawer, createFeed, createField, createGrid, createLink, createListbox, createMenu, createMenubar, createMeter, createPagination, createPlayer, createPopover, createRadioGroup, createRearrangeableListbox, createResizablePanels, createSlider, createSliderMultiThumb, createSpinbutton, createStatus, createSwitch, createTable, createTabs, createTextArea, createTextInput, createTimeline, createToast, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
8828
+ export { ConfirmPendingError, DEFAULT_PREFS, DRAG_PX, GRID, NO_POS, SNAP_PX, TONE, TREEVIEW_NODES, TWEEN_TICKS, WORLD_BOUND, YES_POS, ZMAX, ZMIN, ZONE, cellPart, createAccordion, createAlert, createAlertDialog, createBreadcrumb, createButton, createCanvas, createCarousel, createCheckbox, createCheckboxGroup, createCombobox, createConfirm, createDialogModal, createDisclosure, createDrawer, createFeed, createField, createGrid, createHotkeys, createLink, createListbox, createMenu, createMenubar, createMeter, createPagination, createPanZoom, createPlayer, createPopover, createRadioGroup, createRearrangeableListbox, createResizablePanels, createScrollspy, createSlider, createSliderMultiThumb, createSpinbutton, createStatus, createSwitch, createTable, createTabs, createTextArea, createTextInput, createTimeline, createToast, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cssints/headless",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Headless APG widgets on Solid 2: state plus attribute objects, no elements, no styling, each conformance-checked against its Quint spec",
5
5
  "license": "MIT",
6
6
  "repository": {