@cssints/headless 0.17.0 → 0.19.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
@@ -1497,6 +1497,106 @@ export interface Drawer {
1497
1497
  * dismissal a non-modal drawer has); closing returns focus to the trigger.
1498
1498
  */
1499
1499
  export declare const createDrawer: (options?: DrawerOptions) => Drawer;
1500
+ /** Why a file was refused: the `accept` grammar, or `maxSize`. */
1501
+ export type DropZoneRejectReason = "type" | "size";
1502
+ export interface DropZoneReject {
1503
+ readonly file: File;
1504
+ readonly reason: DropZoneRejectReason;
1505
+ }
1506
+ export interface DropZoneLabels {
1507
+ /** The zone's accessible name (`aria-label` on the `role=group` zone). */
1508
+ readonly zone?: string | undefined;
1509
+ /** The browse button's accessible name; render it visibly, or use it for an icon-only button. */
1510
+ readonly browse?: string | undefined;
1511
+ }
1512
+ export interface DropZoneOptions {
1513
+ /** The HTML `accept` grammar: extensions (`.png`) and MIME types, wildcards included (`image/*`). */
1514
+ readonly accept?: string | undefined;
1515
+ readonly multiple?: boolean | undefined;
1516
+ /** Bytes; a larger file is rejected with `"size"`. */
1517
+ readonly maxSize?: number | undefined;
1518
+ readonly disabled?: boolean | undefined;
1519
+ /** The files that passed, one call per batch; not called when the batch has none. */
1520
+ readonly onFiles: (files: File[]) => void;
1521
+ /** Each refused file, once, with the reason the host shows. */
1522
+ readonly onReject?: ((reject: DropZoneReject) => void) | undefined;
1523
+ readonly labels?: DropZoneLabels | undefined;
1524
+ }
1525
+ /** Spread onto the hidden `<input type=file>`; render it, always. */
1526
+ export interface DropZoneInputProps {
1527
+ /** The id the browse button clicks; generated. */
1528
+ readonly id: string;
1529
+ readonly type: "file";
1530
+ readonly accept: string | undefined;
1531
+ readonly multiple: true | undefined;
1532
+ readonly disabled: true | undefined;
1533
+ readonly hidden: true;
1534
+ readonly onChange: (event: {
1535
+ currentTarget: HTMLInputElement;
1536
+ }) => void;
1537
+ }
1538
+ /** Spread onto the drop target (any element). `dragging()` is the styling hook. */
1539
+ export interface DropZoneZoneProps {
1540
+ readonly role: "group";
1541
+ readonly "aria-label": string | undefined;
1542
+ readonly "aria-disabled": "true" | undefined;
1543
+ readonly onDragEnter: (event: DragEvent) => void;
1544
+ readonly onDragOver: (event: DragEvent) => void;
1545
+ readonly onDragLeave: (event: DragEvent) => void;
1546
+ readonly onDrop: (event: DragEvent) => void;
1547
+ }
1548
+ /** Spread onto a real `<button type="button">`: Enter and Space are the platform's. */
1549
+ export interface DropZoneBrowseProps {
1550
+ readonly type: "button";
1551
+ readonly disabled: true | undefined;
1552
+ readonly "aria-disabled": "true" | undefined;
1553
+ readonly "aria-label": string | undefined;
1554
+ readonly onClick: () => void;
1555
+ }
1556
+ /** Spread onto the live region; render it once, always. */
1557
+ export interface DropZoneStatusProps {
1558
+ readonly role: "status";
1559
+ readonly "aria-live": "polite";
1560
+ readonly "aria-atomic": "true";
1561
+ }
1562
+ export interface DropZone {
1563
+ /** A drag is over the zone; `dragleave` on a child does not clear it. */
1564
+ readonly dragging: Accessor<boolean>;
1565
+ /**
1566
+ * A default sentence for the last batch ("2 files, 1 rejected: too large"),
1567
+ * or `undefined` before one arrived. The host may write its own words from
1568
+ * the same counts.
1569
+ */
1570
+ readonly message: Accessor<string | undefined>;
1571
+ readonly inputProps: DropZoneInputProps;
1572
+ readonly zoneProps: DropZoneZoneProps;
1573
+ readonly browseProps: DropZoneBrowseProps;
1574
+ readonly statusProps: DropZoneStatusProps;
1575
+ }
1576
+ /**
1577
+ * A file drop zone. There is no APG pattern and no Quint spec for one: the
1578
+ * contract is HTML's file input plus WAI-ARIA's live region, and
1579
+ * `test/dropzone.browser.test.tsx` is its oracle.
1580
+ *
1581
+ * Decisions:
1582
+ * - **The zone is never drag-only.** `browseProps` spreads onto a real
1583
+ * `<button type="button">` that clicks the hidden input, so the picker is the
1584
+ * keyboard path (Enter and Space are the platform's) and a pointer user can
1585
+ * click as well as drag. The drop handlers add nothing the button cannot do.
1586
+ * - **Validation happens once per file**, in the order type then size, and a
1587
+ * refused file reaches `onReject` with the reason the host shows. `accept` is
1588
+ * the HTML grammar (`.` extension, MIME type, `*`); `maxSize` is bytes.
1589
+ * - **Dropping a folder or a `DataTransfer` with no files is a no-op**: no
1590
+ * `onFiles`, no `onReject`, no message, and the previous announcement stays.
1591
+ * - **`dragleave` counts.** A child's `dragleave` bubbles to the zone, so the
1592
+ * state is a depth counter: it clears only when the drag leaves the zone
1593
+ * itself, or on a drop.
1594
+ * - **The zone is `role=group`** so `labels.zone` is exposed; `dragging()` is
1595
+ * the only drag state a skin reads (no `data-dragging`).
1596
+ * - `disabled` marks the zone `aria-disabled`, disables the input natively and
1597
+ * the browse button, and drops every drag before it becomes a batch.
1598
+ */
1599
+ export declare const createDropZone: (options: DropZoneOptions) => DropZone;
1500
1600
  /**
1501
1601
  * Options are read lazily (getters); pass a component's `props`.
1502
1602
  */
@@ -1757,6 +1857,128 @@ export interface Pagination {
1757
1857
  * Target size is the skin's job: 24x24 CSS px at AA, 44x44 at AAA.
1758
1858
  */
1759
1859
  export declare const createPagination: (options: PaginationOptions) => Pagination;
1860
+ /** A point in the viewport's own coordinate space: `0, 0` is its top-left corner. */
1861
+ export interface PanZoomPoint {
1862
+ readonly x: number;
1863
+ readonly y: number;
1864
+ }
1865
+ export interface PanZoomOptions {
1866
+ /** Smallest scale, and where `reset` returns to. Default 1. */
1867
+ readonly min?: number | undefined;
1868
+ /** Largest scale. Default 4. */
1869
+ readonly max?: number | undefined;
1870
+ /** The `+` / `-` / `zoomIn` / `zoomOut` increment. Default 0.25. */
1871
+ readonly step?: number | undefined;
1872
+ /** Starting scale when uncontrolled; ignored while `scale` is set. */
1873
+ readonly defaultScale?: number | undefined;
1874
+ /** Controlled: the host owns the scale and this is the only thing that changes it. */
1875
+ readonly scale?: number | undefined;
1876
+ /**
1877
+ * A request to change the scale, called when the intent differs from the
1878
+ * current value. Uncontrolled it is accepted; controlled the host may ignore it.
1879
+ */
1880
+ readonly onScaleChange?: (scale: number) => void;
1881
+ /** The pan changed (a drag, a pinch, an arrow key, a zoom about a point, a reset). */
1882
+ readonly onPanChange?: (offset: PanZoomPoint) => void;
1883
+ /**
1884
+ * `"clamp"` (default) keeps the scaled image covering the viewport: the offset
1885
+ * never opens a gap. `"free"` lets it be dragged out of view.
1886
+ */
1887
+ readonly bounds?: "clamp" | "free" | undefined;
1888
+ readonly labels?: {
1889
+ readonly viewport?: string;
1890
+ } | undefined;
1891
+ }
1892
+ export interface PanZoomViewportProps {
1893
+ readonly tabindex: number;
1894
+ readonly role: "group";
1895
+ readonly "aria-roledescription": string;
1896
+ readonly "aria-label": string;
1897
+ readonly "aria-describedby": string;
1898
+ readonly onKeyDown: (event: KeyboardEvent) => void;
1899
+ readonly onWheel: (event: WheelEvent) => void;
1900
+ readonly onPointerDown: (event: PointerEvent) => void;
1901
+ readonly onPointerMove: (event: PointerEvent) => void;
1902
+ readonly onPointerUp: (event: PointerEvent) => void;
1903
+ readonly onPointerCancel: (event: PointerEvent) => void;
1904
+ readonly onDblClick: (event: MouseEvent) => void;
1905
+ }
1906
+ /**
1907
+ * Spread onto the image. `style` is a getter returning a fresh object, so Solid
1908
+ * reads it inside the tracking scope; `draggable: false` keeps the native image
1909
+ * drag from swallowing the pan gesture.
1910
+ */
1911
+ export interface PanZoomImageProps {
1912
+ readonly draggable: false;
1913
+ readonly style: {
1914
+ readonly transform: string;
1915
+ };
1916
+ }
1917
+ /** The live value: the current percentage, in a polite status region (`aria-describedby` of the viewport). */
1918
+ export interface PanZoomValueProps {
1919
+ readonly id: string;
1920
+ readonly role: "status";
1921
+ readonly "aria-live": "polite";
1922
+ readonly "aria-atomic": "true";
1923
+ }
1924
+ export interface PanZoom {
1925
+ readonly scale: Accessor<number>;
1926
+ readonly offset: Accessor<PanZoomPoint>;
1927
+ /** Back to `min` and centred. */
1928
+ readonly reset: () => void;
1929
+ readonly zoomIn: () => void;
1930
+ readonly zoomOut: () => void;
1931
+ /** A request; `origin` is the viewport point to keep still (its centre by default). */
1932
+ readonly setScale: (scale: number, origin?: PanZoomPoint) => void;
1933
+ readonly viewportProps: PanZoomViewportProps;
1934
+ readonly imageProps: PanZoomImageProps;
1935
+ readonly valueProps: PanZoomValueProps;
1936
+ /** The human value of `valueProps`: `"150%"`. */
1937
+ readonly value: Accessor<string>;
1938
+ }
1939
+ /**
1940
+ * Pan and zoom for one image in a viewport: scale, offset and the prop objects
1941
+ * for a focusable viewport, the transformed image and a live value.
1942
+ *
1943
+ * **There is no APG pattern for pan-zoom.** The accessible contract is the three
1944
+ * parts this returns: a viewport that takes focus (`role=group`, `tabindex=0`,
1945
+ * `aria-roledescription="zoomable image"`), keyboard operations that stand in
1946
+ * for every pointer gesture (`+` / `=` / `-` zoom, arrows pan, `0` resets), and a
1947
+ * polite live region (`aria-describedby` of the viewport) that carries the
1948
+ * current scale. The pointer gestures are an addition for mouse and touch, never
1949
+ * the only way in. That contract is this module's own: the case it replaces
1950
+ * (kocharin-art-3's `Zoomable`, ~110 lines of pointer maths) had no keyboard path
1951
+ * and no ARIA at all, and `createCanvas` is the wrong model here — an infinite
1952
+ * canvas moves a camera over a world, while this is one image fitted to its
1953
+ * viewport.
1954
+ *
1955
+ * Wheel is zoom-with-a-modifier only: `ctrl` / `meta` (the browser's own pinch
1956
+ * arrives as `ctrl+wheel`) zooms about the cursor; a bare wheel is never
1957
+ * `preventDefault`ed, so the page keeps scrolling. Trackpad gestures differ per
1958
+ * platform and a swallowed wheel is the one thing that is never acceptable.
1959
+ *
1960
+ * Pointer: a drag pans, a second pointer pinches (about its midpoint), a double
1961
+ * click resets. Capture is best-effort (a synthetic pointer cannot be captured).
1962
+ *
1963
+ * `bounds: "clamp"` bounds the offset by half the viewport's scaled overflow, so
1964
+ * the image is expected to fill the viewport at `min` (the usual
1965
+ * `width: 100%; height: 100%` / `object-fit: cover` box). A letterboxed image
1966
+ * smaller than its viewport cannot be held gap-free; give it a viewport of its
1967
+ * own or `bounds: "free"`.
1968
+ *
1969
+ * Request/commit (`rt1p`): uncontrolled by default (`defaultScale`); pass `scale`
1970
+ * + `onScaleChange` to own the value. The offset has no controlled prop, so a
1971
+ * hosting page that rejects a scale request also gets the offset resolved for the
1972
+ * scale it refused — accept the request, or pass `bounds: "free"` and own the
1973
+ * pan elsewhere.
1974
+ *
1975
+ * The host owns the box and the looks: size the viewport, put `touch-action:
1976
+ * none` on it if a touch drag should pan, centre the image
1977
+ * (`transform-origin: 50% 50%` is the default this maths assumes), and hide the
1978
+ * live region visually rather than with `hidden` (a `hidden` live region is not
1979
+ * announced).
1980
+ */
1981
+ export declare const createPanZoom: (options?: PanZoomOptions) => PanZoom;
1760
1982
  export type PopoverPlacement = "bottom-start" | "bottom" | "bottom-end" | "top-start" | "top" | "top-end";
1761
1983
  /** A rect in one space, as `getBoundingClientRect` gives it. */
1762
1984
  export interface PopoverRect {
@@ -2027,6 +2249,53 @@ export declare const packSort: (column: number, dir: GridSortDirection) => numbe
2027
2249
  * snapshot); `rows`/`cols`/`page` are captured once.
2028
2250
  */
2029
2251
  export declare const createGrid: (options?: GridOptions) => Grid;
2252
+ /**
2253
+ * Hotkeys: one registry for the chords every app hand-rolls (the docs app's `/`
2254
+ * and Cmd/Ctrl+K, the CMS's Cmd/Ctrl+S), instead of a `keydown` listener per
2255
+ * screen.
2256
+ *
2257
+ * This is a platform utility, not a widget: there is no APG pattern for a
2258
+ * keyboard registry, so no role, no element, no styling, no Quint spec and no
2259
+ * ported scenario. The oracle is `test/hotkeys.browser.test.tsx` in a real
2260
+ * Chromium.
2261
+ */
2262
+ export interface HotkeyBinding {
2263
+ /**
2264
+ * A chord: the modifiers `mod` (Cmd on macOS, Ctrl elsewhere), `ctrl`, `alt`,
2265
+ * `shift`, `meta`, then exactly one key (`k`, `/`, `?`, `escape`, `enter`,
2266
+ * `arrowup`), joined with `+`; `"mod+k"`, `"shift+?"`, `"/"`.
2267
+ */
2268
+ readonly keys: string;
2269
+ readonly onTrigger: (event: KeyboardEvent) => void;
2270
+ /** Gates a binding whose precondition is off: it falls through to the next one. */
2271
+ readonly when?: (() => boolean) | undefined;
2272
+ /** `event.preventDefault()` when the binding fires; default true. */
2273
+ readonly preventDefault?: boolean | undefined;
2274
+ /** Fire while the focus is in an input, a textarea or a contenteditable; default false. */
2275
+ readonly allowInEditable?: boolean | undefined;
2276
+ /** Only events from inside this element: one panel's `"mod+s"` over a page's. */
2277
+ readonly scope?: HTMLElement | undefined;
2278
+ }
2279
+ export interface HotkeysOptions {
2280
+ /** Whether `mod` is Cmd; injected so a test does not read the runner's OS. */
2281
+ readonly isApple?: boolean | undefined;
2282
+ }
2283
+ export interface Hotkeys {
2284
+ /** Registers `binding`; the returned unbind is idempotent. The last bound wins. */
2285
+ readonly bind: (binding: HotkeyBinding) => () => void;
2286
+ /** Unbinds all of them and removes the listener; a later `bind` is a no-op. */
2287
+ readonly dispose: () => void;
2288
+ }
2289
+ /**
2290
+ * One `keydown` listener in the capture phase, one list of chords. A chord
2291
+ * fires when its modifiers match exactly, its `when` holds, its scope contains
2292
+ * the event's target, and the focus is not in a text field (unless
2293
+ * `allowInEditable`). The most recently bound match is the only one that fires,
2294
+ * so a modal's `"/"` overrides the page's and unbinding it brings the page's
2295
+ * back; `preventDefault` is on by default for the bindings whose chord the
2296
+ * browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
2297
+ */
2298
+ export declare const createHotkeys: (options?: HotkeysOptions) => Hotkeys;
2030
2299
  export interface LinkOptions {
2031
2300
  readonly href?: string | undefined;
2032
2301
  /** Render a native `<a href>` (the default when `href` is set). */
@@ -2601,6 +2870,76 @@ export interface ResizablePanels {
2601
2870
  * DOM is checked against it by the differential walk.
2602
2871
  */
2603
2872
  export declare const createResizablePanels: (options: ResizablePanelsOptions) => ResizablePanels;
2873
+ export interface ScrollspyEntry {
2874
+ readonly id: string;
2875
+ readonly label: string;
2876
+ }
2877
+ export interface ScrollspyOptions {
2878
+ /** The table of contents, in document order. The heading elements carry these ids. */
2879
+ readonly entries: ReadonlyArray<ScrollspyEntry>;
2880
+ /**
2881
+ * How far below the scrollport's top edge a heading counts as current, in px.
2882
+ * A host with a sticky header passes its height. @default 0
2883
+ */
2884
+ readonly offset?: number | undefined;
2885
+ /**
2886
+ * The scroll container; the document by default. Read once, when `attach`
2887
+ * runs, so a host that swaps containers attaches again.
2888
+ */
2889
+ readonly root?: (() => HTMLElement | undefined) | undefined;
2890
+ /** The active id changed. */
2891
+ readonly onChange?: ((id: string | undefined) => void) | undefined;
2892
+ }
2893
+ export interface ScrollspyNavProps {
2894
+ /** The navigation landmark's name; the host overrides it after the spread to translate it. */
2895
+ readonly "aria-label": string;
2896
+ }
2897
+ export interface ScrollspyListProps {
2898
+ readonly role: "list";
2899
+ }
2900
+ export interface ScrollspyItemProps {
2901
+ readonly role: "listitem";
2902
+ }
2903
+ export interface ScrollspyLinkProps {
2904
+ readonly href: string;
2905
+ readonly "aria-current": "location" | undefined;
2906
+ }
2907
+ export interface Scrollspy {
2908
+ /** The current entry's id; `undefined` only when there are no entries. */
2909
+ readonly active: Accessor<string | undefined>;
2910
+ readonly navProps: ScrollspyNavProps;
2911
+ readonly listProps: ScrollspyListProps;
2912
+ readonly itemProps: (id: string) => ScrollspyItemProps;
2913
+ readonly linkProps: (id: string) => ScrollspyLinkProps;
2914
+ /**
2915
+ * DOM wiring: one passive `scroll` listener on the root, which measures the
2916
+ * headings. Call it once mounted and call the teardown it returns on cleanup.
2917
+ */
2918
+ readonly attach: () => () => void;
2919
+ }
2920
+ /**
2921
+ * A scrollspy: a table of contents whose current entry is the last heading that
2922
+ * has reached the top of the scrollport, marked `aria-current="location"` on
2923
+ * its link (and nowhere else: exactly one link is current).
2924
+ *
2925
+ * **No APG pattern exists for a scrollspy**, so there is no Quint spec and no
2926
+ * ported scenarios: the contract is WAI-ARIA (`aria-current="location"`, the
2927
+ * `list` / `listitem` roles, the navigation landmark and its name) and HTML,
2928
+ * and `test/scrollspy.browser.test.tsx` is its oracle.
2929
+ *
2930
+ * **A scroll listener, not an `IntersectionObserver`.** "The last heading at or
2931
+ * above the offset" is a total order over every heading, while an observer only
2932
+ * reports what crossed its band: a band observer has to rebuild the order from
2933
+ * its enter/leave state, and it misreports after a jump (an anchor click, a
2934
+ * restored scroll position). The listener is passive, the measurements are
2935
+ * reads only (no writes, so no layout thrash), and a docs page has a dozen
2936
+ * headings — the same cost as the listener the page already keeps.
2937
+ *
2938
+ * Smooth scrolling is the host's (`scroll-behavior: smooth`): the links are
2939
+ * plain `#id` targets and nothing here moves the scrollport, so no
2940
+ * `prefers-reduced-motion` branch is needed either.
2941
+ */
2942
+ export declare const createScrollspy: (options: ScrollspyOptions) => Scrollspy;
2604
2943
  export type SliderOrientation = "horizontal" | "vertical";
2605
2944
  export type SliderDirection = "ltr" | "rtl";
2606
2945
  export interface SliderOptions {
@@ -2615,6 +2954,11 @@ export interface SliderOptions {
2615
2954
  readonly defaultValue?: number;
2616
2955
  readonly orientation?: SliderOrientation;
2617
2956
  readonly direction?: SliderDirection;
2957
+ /**
2958
+ * aria-disabled: the thumb stays the tab stop (the APG pattern's focus
2959
+ * contract) but a key or a rail press changes nothing.
2960
+ */
2961
+ readonly disabled?: boolean;
2618
2962
  /** Base for the generated thumb id. */
2619
2963
  readonly id?: string;
2620
2964
  /**
@@ -2631,6 +2975,7 @@ export interface SliderThumbProps {
2631
2975
  readonly "aria-valuemax": string;
2632
2976
  readonly "aria-valuetext": string;
2633
2977
  readonly "aria-orientation": SliderOrientation;
2978
+ readonly "aria-disabled": "true" | undefined;
2634
2979
  readonly tabindex: number;
2635
2980
  readonly onFocus: () => void;
2636
2981
  readonly onBlur: () => void;
@@ -2668,6 +3013,11 @@ export interface Slider {
2668
3013
  * pass `value` + `onValueChange` to own the value. A key or rail press is a
2669
3014
  * request the host may ignore.
2670
3015
  *
3016
+ * `disabled` is a widget-level extension of the pattern (the spec lists it
3017
+ * under its deliberate omissions): the thumb keeps its tab stop and announces
3018
+ * `aria-disabled`, and every key and rail press is dropped before it becomes a
3019
+ * request.
3020
+ *
2671
3021
  * ponytail: dragging (pointer capture/move) is not modelled — the spec
2672
3022
  * deliberately only fixes a discrete rail press.
2673
3023
  */
@@ -2752,6 +3102,11 @@ export interface SpinbuttonOptions {
2752
3102
  readonly defaultValue?: number;
2753
3103
  /** Wrap around at the bounds instead of clamping. */
2754
3104
  readonly wrap?: boolean;
3105
+ /**
3106
+ * aria-disabled plus the native `disabled` on the text field: keys, typing
3107
+ * and both steppers change nothing, and both steppers announce `aria-disabled`.
3108
+ */
3109
+ readonly disabled?: boolean;
2755
3110
  /** Base for the generated input / button ids. */
2756
3111
  readonly id?: string;
2757
3112
  /**
@@ -2768,6 +3123,8 @@ export interface SpinbuttonInputProps {
2768
3123
  readonly "aria-valuemax": string;
2769
3124
  readonly "aria-valuetext": string;
2770
3125
  readonly "aria-invalid": "true" | undefined;
3126
+ readonly "aria-disabled": "true" | undefined;
3127
+ readonly disabled: boolean;
2771
3128
  readonly tabindex: number;
2772
3129
  readonly value: string;
2773
3130
  readonly onFocus: () => void;
@@ -2823,6 +3180,12 @@ export interface Spinbutton {
2823
3180
  * stepper buttons, Enter and blur are requests the host may ignore; typing and
2824
3181
  * Escape change no committed value.
2825
3182
  *
3183
+ * `disabled` is a widget-level extension of the pattern (the spec models the
3184
+ * steppers' bound only, and lists this under its deliberate omissions): the
3185
+ * field announces `aria-disabled` and carries the native `disabled`, every key,
3186
+ * typed draft and both steppers are dropped before they become a request, and
3187
+ * neither stepper offers itself.
3188
+ *
2826
3189
  * ponytail: commit/revert on Enter/blur/Escape is the common convention, not
2827
3190
  * APG text.
2828
3191
  */
@@ -3194,6 +3557,26 @@ export interface TabsOptions {
3194
3557
  * current value. Uncontrolled it is accepted; controlled the host may ignore it.
3195
3558
  */
3196
3559
  readonly onValueChange?: (index: number) => void;
3560
+ /**
3561
+ * Close buttons and the Delete key (default false): the host renders
3562
+ * `closeProps` and removes the tab from `onClose`. The last tab is never
3563
+ * asked to close.
3564
+ */
3565
+ readonly closable?: boolean | undefined;
3566
+ /**
3567
+ * Inline rename (default false): a double click on a tab starts it, Enter /
3568
+ * blur commit and Escape reverts; the host renames from `onRename`.
3569
+ */
3570
+ readonly renamable?: boolean | undefined;
3571
+ /**
3572
+ * A tab's label: the close / rename button names and the starting draft of a
3573
+ * rename, read lazily (a getter is fine) so a rename follows.
3574
+ */
3575
+ readonly label?: ((index: number) => string | undefined) | undefined;
3576
+ /** A request to remove a tab; the primitive does not own the list. */
3577
+ readonly onClose?: ((index: number) => void) | undefined;
3578
+ /** A request to rename a tab with a non-empty label. */
3579
+ readonly onRename?: ((index: number, label: string) => void) | undefined;
3197
3580
  }
3198
3581
  /** Spread onto the element with `role=tab` (a native `<button>`). */
3199
3582
  export interface TabProps {
@@ -3221,21 +3604,63 @@ export interface TabpanelProps {
3221
3604
  readonly tabindex: 0 | undefined;
3222
3605
  readonly hidden: boolean;
3223
3606
  }
3607
+ /**
3608
+ * Spread onto the tab's close button. `tabindex=-1` keeps it out of the
3609
+ * tablist's tab order (a tablist's stops are its tabs); the Delete key is the
3610
+ * keyboard path.
3611
+ */
3612
+ export interface TabCloseProps {
3613
+ readonly "aria-label": string;
3614
+ readonly tabindex: -1;
3615
+ readonly onClick: () => void;
3616
+ }
3617
+ /** Spread onto the inline `<input>` a renaming tab renders instead of its label. */
3618
+ export interface TabRenameInputProps {
3619
+ readonly type: "text";
3620
+ readonly value: string;
3621
+ readonly "aria-label": string;
3622
+ readonly onInput: (event: InputEvent) => void;
3623
+ readonly onKeyDown: (event: KeyboardEvent) => void;
3624
+ readonly onBlur: () => void;
3625
+ }
3626
+ /** The rename part of one tab: put `onDblClick` on the tab, `inputProps` in it. */
3627
+ export interface TabRenameProps {
3628
+ readonly onDblClick: () => void;
3629
+ /** Whether this tab is the one being renamed. */
3630
+ readonly editing: Accessor<boolean>;
3631
+ /** Start renaming `index` (e.g. a tab the host has just added). */
3632
+ readonly begin: (index: number) => void;
3633
+ /** Commit `label` to the tab being edited; an empty label cancels. */
3634
+ readonly commit: (label: string) => void;
3635
+ readonly cancel: () => void;
3636
+ readonly inputProps: TabRenameInputProps;
3637
+ }
3224
3638
  export interface Tabs {
3639
+ /** The tab count; read from `items` on every read, so a host that shrinks its list stays in step. */
3225
3640
  readonly items: number;
3226
3641
  readonly selected: Accessor<number>;
3227
3642
  /** Tab holding (or last holding) DOM focus; -1 when focus is outside. */
3228
3643
  readonly focused: Accessor<number>;
3644
+ /** The tab being renamed inline, `null` when none. */
3645
+ readonly editing: Accessor<number | null>;
3229
3646
  /** Programmatic selection request; ignores disabled tabs. */
3230
3647
  readonly select: (index: number) => void;
3231
3648
  /** Programmatic selection request (may point at a disabled tab, like base-ui). */
3232
3649
  readonly setSelected: (index: number) => void;
3233
3650
  /** Programmatic focus (does not select). */
3234
3651
  readonly focusTab: (index: number) => void;
3652
+ /**
3653
+ * The tab that takes focus once `onClose(index)`'s removal lands: the tab
3654
+ * that slid into `index`, or the previous one when the last tab closed. Call
3655
+ * it before the host removes the tab.
3656
+ */
3657
+ readonly focusAfterClose: (index: number) => number;
3235
3658
  readonly keyDown: (key: string) => void;
3236
3659
  readonly tablistProps: TablistProps;
3237
3660
  readonly tabProps: (index: number) => TabProps;
3238
3661
  readonly tabpanelProps: (index: number) => TabpanelProps;
3662
+ readonly closeProps: (index: number) => TabCloseProps;
3663
+ readonly renameProps: (index: number) => TabRenameProps;
3239
3664
  }
3240
3665
  /**
3241
3666
  * APG Tabs (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): state plus prop
@@ -3250,6 +3675,14 @@ export interface Tabs {
3250
3675
  * Request/commit (map ticket `rt1p`): uncontrolled by default
3251
3676
  * (`defaultSelected`); pass `value` + `onValueChange` to own the selection.
3252
3677
  * Programmatic methods (`select`, `setSelected`, `keyDown`) are requests too.
3678
+ *
3679
+ * Close and rename are an **extension** (bean `cssints-gdv2`): the APG spec
3680
+ * models a `deletable` flavour but neither a callback nor a rename, and this
3681
+ * primitive owns no list. `closable` / `renamable` turn on the props; `onClose`
3682
+ * / `onRename` ask the host, which removes or renames. Focus after a close goes
3683
+ * to the tab that slid into the closed one's place, or to the previous tab when
3684
+ * the last one closed; closing the last remaining tab is refused, as the spec
3685
+ * refuses an empty tablist.
3253
3686
  */
3254
3687
  export declare const createTabs: (options?: TabsOptions) => Tabs;
3255
3688
  /**
@@ -3267,10 +3700,16 @@ export declare const createTabs: (options?: TabsOptions) => Tabs;
3267
3700
  * may ignore. The zoom window (`viewStart`/`viewEnd`) is not the controlled
3268
3701
  * value and stays uncontrolled.
3269
3702
  *
3270
- * ponytail: the fixed 5-span tree is a demo fixture; a real consumer owns the
3271
- * span list (layout is theirs). Roving tabindex only — a consumer must call
3272
- * `focus()` on the row whose `tabindex` is 0 after arrow navigation (the tests'
3273
- * fixture does this with an effect).
3703
+ * The fixed 5-span tree is the demo fixture (the differential walk's oracle).
3704
+ * A real consumer passes `rows`: span data on one shared axis, laid out by
3705
+ * `barProps` as percentages of the rows' extent. No roles are borrowed from
3706
+ * APG: a trace waterfall is a list of spans in time, so the rows mode is
3707
+ * `role="list"` / `role="listitem"` (with `aria-level` for the depth) and the
3708
+ * current row is `aria-current="true"`, which every element may carry.
3709
+ *
3710
+ * Roving tabindex only — a consumer must call `focus()` on the row whose
3711
+ * `tabindex` is 0 after arrow navigation (the tests' fixture does this with an
3712
+ * effect; the rows mode's span rows are reached by tabbing to the current one).
3274
3713
  */
3275
3714
  /** One span row: a fraction of the parent span on a shared 0..1 axis. */
3276
3715
  export interface TimelineRow {
@@ -3353,12 +3792,90 @@ export interface Timeline {
3353
3792
  readonly rowProps: (id: number) => TimelineRowProps;
3354
3793
  readonly rulerProps: TimelineRulerProps;
3355
3794
  }
3356
- /**
3357
- * Headless span timeline on Solid 2. The exported accessors are signals; the
3358
- * prop objects are getter-backed, so spreading them keeps every attribute
3359
- * reactive.
3360
- */
3361
- export declare const createTimeline: (options?: TimelineOptions) => Timeline;
3795
+ /** One span of a real trace: a point on a shared axis, labelled, at a depth. */
3796
+ export interface TimelineSpan {
3797
+ readonly id: string;
3798
+ readonly label: string;
3799
+ /** Offset on the axis, in the axis' unit; `end` is the same axis, not a duration. */
3800
+ readonly start: number;
3801
+ readonly end: number;
3802
+ /** Nesting level, 0 for a root span. */
3803
+ readonly depth?: number;
3804
+ readonly status?: "ok" | "error" | "unset";
3805
+ }
3806
+ export interface TimelineAxis {
3807
+ /** The unit the numbers are in; the ruler labels its ticks with it. */
3808
+ readonly unit?: "ms" | "us" | "s";
3809
+ }
3810
+ /** The rows mode: real span data, read lazily like every other option. */
3811
+ export interface TimelineRowsOptions {
3812
+ readonly rows: ReadonlyArray<TimelineSpan>;
3813
+ readonly axis?: TimelineAxis | undefined;
3814
+ /** Controlled: the host owns the selection. */
3815
+ readonly selected?: string | undefined;
3816
+ /** A request to change the selection the host may ignore. */
3817
+ readonly onSelect?: ((id: string) => void) | undefined;
3818
+ }
3819
+ export interface TimelineTick {
3820
+ readonly value: number;
3821
+ readonly left: number;
3822
+ readonly label: string;
3823
+ }
3824
+ export interface TimelineListProps {
3825
+ readonly role: "list";
3826
+ readonly "aria-label": string;
3827
+ }
3828
+ export interface TimelineSpanRowProps {
3829
+ readonly role: "listitem";
3830
+ /** `depth + 1`: the nesting a flat list cannot show in the DOM. */
3831
+ readonly "aria-level": number;
3832
+ readonly "aria-current": "true" | undefined;
3833
+ /** A styling hook: the status the span ended with. */
3834
+ readonly "data-status": "ok" | "error" | "unset";
3835
+ readonly tabindex: number;
3836
+ readonly onClick: () => void;
3837
+ readonly onKeyDown: (event: KeyboardEvent) => void;
3838
+ }
3839
+ export interface TimelineBarProps {
3840
+ readonly "aria-hidden": "true";
3841
+ /** `left` / `width` are percentages of the extent; `--depth` is the row's depth. */
3842
+ readonly style: {
3843
+ readonly left: string;
3844
+ readonly width: string;
3845
+ readonly "--depth": string;
3846
+ };
3847
+ }
3848
+ export interface TimelineLabelProps {
3849
+ /** `--depth`, so the host indents the label (`padding-inline-start: calc(var(--depth) * 1rem)`) instead of nesting it. */
3850
+ readonly style: {
3851
+ readonly "--depth": string;
3852
+ };
3853
+ }
3854
+ export interface TimelineRulerRowsProps {
3855
+ readonly role: "presentation";
3856
+ readonly "aria-hidden": "true";
3857
+ /** Ticks of a nice step across the extent; render them as children. */
3858
+ readonly ticks: ReadonlyArray<TimelineTick>;
3859
+ }
3860
+ export interface TimelineRows {
3861
+ /** `{ start, end }` across the rows; `{ 0, 0 }` when there are none. */
3862
+ readonly extent: () => {
3863
+ readonly start: number;
3864
+ readonly end: number;
3865
+ };
3866
+ readonly depthOf: (id: string) => number;
3867
+ readonly listProps: TimelineListProps;
3868
+ readonly rowProps: (id: string) => TimelineSpanRowProps;
3869
+ readonly barProps: (id: string) => TimelineBarProps;
3870
+ readonly labelProps: (id: string) => TimelineLabelProps;
3871
+ readonly rulerProps: TimelineRulerRowsProps;
3872
+ /** Programmatic selection request; a controlled host may ignore it. */
3873
+ readonly select: (id: string) => void;
3874
+ readonly selectedId: Accessor<string | undefined>;
3875
+ }
3876
+ /** The demo five-span tree (`rows` absent) or real span data (`rows` given). */
3877
+ export declare function createTimeline(options: TimelineRowsOptions): TimelineRows;
3878
+ export declare function createTimeline(options?: TimelineOptions): Timeline;
3362
3879
  /** A toast's urgency; `error` is the only tone that announces assertively. */
3363
3880
  export type ToastTone = "info" | "success" | "error";
3364
3881
  export interface ToastOptions {