@cssints/headless 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2051,6 +2051,90 @@ const createCheckbox = (options = {}) => {
2051
2051
  }
2052
2052
  };
2053
2053
  };
2054
+ // src/field.ts
2055
+ const text = (value) => value === void 0 || value === "" ? void 0 : value;
2056
+ const external = (value) => (value === void 0 ? [] : typeof value === "string" ? [value] : value).filter((id) => id !== "");
2057
+ /**
2058
+ * The ARIA contract of a form field: a label, a control, a hint and an error.
2059
+ * There is no APG pattern for it (APG has no "form field"; the nearest is the
2060
+ * label and description rules of the accessible name computation), so the
2061
+ * contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
2062
+ * `aria-required`) and HTML (`label[for]`, `required`), and from the
2063
+ * majority of Base UI Field, react-aria TextField and ariakit FormControl,
2064
+ * which list the description and the error message together. It is not checked
2065
+ * against a Quint spec; `test/field.browser.test.tsx` is its oracle.
2066
+ *
2067
+ * Decisions: the error does not replace the hint, both are listed, hint first
2068
+ * (the hint is what tells the user how to fix the error); an error implies
2069
+ * invalid and `invalid: false` overrides it; a hidden message is never
2070
+ * referenced, so `aria-describedby` has no dangling id. The error element is
2071
+ * `role="alert"` only while it is shown (a hidden element announces nothing, and
2072
+ * one that appears is announced); the hint is never an alert.
2073
+ *
2074
+ * `describedby` lists host-owned ids after the field's own: one refusal that
2075
+ * belongs to two controls is one form-level element (with its own `role`), named
2076
+ * by both. The field cannot know whether that element is shown: the host
2077
+ * passes the id only while it is.
2078
+ *
2079
+ * Options are read when an attribute is read: pass getters, never destructure.
2080
+ */
2081
+ const createField = (options = {}) => {
2082
+ const base = options.id ?? createUniqueId();
2083
+ const controlId = `${base}-control`;
2084
+ const labelId = `${base}-label`;
2085
+ const hintId = `${base}-hint`;
2086
+ const errorId = `${base}-error`;
2087
+ const hint = () => text(options.hint);
2088
+ const message = () => text(options.error) ?? (typeof options.invalid === "string" ? text(options.invalid) : void 0);
2089
+ const invalid = () => options.invalid === false ? false : options.invalid === true || message() !== void 0;
2090
+ const error = () => invalid() ? message() : void 0;
2091
+ const describedIds = () => [
2092
+ ...hint() === void 0 ? [] : [hintId],
2093
+ ...error() === void 0 ? [] : [errorId],
2094
+ ...external(options.describedby)
2095
+ ];
2096
+ return {
2097
+ invalid,
2098
+ hint,
2099
+ error,
2100
+ describedIds,
2101
+ controlProps: {
2102
+ id: controlId,
2103
+ get "aria-describedby"() {
2104
+ const ids = describedIds();
2105
+ return ids.length === 0 ? void 0 : ids.join(" ");
2106
+ },
2107
+ get "aria-invalid"() {
2108
+ return invalid() ? "true" : void 0;
2109
+ },
2110
+ get "aria-required"() {
2111
+ return options.required === true ? "true" : void 0;
2112
+ },
2113
+ get required() {
2114
+ return options.required === true ? true : void 0;
2115
+ }
2116
+ },
2117
+ labelProps: {
2118
+ id: labelId,
2119
+ for: controlId
2120
+ },
2121
+ hintProps: {
2122
+ id: hintId,
2123
+ get hidden() {
2124
+ return hint() === void 0;
2125
+ }
2126
+ },
2127
+ errorProps: {
2128
+ id: errorId,
2129
+ get hidden() {
2130
+ return error() === void 0;
2131
+ },
2132
+ get role() {
2133
+ return error() === void 0 ? void 0 : "alert";
2134
+ }
2135
+ }
2136
+ };
2137
+ };
2054
2138
  // src/checkbox-group.ts
2055
2139
  const CHILDREN = [
2056
2140
  "c0",
@@ -2089,7 +2173,17 @@ const nextFocus = (focus) => {
2089
2173
  * pass `value` + `onValueChange` to own the checked subset. The checked set is
2090
2174
  * packed internally so it rides the scalar `createControllable`.
2091
2175
  *
2092
- * Behaviour is checked against `specs/apg/checkbox-group.qnt` by the bridge.
2176
+ * The name, the description and the error are `createField`'s contract on the
2177
+ * `role="group"` element (`aria-label` / `aria-labelledby`, `aria-describedby`,
2178
+ * `aria-invalid`, an error element that is `role="alert"` while it is shown), so
2179
+ * a group reads like the fields around it. The group is not a `<label for>`
2180
+ * target: a host names a visible label element's id with `labelledby`, or passes
2181
+ * `label` without one.
2182
+ *
2183
+ * Behaviour is checked against `specs/apg/checkbox-group.qnt` by the bridge. That
2184
+ * model leaves labelling out on purpose ("shift+Tab, accessible names,
2185
+ * fieldset/legend or role=group labelling" in its header), so the field contract
2186
+ * is checked by plain assertions in `test/checkbox-group.browser.test.tsx`.
2093
2187
  */
2094
2188
  const createCheckboxGroup = (options) => {
2095
2189
  const values = options.values;
@@ -2100,6 +2194,18 @@ const createCheckboxGroup = (options) => {
2100
2194
  initial: packedOf(options.defaultValue ?? []),
2101
2195
  onChange: () => (next) => options.onValueChange?.(labelsOf(next))
2102
2196
  });
2197
+ const field = createField({
2198
+ id: options.id,
2199
+ get invalid() {
2200
+ return options.invalid;
2201
+ },
2202
+ get error() {
2203
+ return options.error;
2204
+ },
2205
+ get describedby() {
2206
+ return options.describedby;
2207
+ }
2208
+ });
2103
2209
  const [memory, setMemory] = createSignal(normalizeChildren([]));
2104
2210
  const [remember, setRememberSignal] = createSignal(options.remember === true);
2105
2211
  const [focused, setFocused] = createSignal("none");
@@ -2157,6 +2263,9 @@ const createCheckboxGroup = (options) => {
2157
2263
  focused,
2158
2264
  parentChecked,
2159
2265
  checkedValues: () => selectedValues(children()),
2266
+ error: field.error,
2267
+ invalid: field.invalid,
2268
+ describedIds: field.describedIds,
2160
2269
  click: (target) => {
2161
2270
  activate(target);
2162
2271
  },
@@ -2179,7 +2288,20 @@ const createCheckboxGroup = (options) => {
2179
2288
  requestPacked(packed(normalizeChildren(next)));
2180
2289
  },
2181
2290
  groupProps: {
2291
+ id: field.controlProps.id,
2182
2292
  role: "group",
2293
+ get "aria-label"() {
2294
+ return options.label;
2295
+ },
2296
+ get "aria-labelledby"() {
2297
+ return options.labelledby;
2298
+ },
2299
+ get "aria-describedby"() {
2300
+ return field.controlProps["aria-describedby"];
2301
+ },
2302
+ get "aria-invalid"() {
2303
+ return field.controlProps["aria-invalid"];
2304
+ },
2183
2305
  onKeyDown: (event) => {
2184
2306
  if (event.key === "Tab") {
2185
2307
  event.preventDefault();
@@ -2190,6 +2312,7 @@ const createCheckboxGroup = (options) => {
2190
2312
  }
2191
2313
  }
2192
2314
  },
2315
+ errorProps: field.errorProps,
2193
2316
  parentProps: {
2194
2317
  role: "checkbox",
2195
2318
  get "aria-checked"() {
@@ -2584,72 +2707,472 @@ const createDialogModal = (options = {}) => {
2584
2707
  }
2585
2708
  };
2586
2709
  };
2587
- // src/overlay-shell.ts
2588
- /**
2589
- * The page-wide half of a modal overlay: one scroll lock, one stack of open
2590
- * overlays, the tabbables of a panel and the Tab cycle over them. Shared by
2591
- * `createConfirm` and `createDrawer` so that a confirm opened over a drawer (or
2592
- * the reverse) takes Escape and Tab first, and the scroll lock outlives the
2593
- * first of them to close.
2594
- */
2595
- const TABBABLE = "a[href], button, input, select, textarea, [tabindex], [contenteditable]";
2596
- /** The visible, tabbable elements of `root`, in document order. */
2597
- const tabbables = (root) => Array.from(root.querySelectorAll(TABBABLE)).filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && element.getClientRects().length > 0);
2598
- /** The first tabbable element of `root`, if any. */
2599
- const firstFocusable$1 = (root) => tabbables(root)[0];
2600
- /**
2601
- * Wraps Tab / Shift+Tab inside `root` and pulls a focus that escaped back in
2602
- * on the next Tab. With nothing tabbable the panel itself (which is
2603
- * `tabindex="-1"`) keeps the focus.
2604
- */
2605
- const cycleFocus = (event, root) => {
2606
- const items = tabbables(root);
2607
- const first = items[0];
2608
- const last = items.at(-1);
2609
- const active = document.activeElement;
2610
- const inside = root.contains(active);
2611
- if (first === void 0 || last === void 0) {
2612
- event.preventDefault();
2613
- root.focus();
2614
- } else if (event.shiftKey && (!inside || active === first || active === root)) {
2615
- event.preventDefault();
2616
- last.focus();
2617
- } else if (!event.shiftKey && (!inside || active === last)) {
2618
- event.preventDefault();
2619
- first.focus();
2710
+ // src/hotkeys.ts
2711
+ const MODIFIERS = /* @__PURE__ */ new Set([
2712
+ "mod",
2713
+ "ctrl",
2714
+ "alt",
2715
+ "shift",
2716
+ "meta"
2717
+ ]);
2718
+ const SHIFTED = /* @__PURE__ */ new Set("~!@#$%^&*()_+{}|:\"<>?");
2719
+ const detectsApple = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
2720
+ const parse = (keys, isApple) => {
2721
+ const parts = keys.toLowerCase().split("+");
2722
+ const key = parts.pop() ?? "";
2723
+ if (key === "" || MODIFIERS.has(key)) throw new TypeError(`hotkeys: "${keys}" ends with no key`);
2724
+ const chord = {
2725
+ ctrl: false,
2726
+ meta: false,
2727
+ alt: false,
2728
+ shift: SHIFTED.has(key),
2729
+ key
2730
+ };
2731
+ for (const part of parts) switch (part) {
2732
+ case "mod":
2733
+ if (isApple) chord.meta = true;
2734
+ else chord.ctrl = true;
2735
+ break;
2736
+ case "ctrl":
2737
+ chord.ctrl = true;
2738
+ break;
2739
+ case "meta":
2740
+ chord.meta = true;
2741
+ break;
2742
+ case "alt":
2743
+ chord.alt = true;
2744
+ break;
2745
+ case "shift":
2746
+ chord.shift = true;
2747
+ break;
2748
+ default: throw new TypeError(`hotkeys: "${part}" in "${keys}" is not a modifier`);
2620
2749
  }
2750
+ return chord;
2621
2751
  };
2622
- let scrollLocks = 0;
2623
- let scrollBefore = "";
2752
+ 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;
2753
+ const editable = (target) => target instanceof HTMLElement && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable);
2754
+ const inScope = (scope, target) => scope === void 0 || target instanceof Node && scope.contains(target);
2624
2755
  /**
2625
- * `overflow: hidden` on the document element, counted: the first holder takes
2626
- * it, the last to release restores what `overflow` was. The returned release is
2627
- * idempotent.
2756
+ * One `keydown` listener in the capture phase, one list of chords. A chord
2757
+ * fires when its modifiers match exactly, its `when` holds, its scope contains
2758
+ * the event's target, and the focus is not in a text field (unless
2759
+ * `allowInEditable`). The most recently bound match is the only one that fires,
2760
+ * so a modal's `"/"` overrides the page's and unbinding it brings the page's
2761
+ * back; `preventDefault` is on by default for the bindings whose chord the
2762
+ * browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
2628
2763
  */
2629
- const lockScroll = () => {
2630
- const root = document.documentElement;
2631
- if (scrollLocks++ === 0) {
2632
- scrollBefore = root.style.overflow;
2633
- root.style.overflow = "hidden";
2634
- }
2635
- let released = false;
2636
- return () => {
2637
- if (released) return;
2638
- released = true;
2639
- if (--scrollLocks === 0) root.style.overflow = scrollBefore;
2764
+ const createHotkeys = (options = {}) => {
2765
+ const isApple = options.isApple ?? detectsApple();
2766
+ const bindings = [];
2767
+ let listening = false;
2768
+ let disposed = false;
2769
+ const onKeyDown = (event) => {
2770
+ for (let i = bindings.length - 1; i >= 0; i--) {
2771
+ const entry = bindings[i];
2772
+ if (entry === void 0) continue;
2773
+ const { chord, binding } = entry;
2774
+ if (!matches(chord, event) || binding.when?.() === false) continue;
2775
+ if (!inScope(binding.scope, event.target)) continue;
2776
+ if (binding.allowInEditable !== true && editable(event.target)) continue;
2777
+ if (binding.preventDefault !== false) event.preventDefault();
2778
+ binding.onTrigger(event);
2779
+ return;
2780
+ }
2781
+ };
2782
+ const stop = () => {
2783
+ if (!listening) return;
2784
+ listening = false;
2785
+ document.removeEventListener("keydown", onKeyDown, true);
2786
+ };
2787
+ return {
2788
+ bind: (binding) => {
2789
+ if (disposed) return () => {};
2790
+ const entry = {
2791
+ chord: parse(binding.keys, isApple),
2792
+ binding
2793
+ };
2794
+ bindings.push(entry);
2795
+ if (!listening) {
2796
+ listening = true;
2797
+ document.addEventListener("keydown", onKeyDown, true);
2798
+ }
2799
+ return () => {
2800
+ const at = bindings.indexOf(entry);
2801
+ if (at === -1) return;
2802
+ bindings.splice(at, 1);
2803
+ if (bindings.length === 0) stop();
2804
+ };
2805
+ },
2806
+ dispose: () => {
2807
+ disposed = true;
2808
+ bindings.length = 0;
2809
+ stop();
2810
+ }
2640
2811
  };
2641
2812
  };
2642
- const openOverlays = [];
2813
+ // src/status.ts
2643
2814
  /**
2644
- * Joins the open overlays; the last one to claim takes Escape and Tab. Returns
2645
- * the release (idempotent), which may be called in any order.
2646
- */
2647
- const claimOverlay = (self) => {
2648
- openOverlays.push(self);
2649
- let claimed = true;
2650
- return () => {
2651
- if (!claimed) return;
2652
- claimed = false;
2815
+ * APG Alert's polite sibling: one `role=status` live region whose text the host
2816
+ * replaces ("Link copied", "Saved"). It is not a queue: a second message shows
2817
+ * in place of the first, so a burst reads as the last state, not as N alerts.
2818
+ * The region is rendered once and stays mounted, which is what makes a change
2819
+ * of its text announced.
2820
+ *
2821
+ * A status is never for errors: an error that needs attention now is
2822
+ * `createAlert` (`role=alert`, assertive), and a message with a title, a
2823
+ * description and a timeout is `createToast`.
2824
+ */
2825
+ const createStatus = (options = {}) => {
2826
+ const id = options.id ?? createUniqueId();
2827
+ const [message, setMessage] = createSignal();
2828
+ return {
2829
+ message,
2830
+ show: (text) => {
2831
+ setMessage(text);
2832
+ },
2833
+ clear: () => {
2834
+ setMessage(void 0);
2835
+ },
2836
+ statusProps: {
2837
+ id,
2838
+ role: "status",
2839
+ "aria-live": "polite",
2840
+ "aria-atomic": "true"
2841
+ }
2842
+ };
2843
+ };
2844
+ // src/textbox.ts
2845
+ const control = (field, options) => ({
2846
+ ...field.controlProps,
2847
+ get type() {
2848
+ return options.type ?? "text";
2849
+ },
2850
+ get autoComplete() {
2851
+ return options.autoComplete;
2852
+ },
2853
+ get spellcheck() {
2854
+ if (options.spellCheck === void 0) return;
2855
+ return options.spellCheck ? "true" : "false";
2856
+ },
2857
+ get autocapitalize() {
2858
+ return options.autoCapitalize;
2859
+ },
2860
+ get inputmode() {
2861
+ return options.inputMode;
2862
+ },
2863
+ get maxlength() {
2864
+ return options.maxLength;
2865
+ },
2866
+ get readOnly() {
2867
+ return options.readOnly === true ? true : void 0;
2868
+ },
2869
+ get disabled() {
2870
+ return options.disabled === true ? true : void 0;
2871
+ },
2872
+ get name() {
2873
+ return options.name;
2874
+ },
2875
+ get dir() {
2876
+ return options.dir;
2877
+ },
2878
+ get placeholder() {
2879
+ return options.placeholder;
2880
+ },
2881
+ onInput: (event) => {
2882
+ options.onValueChange?.(event.currentTarget.value);
2883
+ }
2884
+ });
2885
+ /**
2886
+ * A text input with the field contract attached: label, hint, error (announced),
2887
+ * `aria-describedby` and `aria-invalid` come from `createField`; this adds the
2888
+ * props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
2889
+ * disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
2890
+ * no primitive re-implements text editing, which is why there is no key
2891
+ * handling here).
2892
+ *
2893
+ * Both `required` and `aria-required` are emitted: the native attribute is what
2894
+ * the browser enforces, the ARIA one is what a custom widget reading the same
2895
+ * props needs. A host that validates itself passes `noValidate` on the form
2896
+ * rather than dropping the attribute.
2897
+ */
2898
+ const createTextInput = (options = {}) => {
2899
+ const field = createField(options);
2900
+ return {
2901
+ ...field,
2902
+ controlProps: control(field, options)
2903
+ };
2904
+ };
2905
+ /**
2906
+ * The same contract for a textarea, plus rows, wrap and resize. `resize` is a
2907
+ * style because CSS has no attribute for it; `none` is the value a layout that
2908
+ * owns its own height wants.
2909
+ */
2910
+ const createTextArea = (options = {}) => {
2911
+ const field = createField(options);
2912
+ return {
2913
+ ...field,
2914
+ controlProps: {
2915
+ ...control(field, options),
2916
+ get rows() {
2917
+ return options.rows;
2918
+ },
2919
+ get wrap() {
2920
+ return options.wrap;
2921
+ },
2922
+ get style() {
2923
+ return options.resize === void 0 ? void 0 : { resize: options.resize };
2924
+ }
2925
+ }
2926
+ };
2927
+ };
2928
+ // src/command-palette.ts
2929
+ const countLabel = (count) => count === 0 ? "No commands" : count === 1 ? "1 command" : `${count} commands`;
2930
+ /**
2931
+ * A command palette: `createDialogModal` (a modal dialog, announced as one),
2932
+ * `createTextInput` (a `searchbox`, not a combobox — choosing runs a command,
2933
+ * it does not complete the field) and one filtered list. Focus stays in the
2934
+ * field; `aria-activedescendant` names the highlighted option, and ↓ on the
2935
+ * last / ↑ on the first return the highlight to the field. Home/End are menu
2936
+ * keys only while the field is empty, so the caret keeps them while typing.
2937
+ * Filtering is a plain case-insensitive substring match on the label and the
2938
+ * keywords, no fuzzy scoring: a palette is short and stable, and ranking
2939
+ * (recency, scopes) is a policy the host owns.
2940
+ *
2941
+ * The host's: rendering and grouping (the `group` field, headings, the CSS),
2942
+ * async commands (a rejected `run` is the host's to report), and any close
2943
+ * other than Escape / a command. `runActive` closes before it runs.
2944
+ */
2945
+ const createCommandPalette = (options) => {
2946
+ const corpus = options.commands;
2947
+ const base = createUniqueId();
2948
+ const titleId = `${base}-title`;
2949
+ const listId = `${base}-list`;
2950
+ const optionId = (id) => `${base}-option-${id}`;
2951
+ const labels = {
2952
+ title: options.labels?.title ?? "Command palette",
2953
+ placeholder: options.labels?.placeholder ?? "Type a command",
2954
+ empty: options.labels?.empty ?? "No commands found",
2955
+ results: options.labels?.results ?? countLabel
2956
+ };
2957
+ const filter = (text) => {
2958
+ const needle = text.toLowerCase();
2959
+ return corpus.filter((command) => command.label.toLowerCase().includes(needle) || (command.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(needle))).map(({ id, label, group }) => ({
2960
+ id,
2961
+ label,
2962
+ group
2963
+ }));
2964
+ };
2965
+ const [query, setQueryRaw] = createSignal("");
2966
+ const [active, setActiveRaw] = createSignal(0);
2967
+ const results = () => filter(query());
2968
+ const dialog = createDialogModal({
2969
+ id: base,
2970
+ role: "dialog",
2971
+ labelledby: titleId,
2972
+ value: options.open,
2973
+ onOpenChange: (open) => options.onOpenChange?.(open)
2974
+ });
2975
+ const status = createStatus();
2976
+ const setQuery = (text) => {
2977
+ setQueryRaw(text);
2978
+ setActiveRaw(filter(text).length > 0 ? 0 : -1);
2979
+ };
2980
+ const run = (id) => {
2981
+ const chosen = corpus.find((command) => command.id === id);
2982
+ if (chosen === void 0) return;
2983
+ dialog.close();
2984
+ return chosen.run();
2985
+ };
2986
+ const runActive = () => {
2987
+ const current = results()[active()];
2988
+ return current === void 0 ? void 0 : run(current.id);
2989
+ };
2990
+ const onKeyDown = (event) => {
2991
+ if (event.isComposing) return;
2992
+ const last = results().length - 1;
2993
+ switch (event.key) {
2994
+ case "ArrowDown":
2995
+ event.preventDefault();
2996
+ setActiveRaw(active() < last ? active() + 1 : -1);
2997
+ return;
2998
+ case "ArrowUp":
2999
+ event.preventDefault();
3000
+ setActiveRaw(active() > 0 ? active() - 1 : last);
3001
+ return;
3002
+ case "Home":
3003
+ case "End":
3004
+ if (query() !== "") return;
3005
+ event.preventDefault();
3006
+ setActiveRaw(event.key === "Home" ? last >= 0 ? 0 : -1 : last);
3007
+ return;
3008
+ case "Enter":
3009
+ event.preventDefault();
3010
+ runActive();
3011
+ return;
3012
+ default: return;
3013
+ }
3014
+ };
3015
+ let returnFocus = null;
3016
+ createEffect(() => dialog.open(), (isOpen) => {
3017
+ if (isOpen) {
3018
+ returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3019
+ document.getElementById(`${base}-control`)?.focus();
3020
+ return;
3021
+ }
3022
+ setQueryRaw("");
3023
+ setActiveRaw(0);
3024
+ const back = returnFocus;
3025
+ returnFocus = null;
3026
+ if (back !== null && back.isConnected) back.focus();
3027
+ });
3028
+ createEffect(() => [dialog.open(), results().length], ([isOpen, count]) => {
3029
+ if (isOpen) status.show(labels.results(count));
3030
+ else status.clear();
3031
+ });
3032
+ const input = createTextInput({
3033
+ id: base,
3034
+ type: "text",
3035
+ autoComplete: "off",
3036
+ spellCheck: false,
3037
+ placeholder: labels.placeholder,
3038
+ onValueChange: setQuery
3039
+ });
3040
+ return {
3041
+ open: dialog.open,
3042
+ openPalette: dialog.openDialog,
3043
+ closePalette: dialog.close,
3044
+ query,
3045
+ setQuery,
3046
+ active,
3047
+ setActive: (index) => setActiveRaw(Math.max(-1, Math.min(index, results().length - 1))),
3048
+ results,
3049
+ labels,
3050
+ status: status.message,
3051
+ inputProps: {
3052
+ ...input.controlProps,
3053
+ role: "searchbox",
3054
+ get value() {
3055
+ return query();
3056
+ },
3057
+ "aria-controls": listId,
3058
+ get "aria-activedescendant"() {
3059
+ const current = results()[active()];
3060
+ return current === void 0 ? void 0 : optionId(current.id);
3061
+ },
3062
+ onKeyDown
3063
+ },
3064
+ listProps: {
3065
+ id: listId,
3066
+ role: "listbox",
3067
+ "aria-labelledby": titleId,
3068
+ onMouseDown: (event) => event.preventDefault()
3069
+ },
3070
+ optionProps: (id) => ({
3071
+ id: optionId(id),
3072
+ role: "option",
3073
+ get "aria-selected"() {
3074
+ return results()[active()]?.id === id ? "true" : "false";
3075
+ },
3076
+ tabindex: -1,
3077
+ onClick: () => {
3078
+ if (results().some((result) => result.id === id)) run(id);
3079
+ },
3080
+ onMouseEnter: () => {
3081
+ const index = results().findIndex((result) => result.id === id);
3082
+ if (index >= 0) setActiveRaw(index);
3083
+ }
3084
+ }),
3085
+ emptyProps: { get hidden() {
3086
+ return results().length > 0;
3087
+ } },
3088
+ statusProps: status.statusProps,
3089
+ dialogProps: dialog.dialogProps,
3090
+ backdropProps: dialog.backdropProps,
3091
+ titleProps: { id: titleId },
3092
+ runActive,
3093
+ attach: () => {
3094
+ const hotkeys = createHotkeys();
3095
+ const unbind = hotkeys.bind({
3096
+ keys: options.hotkey ?? "mod+k",
3097
+ allowInEditable: true,
3098
+ onTrigger: () => {
3099
+ if (dialog.open()) dialog.close();
3100
+ else dialog.openDialog();
3101
+ }
3102
+ });
3103
+ return () => {
3104
+ unbind();
3105
+ hotkeys.dispose();
3106
+ };
3107
+ }
3108
+ };
3109
+ };
3110
+ // src/overlay-shell.ts
3111
+ /**
3112
+ * The page-wide half of a modal overlay: one scroll lock, one stack of open
3113
+ * overlays, the tabbables of a panel and the Tab cycle over them. Shared by
3114
+ * `createConfirm` and `createDrawer` so that a confirm opened over a drawer (or
3115
+ * the reverse) takes Escape and Tab first, and the scroll lock outlives the
3116
+ * first of them to close.
3117
+ */
3118
+ const TABBABLE = "a[href], button, input, select, textarea, [tabindex], [contenteditable]";
3119
+ /** The visible, tabbable elements of `root`, in document order. */
3120
+ const tabbables = (root) => Array.from(root.querySelectorAll(TABBABLE)).filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && element.getClientRects().length > 0);
3121
+ /** The first tabbable element of `root`, if any. */
3122
+ const firstFocusable$1 = (root) => tabbables(root)[0];
3123
+ /**
3124
+ * Wraps Tab / Shift+Tab inside `root` and pulls a focus that escaped back in
3125
+ * on the next Tab. With nothing tabbable the panel itself (which is
3126
+ * `tabindex="-1"`) keeps the focus.
3127
+ */
3128
+ const cycleFocus = (event, root) => {
3129
+ const items = tabbables(root);
3130
+ const first = items[0];
3131
+ const last = items.at(-1);
3132
+ const active = document.activeElement;
3133
+ const inside = root.contains(active);
3134
+ if (first === void 0 || last === void 0) {
3135
+ event.preventDefault();
3136
+ root.focus();
3137
+ } else if (event.shiftKey && (!inside || active === first || active === root)) {
3138
+ event.preventDefault();
3139
+ last.focus();
3140
+ } else if (!event.shiftKey && (!inside || active === last)) {
3141
+ event.preventDefault();
3142
+ first.focus();
3143
+ }
3144
+ };
3145
+ let scrollLocks = 0;
3146
+ let scrollBefore = "";
3147
+ /**
3148
+ * `overflow: hidden` on the document element, counted: the first holder takes
3149
+ * it, the last to release restores what `overflow` was. The returned release is
3150
+ * idempotent.
3151
+ */
3152
+ const lockScroll = () => {
3153
+ const root = document.documentElement;
3154
+ if (scrollLocks++ === 0) {
3155
+ scrollBefore = root.style.overflow;
3156
+ root.style.overflow = "hidden";
3157
+ }
3158
+ let released = false;
3159
+ return () => {
3160
+ if (released) return;
3161
+ released = true;
3162
+ if (--scrollLocks === 0) root.style.overflow = scrollBefore;
3163
+ };
3164
+ };
3165
+ const openOverlays = [];
3166
+ /**
3167
+ * Joins the open overlays; the last one to claim takes Escape and Tab. Returns
3168
+ * the release (idempotent), which may be called in any order.
3169
+ */
3170
+ const claimOverlay = (self) => {
3171
+ openOverlays.push(self);
3172
+ let claimed = true;
3173
+ return () => {
3174
+ if (!claimed) return;
3175
+ claimed = false;
2653
3176
  const at = openOverlays.indexOf(self);
2654
3177
  if (at !== -1) openOverlays.splice(at, 1);
2655
3178
  };
@@ -2939,6 +3462,165 @@ const createDrawer = (options = {}) => {
2939
3462
  handleProps: { "aria-hidden": "true" }
2940
3463
  };
2941
3464
  };
3465
+ // src/dropzone.ts
3466
+ const REASON_TEXT = {
3467
+ type: "unsupported type",
3468
+ size: "too large"
3469
+ };
3470
+ const tokensOf = (accept) => (accept ?? "").split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "");
3471
+ /** The HTML `accept` grammar: a token starting with "." is an extension, else a MIME type, `*` allowed. */
3472
+ const accepts = (accept, file) => {
3473
+ const tokens = tokensOf(accept);
3474
+ if (tokens.length === 0) return true;
3475
+ const name = file.name.toLowerCase();
3476
+ const [fileType = "", fileSubtype = ""] = file.type.toLowerCase().split("/");
3477
+ return tokens.some((token) => {
3478
+ if (token.startsWith(".")) return name.endsWith(token);
3479
+ const [type = "", subtype = ""] = token.split("/");
3480
+ return type === fileType && (subtype === "*" || subtype === fileSubtype);
3481
+ });
3482
+ };
3483
+ const refusal = (options, file) => {
3484
+ if (!accepts(options.accept, file)) return "type";
3485
+ if (options.maxSize !== void 0 && file.size > options.maxSize) return "size";
3486
+ };
3487
+ const messageOf = (batch) => {
3488
+ if (batch === void 0) return;
3489
+ const files = batch.total === 1 ? "1 file" : `${batch.total} files`;
3490
+ if (batch.rejected.length === 0) return files;
3491
+ const reasons = [...new Set(batch.rejected)].map((reason) => REASON_TEXT[reason]).join(", ");
3492
+ return `${files}, ${batch.rejected.length} rejected: ${reasons}`;
3493
+ };
3494
+ /**
3495
+ * A file drop zone. There is no APG pattern and no Quint spec for one: the
3496
+ * contract is HTML's file input plus WAI-ARIA's live region, and
3497
+ * `test/dropzone.browser.test.tsx` is its oracle.
3498
+ *
3499
+ * Decisions:
3500
+ * - **The zone is never drag-only.** `browseProps` spreads onto a real
3501
+ * `<button type="button">` that clicks the hidden input, so the picker is the
3502
+ * keyboard path (Enter and Space are the platform's) and a pointer user can
3503
+ * click as well as drag. The drop handlers add nothing the button cannot do.
3504
+ * - **Validation happens once per file**, in the order type then size, and a
3505
+ * refused file reaches `onReject` with the reason the host shows. `accept` is
3506
+ * the HTML grammar (`.` extension, MIME type, `*`); `maxSize` is bytes.
3507
+ * - **Dropping a folder or a `DataTransfer` with no files is a no-op**: no
3508
+ * `onFiles`, no `onReject`, no message, and the previous announcement stays.
3509
+ * - **`dragleave` counts.** A child's `dragleave` bubbles to the zone, so the
3510
+ * state is a depth counter: it clears only when the drag leaves the zone
3511
+ * itself, or on a drop.
3512
+ * - **The zone is `role=group`** so `labels.zone` is exposed; `dragging()` is
3513
+ * the only drag state a skin reads (no `data-dragging`).
3514
+ * - `disabled` marks the zone `aria-disabled`, disables the input natively and
3515
+ * the browse button, and drops every drag before it becomes a batch.
3516
+ */
3517
+ const createDropZone = (options) => {
3518
+ const id = createUniqueId();
3519
+ const [depth, setDepth] = createSignal(0);
3520
+ const [batch, setBatch] = createSignal();
3521
+ const disabled = () => options.disabled === true;
3522
+ const deliver = (files) => {
3523
+ if (disabled() || files.length === 0) return;
3524
+ const accepted = [];
3525
+ const rejected = [];
3526
+ for (const file of files) {
3527
+ const reason = refusal(options, file);
3528
+ if (reason === void 0) accepted.push(file);
3529
+ else {
3530
+ rejected.push(reason);
3531
+ options.onReject?.({
3532
+ file,
3533
+ reason
3534
+ });
3535
+ }
3536
+ }
3537
+ setBatch({
3538
+ total: files.length,
3539
+ rejected
3540
+ });
3541
+ if (accepted.length > 0) options.onFiles(accepted);
3542
+ };
3543
+ const dropped = (event) => {
3544
+ event.preventDefault();
3545
+ setDepth(0);
3546
+ if (disabled()) return;
3547
+ const transfer = event.dataTransfer;
3548
+ if (transfer === null) return;
3549
+ const items = Array.from(transfer.items).filter((item) => item.kind === "file");
3550
+ const files = Array.from(transfer.files);
3551
+ if (files.length === 0) return;
3552
+ if (items.length > 0 && items.every((item) => item.webkitGetAsEntry?.()?.isDirectory === true)) return;
3553
+ deliver(files);
3554
+ };
3555
+ const picked = (event) => {
3556
+ const input = event.currentTarget;
3557
+ const files = Array.from(input.files ?? []);
3558
+ input.value = "";
3559
+ deliver(files);
3560
+ };
3561
+ const browse = () => {
3562
+ if (disabled()) return;
3563
+ document.getElementById(id)?.click();
3564
+ };
3565
+ return {
3566
+ dragging: () => depth() > 0,
3567
+ message: () => messageOf(batch()),
3568
+ inputProps: {
3569
+ id,
3570
+ type: "file",
3571
+ get accept() {
3572
+ return options.accept;
3573
+ },
3574
+ get multiple() {
3575
+ return options.multiple === true ? true : void 0;
3576
+ },
3577
+ get disabled() {
3578
+ return disabled() ? true : void 0;
3579
+ },
3580
+ hidden: true,
3581
+ onChange: picked
3582
+ },
3583
+ zoneProps: {
3584
+ role: "group",
3585
+ get "aria-label"() {
3586
+ return options.labels?.zone;
3587
+ },
3588
+ get "aria-disabled"() {
3589
+ return disabled() ? "true" : void 0;
3590
+ },
3591
+ onDragEnter: (event) => {
3592
+ event.preventDefault();
3593
+ if (!disabled()) setDepth(depth() + 1);
3594
+ },
3595
+ onDragOver: (event) => {
3596
+ event.preventDefault();
3597
+ },
3598
+ onDragLeave: (event) => {
3599
+ event.preventDefault();
3600
+ if (!disabled()) setDepth(Math.max(0, depth() - 1));
3601
+ },
3602
+ onDrop: dropped
3603
+ },
3604
+ browseProps: {
3605
+ type: "button",
3606
+ get disabled() {
3607
+ return disabled() ? true : void 0;
3608
+ },
3609
+ get "aria-disabled"() {
3610
+ return disabled() ? "true" : void 0;
3611
+ },
3612
+ get "aria-label"() {
3613
+ return options.labels?.browse;
3614
+ },
3615
+ onClick: browse
3616
+ },
3617
+ statusProps: {
3618
+ role: "status",
3619
+ "aria-live": "polite",
3620
+ "aria-atomic": "true"
3621
+ }
3622
+ };
3623
+ };
2942
3624
  // src/feed.ts
2943
3625
  /**
2944
3626
  * APG Feed (https://www.w3.org/WAI/ARIA/apg/patterns/feed/): state plus prop
@@ -3013,148 +3695,64 @@ const createFeed = (options = {}) => {
3013
3695
  }
3014
3696
  };
3015
3697
  return {
3016
- items,
3017
- loaded,
3018
- busy,
3019
- active,
3020
- focusArticle,
3021
- pageDown,
3022
- pageUp,
3023
- ctrlEnd: () => setActive("after"),
3024
- ctrlHome: () => setActive("before"),
3025
- loadStart: startLoad,
3026
- loadFinish: (count) => {
3027
- if (phase() !== "loading") return;
3028
- const all = total();
3029
- const remaining = all === null ? count : Math.max(0, all - loaded());
3030
- const next = loaded() + Math.min(count, all === null ? count : remaining);
3031
- requestLoaded(next);
3032
- setPhase("idle");
3033
- },
3034
- loadFail: () => {
3035
- if (phase() === "loading") setPhase("idle");
3036
- },
3037
- setSizeMode: (mode) => setSizeMode(mode),
3038
- setTotalKnown: (known) => setTotalKnown(known),
3039
- feedProps: {
3040
- id: base,
3041
- role: "feed",
3042
- get "aria-busy"() {
3043
- return busy() ? "true" : void 0;
3044
- },
3045
- "aria-labelledby": options.labelledBy
3046
- },
3047
- articleProps: (posinset) => ({
3048
- id: `${base}-article-${posinset}`,
3049
- role: "article",
3050
- "aria-posinset": posinset,
3051
- get "aria-setsize"() {
3052
- if (sizeMode() === "loaded") return loaded();
3053
- const all = total();
3054
- return totalKnown() && all !== null ? all : -1;
3055
- },
3056
- "aria-labelledby": `${base}-article-${posinset}-title`,
3057
- "aria-describedby": `${base}-article-${posinset}-content`,
3058
- tabindex: 0,
3059
- get hidden() {
3060
- return posinset > loaded();
3061
- },
3062
- onFocus: () => focusArticle(posinset),
3063
- onKeyDown
3064
- }),
3065
- beforeProps: {
3066
- id: `${base}-before`,
3067
- tabindex: 0,
3068
- onFocus: () => setActive("before")
3069
- },
3070
- afterProps: {
3071
- id: `${base}-after`,
3072
- tabindex: 0,
3073
- onFocus: () => setActive("after")
3074
- }
3075
- };
3076
- };
3077
- // src/field.ts
3078
- const text = (value) => value === void 0 || value === "" ? void 0 : value;
3079
- const external = (value) => (value === void 0 ? [] : typeof value === "string" ? [value] : value).filter((id) => id !== "");
3080
- /**
3081
- * The ARIA contract of a form field: a label, a control, a hint and an error.
3082
- * There is no APG pattern for it (APG has no "form field"; the nearest is the
3083
- * label and description rules of the accessible name computation), so the
3084
- * contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
3085
- * `aria-required`) and HTML (`label[for]`, `required`), and from the
3086
- * majority of Base UI Field, react-aria TextField and ariakit FormControl,
3087
- * which list the description and the error message together. It is not checked
3088
- * against a Quint spec; `test/field.browser.test.tsx` is its oracle.
3089
- *
3090
- * Decisions: the error does not replace the hint, both are listed, hint first
3091
- * (the hint is what tells the user how to fix the error); an error implies
3092
- * invalid and `invalid: false` overrides it; a hidden message is never
3093
- * referenced, so `aria-describedby` has no dangling id. The error element is
3094
- * `role="alert"` only while it is shown (a hidden element announces nothing, and
3095
- * one that appears is announced); the hint is never an alert.
3096
- *
3097
- * `describedby` lists host-owned ids after the field's own: one refusal that
3098
- * belongs to two controls is one form-level element (with its own `role`), named
3099
- * by both. The field cannot know whether that element is shown: the host
3100
- * passes the id only while it is.
3101
- *
3102
- * Options are read when an attribute is read: pass getters, never destructure.
3103
- */
3104
- const createField = (options = {}) => {
3105
- const base = options.id ?? createUniqueId();
3106
- const controlId = `${base}-control`;
3107
- const labelId = `${base}-label`;
3108
- const hintId = `${base}-hint`;
3109
- const errorId = `${base}-error`;
3110
- const hint = () => text(options.hint);
3111
- const message = () => text(options.error) ?? (typeof options.invalid === "string" ? text(options.invalid) : void 0);
3112
- const invalid = () => options.invalid === false ? false : options.invalid === true || message() !== void 0;
3113
- const error = () => invalid() ? message() : void 0;
3114
- const describedIds = () => [
3115
- ...hint() === void 0 ? [] : [hintId],
3116
- ...error() === void 0 ? [] : [errorId],
3117
- ...external(options.describedby)
3118
- ];
3119
- return {
3120
- invalid,
3121
- hint,
3122
- error,
3123
- describedIds,
3124
- controlProps: {
3125
- id: controlId,
3126
- get "aria-describedby"() {
3127
- const ids = describedIds();
3128
- return ids.length === 0 ? void 0 : ids.join(" ");
3129
- },
3130
- get "aria-invalid"() {
3131
- return invalid() ? "true" : void 0;
3132
- },
3133
- get "aria-required"() {
3134
- return options.required === true ? "true" : void 0;
3135
- },
3136
- get required() {
3137
- return options.required === true ? true : void 0;
3138
- }
3698
+ items,
3699
+ loaded,
3700
+ busy,
3701
+ active,
3702
+ focusArticle,
3703
+ pageDown,
3704
+ pageUp,
3705
+ ctrlEnd: () => setActive("after"),
3706
+ ctrlHome: () => setActive("before"),
3707
+ loadStart: startLoad,
3708
+ loadFinish: (count) => {
3709
+ if (phase() !== "loading") return;
3710
+ const all = total();
3711
+ const remaining = all === null ? count : Math.max(0, all - loaded());
3712
+ const next = loaded() + Math.min(count, all === null ? count : remaining);
3713
+ requestLoaded(next);
3714
+ setPhase("idle");
3139
3715
  },
3140
- labelProps: {
3141
- id: labelId,
3142
- for: controlId
3716
+ loadFail: () => {
3717
+ if (phase() === "loading") setPhase("idle");
3143
3718
  },
3144
- hintProps: {
3145
- id: hintId,
3146
- get hidden() {
3147
- return hint() === void 0;
3148
- }
3719
+ setSizeMode: (mode) => setSizeMode(mode),
3720
+ setTotalKnown: (known) => setTotalKnown(known),
3721
+ feedProps: {
3722
+ id: base,
3723
+ role: "feed",
3724
+ get "aria-busy"() {
3725
+ return busy() ? "true" : void 0;
3726
+ },
3727
+ "aria-labelledby": options.labelledBy
3149
3728
  },
3150
- errorProps: {
3151
- id: errorId,
3729
+ articleProps: (posinset) => ({
3730
+ id: `${base}-article-${posinset}`,
3731
+ role: "article",
3732
+ "aria-posinset": posinset,
3733
+ get "aria-setsize"() {
3734
+ if (sizeMode() === "loaded") return loaded();
3735
+ const all = total();
3736
+ return totalKnown() && all !== null ? all : -1;
3737
+ },
3738
+ "aria-labelledby": `${base}-article-${posinset}-title`,
3739
+ "aria-describedby": `${base}-article-${posinset}-content`,
3740
+ tabindex: 0,
3152
3741
  get hidden() {
3153
- return error() === void 0;
3742
+ return posinset > loaded();
3154
3743
  },
3155
- get role() {
3156
- return error() === void 0 ? void 0 : "alert";
3157
- }
3744
+ onFocus: () => focusArticle(posinset),
3745
+ onKeyDown
3746
+ }),
3747
+ beforeProps: {
3748
+ id: `${base}-before`,
3749
+ tabindex: 0,
3750
+ onFocus: () => setActive("before")
3751
+ },
3752
+ afterProps: {
3753
+ id: `${base}-after`,
3754
+ tabindex: 0,
3755
+ onFocus: () => setActive("after")
3158
3756
  }
3159
3757
  };
3160
3758
  };
@@ -4131,106 +4729,218 @@ const createGrid = (options = {}) => {
4131
4729
  }
4132
4730
  };
4133
4731
  };
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;
4732
+ // src/knob.ts
4733
+ /** The pointer must travel this far before the drag picks a mode. */
4734
+ const LOCK_PX = 6;
4735
+ /** A slide is only locked while the other axis stays this small a share of the movement. */
4736
+ const SLIDE_VARIANCE = .35;
4737
+ /** The radius (px) at which a full turn sweeps the whole range; farther out is finer. */
4738
+ const REFERENCE_RADIUS = 100;
4739
+ /** The radius floor of the spin sensitivity, so a grab at the centre is not wildly coarse. */
4740
+ const MIN_SPIN_RADIUS = 25;
4741
+ /** Pixels of slide that move the value by the whole range. */
4742
+ const SLIDE_PX_PER_RANGE = 200;
4743
+ const angleOf = (x, y) => Math.atan2(y, x) * 180 / Math.PI;
4744
+ const angleDelta = (from, to) => {
4745
+ const diff = (to - from) % 360;
4746
+ return diff > 180 ? diff - 360 : diff < -180 ? diff + 360 : diff;
4175
4747
  };
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
4748
  /**
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).
4749
+ * A virtual knob after GarageBand's (https://jherrm.github.io/knobs/): one
4750
+ * control turned three ways — spun around its centre, or slid vertically or
4751
+ * horizontally, one gesture locked per drag. Plain Solid signals, no elements,
4752
+ * no styling. **No ARIA role exists for a dial**, so the knob is a `role=slider`
4753
+ * with `aria-valuenow` / `aria-valuetext`; the value the host renders in
4754
+ * `valueProps` is the live announcement (`role=status`), and a numeric field
4755
+ * must sit next to the dial, because a dial is never the only control.
4756
+ *
4757
+ * Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultValue`);
4758
+ * pass `value` + `onValueChange` to own the value. A key, a wheel, a drag and
4759
+ * `setValue`/`stepBy` are requests the host may ignore.
4760
+ *
4761
+ * Gestures (`gesture`, default "both"), decided once on the first meaningful
4762
+ * movement so a drag never switches modes:
4763
+ * - a movement that stays on one axis (the other axis under `SLIDE_VARIANCE` of
4764
+ * it) locks a slide — the reference's "slide in a straight line";
4765
+ * - any other movement is a turn around the centre: the angle the pointer
4766
+ * covers is the value, and the farther from the centre the finer it is;
4767
+ * - `gesture: "spin"` / `"slide"` disable the other.
4768
+ *
4769
+ * A vertical slide turns the knob the way the grabbed side would turn a real
4770
+ * one: down on the right half is clockwise (up in value), down on the left half
4771
+ * counter-clockwise. A horizontal slide uses the same left/right side (the
4772
+ * reference picks top/bottom for this one; one rule for both is simpler to
4773
+ * learn). `wrap` makes the dial continuous instead of clamped.
4774
+ *
4775
+ * ponytail: no momentum/inertia and no `disabled` — the reference has a
4776
+ * deceleration tail, and no host here has asked for one; add inertia in the
4777
+ * host's rAF loop if a look needs it.
4187
4778
  */
4188
- const createHotkeys = (options = {}) => {
4189
- const isApple = options.isApple ?? detectsApple();
4190
- const bindings = [];
4191
- let listening = false;
4192
- let disposed = false;
4779
+ const createKnob = (options = {}) => {
4780
+ const min = options.min ?? 0;
4781
+ const max = options.max ?? 100;
4782
+ const step = options.step ?? 1;
4783
+ const span = max - min;
4784
+ const base = options.id ?? createUniqueId();
4785
+ const wrap = () => options.wrap === true;
4786
+ const bound = (next) => {
4787
+ if (span <= 0) return min;
4788
+ if (wrap()) return min + ((next - min) % span + span) % span;
4789
+ return Math.max(min, Math.min(max, next));
4790
+ };
4791
+ const quantize = (next) => {
4792
+ const stepped = step > 0 ? min + Math.round((next - min) / step) * step : next;
4793
+ return bound(Number(stepped.toPrecision(12)));
4794
+ };
4795
+ const [value, request] = createControllable({
4796
+ value: () => options.value,
4797
+ initial: quantize(options.defaultValue ?? min + span / 2),
4798
+ onChange: () => options.onValueChange
4799
+ });
4800
+ const commit = (next) => request(quantize(next));
4801
+ const percentOf = (at) => {
4802
+ if (span <= 0) return 0;
4803
+ const fraction = (at - min) / span;
4804
+ return wrap() ? fraction - Math.floor(fraction) : Math.max(0, Math.min(1, fraction));
4805
+ };
4806
+ const stepBy = (delta) => commit(value() + delta);
4193
4807
  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;
4808
+ switch (event.key) {
4809
+ case "ArrowUp":
4810
+ case "ArrowRight":
4811
+ stepBy(step);
4812
+ break;
4813
+ case "ArrowDown":
4814
+ case "ArrowLeft":
4815
+ stepBy(-step);
4816
+ break;
4817
+ case "PageUp":
4818
+ stepBy(step * 10);
4819
+ break;
4820
+ case "PageDown":
4821
+ stepBy(-step * 10);
4822
+ break;
4823
+ case "Home":
4824
+ commit(min);
4825
+ break;
4826
+ case "End":
4827
+ commit(max);
4828
+ break;
4829
+ default: return;
4204
4830
  }
4831
+ event.preventDefault();
4205
4832
  };
4206
- const stop = () => {
4207
- if (!listening) return;
4208
- listening = false;
4209
- document.removeEventListener("keydown", onKeyDown, true);
4833
+ let drag;
4834
+ const modeFor = (current, event) => {
4835
+ if (options.gesture === "spin") return "spin";
4836
+ const dx = event.clientX - current.fromX;
4837
+ const dy = event.clientY - current.fromY;
4838
+ const vertical = Math.abs(dy);
4839
+ const horizontal = Math.abs(dx);
4840
+ const straight = Math.min(vertical, horizontal) <= Math.max(vertical, horizontal) * SLIDE_VARIANCE;
4841
+ if (options.gesture !== "slide" && !straight) return "spin";
4842
+ return vertical >= horizontal ? "vertical" : "horizontal";
4210
4843
  };
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);
4844
+ const pointerDown = (event) => {
4845
+ if (event.pointerType === "mouse" && event.button !== 0) return;
4846
+ const element = event.currentTarget;
4847
+ if (!(element instanceof Element)) return;
4848
+ try {
4849
+ element.setPointerCapture(event.pointerId);
4850
+ } catch {}
4851
+ const rect = element.getBoundingClientRect();
4852
+ const centreX = rect.left + rect.width / 2;
4853
+ const centreY = rect.top + rect.height / 2;
4854
+ const startAngle = angleOf(event.clientX - centreX, event.clientY - centreY);
4855
+ drag = {
4856
+ pointerId: event.pointerId,
4857
+ centreX,
4858
+ centreY,
4859
+ side: event.clientX >= centreX ? 1 : -1,
4860
+ fromX: event.clientX,
4861
+ fromY: event.clientY,
4862
+ startAngle,
4863
+ raw: value(),
4864
+ mode: void 0,
4865
+ lastAngle: startAngle,
4866
+ lastX: event.clientX,
4867
+ lastY: event.clientY
4868
+ };
4869
+ };
4870
+ const pointerMove = (event) => {
4871
+ const current = drag;
4872
+ if (current === void 0 || current.pointerId !== event.pointerId) return;
4873
+ if (current.mode === void 0) {
4874
+ if (Math.hypot(event.clientX - current.fromX, event.clientY - current.fromY) < LOCK_PX) return;
4875
+ current.mode = modeFor(current, event);
4876
+ if (current.mode === "spin") current.lastAngle = current.startAngle;
4877
+ else {
4878
+ current.lastX = current.fromX;
4879
+ current.lastY = current.fromY;
4222
4880
  }
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
- };
4881
+ }
4882
+ if (current.mode === "spin") {
4883
+ const angle = angleOf(event.clientX - current.centreX, event.clientY - current.centreY);
4884
+ const delta = angleDelta(current.lastAngle, angle);
4885
+ current.lastAngle = angle;
4886
+ const radius = Math.hypot(event.clientX - current.centreX, event.clientY - current.centreY);
4887
+ const perDegree = span / 360 * (REFERENCE_RADIUS / Math.max(radius, MIN_SPIN_RADIUS));
4888
+ current.raw = bound(current.raw + delta * perDegree);
4889
+ } else {
4890
+ const pixels = current.mode === "vertical" ? event.clientY - current.lastY : event.clientX - current.lastX;
4891
+ current.lastX = event.clientX;
4892
+ current.lastY = event.clientY;
4893
+ current.raw = bound(current.raw + pixels * (span / SLIDE_PX_PER_RANGE) * current.side);
4894
+ }
4895
+ commit(current.raw);
4896
+ };
4897
+ const pointerUp = (event) => {
4898
+ const current = drag;
4899
+ if (current === void 0 || current.pointerId !== event.pointerId) return;
4900
+ drag = void 0;
4901
+ const target = event.currentTarget;
4902
+ if (target instanceof Element) try {
4903
+ target.releasePointerCapture(event.pointerId);
4904
+ } catch {}
4905
+ };
4906
+ const onWheel = (event) => {
4907
+ if (event.deltaY === 0) return;
4908
+ event.preventDefault();
4909
+ stepBy(event.deltaY < 0 ? step : -step);
4910
+ };
4911
+ return {
4912
+ value,
4913
+ setValue: commit,
4914
+ percentOf,
4915
+ stepBy,
4916
+ knobProps: {
4917
+ role: "slider",
4918
+ get tabindex() {
4919
+ return 0;
4920
+ },
4921
+ "aria-valuemin": String(min),
4922
+ "aria-valuemax": String(max),
4923
+ get "aria-valuenow"() {
4924
+ return String(value());
4925
+ },
4926
+ get "aria-valuetext"() {
4927
+ const format = options.format;
4928
+ return format === void 0 ? String(value()) : format(value());
4929
+ },
4930
+ get "aria-label"() {
4931
+ return options.labels?.knob ?? "Knob";
4932
+ },
4933
+ onKeyDown,
4934
+ onPointerDown: pointerDown,
4935
+ onPointerMove: pointerMove,
4936
+ onPointerUp: pointerUp,
4937
+ onPointerCancel: pointerUp,
4938
+ onWheel
4229
4939
  },
4230
- dispose: () => {
4231
- disposed = true;
4232
- bindings.length = 0;
4233
- stop();
4940
+ valueProps: {
4941
+ id: `${base}-value`,
4942
+ role: "status",
4943
+ "aria-live": "polite"
4234
4944
  }
4235
4945
  };
4236
4946
  };
@@ -4661,9 +5371,11 @@ const createMenu = (options) => {
4661
5371
  const disabled = new Set(options.disabled ?? []);
4662
5372
  for (const [index, item] of items.entries()) if (item.disabled) disabled.add(index);
4663
5373
  const held = (options.triggerMode ?? "outside") === "held";
4664
- let level = 0;
4665
- let index = -1;
4666
- let open = options.defaultOpen ?? false;
5374
+ const [isOpen, requestOpen] = createControllable({
5375
+ value: () => options.open,
5376
+ initial: options.defaultOpen === true,
5377
+ onChange: () => options.onOpenChange
5378
+ });
4667
5379
  let checked = new Set(options.defaultChecked ?? []);
4668
5380
  const variantChoice = () => {
4669
5381
  if (options.variant === "mixed") return 1;
@@ -4699,57 +5411,80 @@ const createMenu = (options) => {
4699
5411
  version();
4700
5412
  return read();
4701
5413
  };
4702
- if (open) {
4703
- level = 2;
4704
- index = 0;
4705
- } else if (held) level = 1;
5414
+ let closed = held ? 1 : 0;
5415
+ let index = 0;
5416
+ let seenOpen = isOpen();
4706
5417
  const isDisabled = (i) => disabled.has(i);
4707
- const setFocus = (nextLevel, nextIndex) => {
4708
- level = nextLevel;
4709
- index = nextIndex;
5418
+ const sync = () => {
5419
+ const now = isOpen();
5420
+ if (now === seenOpen) return;
5421
+ seenOpen = now;
5422
+ index = now ? 0 : -1;
5423
+ closed = now ? closed : 1;
5424
+ };
5425
+ const level = () => {
5426
+ sync();
5427
+ return isOpen() ? 2 : closed;
5428
+ };
5429
+ const focusedIndex = () => {
5430
+ sync();
5431
+ return index;
5432
+ };
5433
+ const focusItem = (next) => {
5434
+ index = next;
4710
5435
  bump();
4711
5436
  };
5437
+ const move = (want, nextIndex, nextClosed) => {
5438
+ const apply = () => {
5439
+ seenOpen = isOpen();
5440
+ index = want ? nextIndex : -1;
5441
+ closed = nextClosed;
5442
+ bump();
5443
+ };
5444
+ if (isOpen() === want) {
5445
+ apply();
5446
+ return;
5447
+ }
5448
+ requestOpen(want);
5449
+ if (options.open === void 0) {
5450
+ apply();
5451
+ return;
5452
+ }
5453
+ queueMicrotask(() => {
5454
+ if (isOpen() === want) apply();
5455
+ });
5456
+ };
4712
5457
  const openOn = (target) => {
4713
- level = 2;
4714
- index = target;
4715
- open = true;
4716
- bump();
4717
- options.onOpenChange?.(true);
5458
+ move(true, target, 1);
4718
5459
  };
4719
5460
  const closeToTrigger = () => {
4720
- level = 1;
4721
- index = -1;
4722
- open = false;
4723
- bump();
4724
- options.onOpenChange?.(false);
5461
+ move(false, -1, 1);
4725
5462
  };
4726
5463
  const focusTrigger = () => {
4727
- if (level === 0) setFocus(1, -1);
5464
+ if (level() === 0) {
5465
+ closed = 1;
5466
+ bump();
5467
+ }
4728
5468
  };
4729
5469
  const leave = () => {
4730
- const wasOpen = open;
4731
- level = held ? 1 : 0;
4732
- index = -1;
4733
- open = false;
4734
- bump();
4735
- if (wasOpen) options.onOpenChange?.(false);
5470
+ move(false, -1, held ? 1 : 0);
4736
5471
  };
4737
5472
  const clickTrigger = () => {
4738
- if (open) closeToTrigger();
5473
+ if (isOpen()) closeToTrigger();
4739
5474
  else openOn(0);
4740
5475
  };
4741
5476
  const openMenu = () => {
4742
- if (!open) openOn(0);
5477
+ if (!isOpen()) openOn(0);
4743
5478
  };
4744
5479
  const closeMenu = () => {
4745
- if (open) closeToTrigger();
5480
+ if (isOpen()) closeToTrigger();
4746
5481
  };
4747
5482
  const activate = (close) => {
4748
- if (level === 1) {
5483
+ if (level() === 1) {
4749
5484
  openOn(0);
4750
5485
  return;
4751
5486
  }
4752
- if (level !== 2 || index < 0 || isDisabled(index)) return;
5487
+ if (level() !== 2 || index < 0 || isDisabled(index)) return;
4753
5488
  const item = items[index];
4754
5489
  if (item === void 0) return;
4755
5490
  if (item.kind === "checkbox") {
@@ -4790,23 +5525,23 @@ const createMenu = (options) => {
4790
5525
  activate(false);
4791
5526
  break;
4792
5527
  case "ArrowDown":
4793
- if (level === 1) openOn(0);
4794
- else if (level === 2) setFocus(2, wrapNext(index));
5528
+ if (level() === 1) openOn(0);
5529
+ else if (level() === 2) focusItem(wrapNext(focusedIndex()));
4795
5530
  break;
4796
5531
  case "ArrowUp":
4797
- if (level === 1) openOn(count - 1);
4798
- else if (level === 2) setFocus(2, wrapPrev(index));
5532
+ if (level() === 1) openOn(count - 1);
5533
+ else if (level() === 2) focusItem(wrapPrev(focusedIndex()));
4799
5534
  break;
4800
5535
  case "Home":
4801
- if (level === 2) setFocus(2, 0);
5536
+ if (level() === 2) focusItem(0);
4802
5537
  break;
4803
5538
  case "End":
4804
- if (level === 2) setFocus(2, count - 1);
5539
+ if (level() === 2) focusItem(count - 1);
4805
5540
  break;
4806
5541
  case "Escape":
4807
- if (level === 2) closeToTrigger();
5542
+ if (level() === 2) closeToTrigger();
4808
5543
  break;
4809
- case "Tab": if (level !== 0) leave();
5544
+ case "Tab": if (level() !== 0) leave();
4810
5545
  }
4811
5546
  };
4812
5547
  const search = (from, text) => {
@@ -4817,36 +5552,36 @@ const createMenu = (options) => {
4817
5552
  }
4818
5553
  };
4819
5554
  const type = (text) => {
4820
- if (!open || text.length === 0) return;
5555
+ if (!isOpen() || text.length === 0) return;
4821
5556
  buffer += text;
4822
5557
  if (bufferTimer !== void 0) clearTimeout(bufferTimer);
4823
5558
  bufferTimer = setTimeout(() => {
4824
5559
  buffer = "";
4825
5560
  }, prefs.typeaheadDelayMs);
4826
- const match = search(index >= 0 ? index : 0, buffer);
4827
- if (match !== void 0) setFocus(2, match);
5561
+ const from = focusedIndex() >= 0 ? focusedIndex() : 0;
5562
+ const match = search(from, buffer);
5563
+ if (match !== void 0) focusItem(match);
4828
5564
  };
4829
5565
  const codeOf = (i) => i % 2 === 0 ? 1 : 2;
4830
5566
  const typeaheadCode = (code) => {
4831
- if (!open) return;
4832
- const from = index >= 0 ? index : 0;
5567
+ if (!isOpen()) return;
5568
+ const from = focusedIndex() >= 0 ? focusedIndex() : 0;
4833
5569
  for (let offset = 1; offset < count; offset += 1) {
4834
5570
  const candidate = (from + offset) % count;
4835
5571
  if (codeOf(candidate) === code) {
4836
- setFocus(2, candidate);
5572
+ focusItem(candidate);
4837
5573
  return;
4838
5574
  }
4839
5575
  }
4840
5576
  };
4841
5577
  const setFocusItem = (i) => {
4842
5578
  if (i < 0 || i >= count) return;
4843
- open = true;
4844
- setFocus(2, i);
5579
+ move(true, i, 1);
4845
5580
  };
4846
5581
  return {
4847
- open: track(() => open),
4848
- focusLevel: track(() => level),
4849
- focusIndex: track(() => index),
5582
+ open: track(isOpen),
5583
+ focusLevel: track(level),
5584
+ focusIndex: track(focusedIndex),
4850
5585
  items,
4851
5586
  checked: track(() => [...currentChecked()].sort((a, b) => a - b)),
4852
5587
  choice: track(() => currentChoice()),
@@ -4865,7 +5600,7 @@ const createMenu = (options) => {
4865
5600
  "aria-haspopup": "menu",
4866
5601
  get "aria-expanded"() {
4867
5602
  version();
4868
- return open ? "true" : "false";
5603
+ return isOpen() ? "true" : "false";
4869
5604
  },
4870
5605
  "aria-controls": menuId,
4871
5606
  get disabled() {
@@ -4887,7 +5622,7 @@ const createMenu = (options) => {
4887
5622
  role: "menu",
4888
5623
  get hidden() {
4889
5624
  version();
4890
- return !open;
5625
+ return !isOpen();
4891
5626
  },
4892
5627
  onKeyDown: (event) => {
4893
5628
  if (event.key === "Tab" || event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") event.preventDefault();
@@ -4913,7 +5648,7 @@ const createMenu = (options) => {
4913
5648
  },
4914
5649
  get tabindex() {
4915
5650
  version();
4916
- return level === 2 && index === i ? 0 : -1;
5651
+ return level() === 2 && index === i ? 0 : -1;
4917
5652
  },
4918
5653
  onClick: () => {
4919
5654
  if (isDisabled(i)) return;
@@ -5930,6 +6665,11 @@ const valueTextFor$1 = (value) => value <= 3 ? "low" : value <= 7 ? "medium" : "
5930
6665
  * pass `value` + `onValueChange` to own the value. A key or rail press is a
5931
6666
  * request the host may ignore.
5932
6667
  *
6668
+ * `disabled` is a widget-level extension of the pattern (the spec lists it
6669
+ * under its deliberate omissions): the thumb keeps its tab stop and announces
6670
+ * `aria-disabled`, and every key and rail press is dropped before it becomes a
6671
+ * request.
6672
+ *
5933
6673
  * ponytail: dragging (pointer capture/move) is not modelled — the spec
5934
6674
  * deliberately only fixes a discrete rail press.
5935
6675
  */
@@ -5948,6 +6688,7 @@ const createSlider = (options = {}) => {
5948
6688
  const [focused, setFocused] = createSignal(false);
5949
6689
  const orientation = () => options.orientation ?? "horizontal";
5950
6690
  const direction = () => options.direction ?? "ltr";
6691
+ const disabled = () => options.disabled === true;
5951
6692
  const flipped = () => orientation() === "horizontal" && direction() === "rtl";
5952
6693
  const commit = (next) => request(clamp(next));
5953
6694
  const delta = (key) => {
@@ -5962,7 +6703,7 @@ const createSlider = (options = {}) => {
5962
6703
  }
5963
6704
  };
5964
6705
  const keyDown = (key) => {
5965
- if (!focused()) return false;
6706
+ if (!focused() || disabled()) return false;
5966
6707
  if (key === "Home") {
5967
6708
  commit(min);
5968
6709
  return true;
@@ -5977,6 +6718,7 @@ const createSlider = (options = {}) => {
5977
6718
  return true;
5978
6719
  };
5979
6720
  const pointerDown = (position) => {
6721
+ if (disabled()) return;
5980
6722
  const steps = Math.round(position * (max - min) / step);
5981
6723
  commit(min + steps * step);
5982
6724
  setFocused(true);
@@ -6003,6 +6745,9 @@ const createSlider = (options = {}) => {
6003
6745
  get "aria-orientation"() {
6004
6746
  return orientation();
6005
6747
  },
6748
+ get "aria-disabled"() {
6749
+ return disabled() ? "true" : void 0;
6750
+ },
6006
6751
  get tabindex() {
6007
6752
  return 0;
6008
6753
  },
@@ -6167,6 +6912,12 @@ const createSliderMultiThumb = (options = {}) => {
6167
6912
  * stepper buttons, Enter and blur are requests the host may ignore; typing and
6168
6913
  * Escape change no committed value.
6169
6914
  *
6915
+ * `disabled` is a widget-level extension of the pattern (the spec models the
6916
+ * steppers' bound only, and lists this under its deliberate omissions): the
6917
+ * field announces `aria-disabled` and carries the native `disabled`, every key,
6918
+ * typed draft and both steppers are dropped before they become a request, and
6919
+ * neither stepper offers itself.
6920
+ *
6170
6921
  * ponytail: commit/revert on Enter/blur/Escape is the common convention, not
6171
6922
  * APG text.
6172
6923
  */
@@ -6187,6 +6938,7 @@ const createSpinbutton = (options = {}) => {
6187
6938
  const [editing, setEditing] = createSignal(false);
6188
6939
  const [draft, setDraft] = createSignal("");
6189
6940
  const [wrap, setWrapSignal] = createSignal(options.wrap ?? false);
6941
+ const disabled = () => options.disabled === true;
6190
6942
  const draftInRange = (text) => {
6191
6943
  const number = Number(text);
6192
6944
  return text.trim() !== "" && number >= min && number <= max;
@@ -6214,7 +6966,7 @@ const createSpinbutton = (options = {}) => {
6214
6966
  commitValue(next);
6215
6967
  };
6216
6968
  const keyDown = (key) => {
6217
- if (!focused()) return false;
6969
+ if (!focused() || disabled()) return false;
6218
6970
  switch (key) {
6219
6971
  case "ArrowUp":
6220
6972
  applyStep(1, step);
@@ -6246,7 +6998,7 @@ const createSpinbutton = (options = {}) => {
6246
6998
  }
6247
6999
  };
6248
7000
  const commit = () => {
6249
- if (!focused()) return;
7001
+ if (!focused() || disabled()) return;
6250
7002
  const next = baseValue();
6251
7003
  clearDraft();
6252
7004
  commitValue(next);
@@ -6254,8 +7006,25 @@ const createSpinbutton = (options = {}) => {
6254
7006
  const revert = () => {
6255
7007
  clearDraft();
6256
7008
  };
7009
+ const blurField = () => {
7010
+ const next = baseValue();
7011
+ clearDraft();
7012
+ setFocused(false);
7013
+ if (disabled()) return;
7014
+ commitValue(next);
7015
+ };
7016
+ const increment = () => {
7017
+ if (disabled()) return;
7018
+ setFocused(true);
7019
+ applyStep(1, step);
7020
+ };
7021
+ const decrement = () => {
7022
+ if (disabled()) return;
7023
+ setFocused(true);
7024
+ applyStep(-1, step);
7025
+ };
6257
7026
  const displayed = () => editing() ? draft() : String(value());
6258
- const ariaDisabled = (atBound) => !wrap() && atBound ? "true" : void 0;
7027
+ const ariaDisabled = (atBound) => disabled() || !wrap() && atBound ? "true" : void 0;
6259
7028
  return {
6260
7029
  value,
6261
7030
  focused,
@@ -6264,28 +7033,17 @@ const createSpinbutton = (options = {}) => {
6264
7033
  displayed,
6265
7034
  setValue: commitValue,
6266
7035
  focus: () => setFocused(true),
6267
- blur: () => {
6268
- const next = baseValue();
6269
- clearDraft();
6270
- setFocused(false);
6271
- commitValue(next);
6272
- },
7036
+ blur: blurField,
6273
7037
  keyDown,
6274
7038
  typeInput: (text) => {
6275
- if (!focused()) return;
7039
+ if (!focused() || disabled()) return;
6276
7040
  setEditing(true);
6277
7041
  setDraft(text);
6278
7042
  },
6279
7043
  commit,
6280
7044
  revert,
6281
- increment: () => {
6282
- setFocused(true);
6283
- applyStep(1, step);
6284
- },
6285
- decrement: () => {
6286
- setFocused(true);
6287
- applyStep(-1, step);
6288
- },
7045
+ increment,
7046
+ decrement,
6289
7047
  setWrap: (next) => setWrapSignal(next),
6290
7048
  inputProps: {
6291
7049
  id: `${base}-input`,
@@ -6301,6 +7059,12 @@ const createSpinbutton = (options = {}) => {
6301
7059
  get "aria-invalid"() {
6302
7060
  return editing() && !draftInRange(draft()) ? "true" : void 0;
6303
7061
  },
7062
+ get "aria-disabled"() {
7063
+ return disabled() ? "true" : void 0;
7064
+ },
7065
+ get disabled() {
7066
+ return disabled();
7067
+ },
6304
7068
  get tabindex() {
6305
7069
  return 0;
6306
7070
  },
@@ -6308,18 +7072,13 @@ const createSpinbutton = (options = {}) => {
6308
7072
  return displayed();
6309
7073
  },
6310
7074
  onFocus: () => setFocused(true),
6311
- onBlur: () => {
6312
- const next = baseValue();
6313
- clearDraft();
6314
- setFocused(false);
6315
- commitValue(next);
6316
- },
7075
+ onBlur: blurField,
6317
7076
  onKeyDown: (event) => {
6318
7077
  if (keyDown(event.key)) event.preventDefault();
6319
7078
  },
6320
7079
  onInput: (event) => {
6321
7080
  const target = event.currentTarget;
6322
- if (target !== null) {
7081
+ if (target !== null && !disabled()) {
6323
7082
  setEditing(true);
6324
7083
  setDraft(target.value);
6325
7084
  }
@@ -6333,10 +7092,7 @@ const createSpinbutton = (options = {}) => {
6333
7092
  return ariaDisabled(value() === max);
6334
7093
  },
6335
7094
  onMouseDown: (event) => event.preventDefault(),
6336
- onClick: () => {
6337
- setFocused(true);
6338
- applyStep(1, step);
6339
- }
7095
+ onClick: increment
6340
7096
  },
6341
7097
  decrementProps: {
6342
7098
  role: "button",
@@ -6346,41 +7102,7 @@ const createSpinbutton = (options = {}) => {
6346
7102
  return ariaDisabled(value() === min);
6347
7103
  },
6348
7104
  onMouseDown: (event) => event.preventDefault(),
6349
- onClick: () => {
6350
- setFocused(true);
6351
- applyStep(-1, step);
6352
- }
6353
- }
6354
- };
6355
- };
6356
- // src/status.ts
6357
- /**
6358
- * APG Alert's polite sibling: one `role=status` live region whose text the host
6359
- * replaces ("Link copied", "Saved"). It is not a queue: a second message shows
6360
- * in place of the first, so a burst reads as the last state, not as N alerts.
6361
- * The region is rendered once and stays mounted, which is what makes a change
6362
- * of its text announced.
6363
- *
6364
- * A status is never for errors: an error that needs attention now is
6365
- * `createAlert` (`role=alert`, assertive), and a message with a title, a
6366
- * description and a timeout is `createToast`.
6367
- */
6368
- const createStatus = (options = {}) => {
6369
- const id = options.id ?? createUniqueId();
6370
- const [message, setMessage] = createSignal();
6371
- return {
6372
- message,
6373
- show: (text) => {
6374
- setMessage(text);
6375
- },
6376
- clear: () => {
6377
- setMessage(void 0);
6378
- },
6379
- statusProps: {
6380
- id,
6381
- role: "status",
6382
- "aria-live": "polite",
6383
- "aria-atomic": "true"
7105
+ onClick: decrement
6384
7106
  }
6385
7107
  };
6386
7108
  };
@@ -6638,90 +7360,6 @@ const createTable = (options) => {
6638
7360
  toggleRow
6639
7361
  };
6640
7362
  };
6641
- // src/textbox.ts
6642
- const control = (field, options) => ({
6643
- ...field.controlProps,
6644
- get type() {
6645
- return options.type ?? "text";
6646
- },
6647
- get autoComplete() {
6648
- return options.autoComplete;
6649
- },
6650
- get spellcheck() {
6651
- if (options.spellCheck === void 0) return;
6652
- return options.spellCheck ? "true" : "false";
6653
- },
6654
- get autocapitalize() {
6655
- return options.autoCapitalize;
6656
- },
6657
- get inputmode() {
6658
- return options.inputMode;
6659
- },
6660
- get maxlength() {
6661
- return options.maxLength;
6662
- },
6663
- get readOnly() {
6664
- return options.readOnly === true ? true : void 0;
6665
- },
6666
- get disabled() {
6667
- return options.disabled === true ? true : void 0;
6668
- },
6669
- get name() {
6670
- return options.name;
6671
- },
6672
- get dir() {
6673
- return options.dir;
6674
- },
6675
- get placeholder() {
6676
- return options.placeholder;
6677
- },
6678
- onInput: (event) => {
6679
- options.onValueChange?.(event.currentTarget.value);
6680
- }
6681
- });
6682
- /**
6683
- * A text input with the field contract attached: label, hint, error (announced),
6684
- * `aria-describedby` and `aria-invalid` come from `createField`; this adds the
6685
- * props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
6686
- * disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
6687
- * no primitive re-implements text editing, which is why there is no key
6688
- * handling here).
6689
- *
6690
- * Both `required` and `aria-required` are emitted: the native attribute is what
6691
- * the browser enforces, the ARIA one is what a custom widget reading the same
6692
- * props needs. A host that validates itself passes `noValidate` on the form
6693
- * rather than dropping the attribute.
6694
- */
6695
- const createTextInput = (options = {}) => {
6696
- const field = createField(options);
6697
- return {
6698
- ...field,
6699
- controlProps: control(field, options)
6700
- };
6701
- };
6702
- /**
6703
- * The same contract for a textarea, plus rows, wrap and resize. `resize` is a
6704
- * style because CSS has no attribute for it; `none` is the value a layout that
6705
- * owns its own height wants.
6706
- */
6707
- const createTextArea = (options = {}) => {
6708
- const field = createField(options);
6709
- return {
6710
- ...field,
6711
- controlProps: {
6712
- ...control(field, options),
6713
- get rows() {
6714
- return options.rows;
6715
- },
6716
- get wrap() {
6717
- return options.wrap;
6718
- },
6719
- get style() {
6720
- return options.resize === void 0 ? void 0 : { resize: options.resize };
6721
- }
6722
- }
6723
- };
6724
- };
6725
7363
  // src/tabs.ts
6726
7364
  /**
6727
7365
  * APG Tabs (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): state plus prop
@@ -6736,28 +7374,40 @@ const createTextArea = (options = {}) => {
6736
7374
  * Request/commit (map ticket `rt1p`): uncontrolled by default
6737
7375
  * (`defaultSelected`); pass `value` + `onValueChange` to own the selection.
6738
7376
  * Programmatic methods (`select`, `setSelected`, `keyDown`) are requests too.
7377
+ *
7378
+ * Close and rename are an **extension** (bean `cssints-gdv2`): the APG spec
7379
+ * models a `deletable` flavour but neither a callback nor a rename, and this
7380
+ * primitive owns no list. `closable` / `renamable` turn on the props; `onClose`
7381
+ * / `onRename` ask the host, which removes or renames. Focus after a close goes
7382
+ * to the tab that slid into the closed one's place, or to the previous tab when
7383
+ * the last one closed; closing the last remaining tab is refused, as the spec
7384
+ * refuses an empty tablist.
6739
7385
  */
6740
7386
  const createTabs = (options = {}) => {
6741
- const items = Math.max(1, options.items ?? 4);
7387
+ /** The live count: the host shrinks its list after a close, and the options are read lazily. */
7388
+ const count = () => Math.max(1, options.items ?? 4);
6742
7389
  const base = options.id ?? createUniqueId();
6743
7390
  const disabled = new Set(options.disabled ?? []);
6744
7391
  const focusablePanels = new Set(options.focusablePanels ?? []);
6745
7392
  const activation = () => options.activation ?? "automatic";
6746
7393
  const orientation = () => options.orientation ?? "horizontal";
7394
+ const closable = () => options.closable ?? false;
7395
+ const renamable = () => options.renamable ?? false;
7396
+ const labelOf = (index) => options.label?.(index) ?? `tab ${index + 1}`;
6747
7397
  const firstEnabled = () => {
6748
- for (let index = 0; index < items; index += 1) if (!disabled.has(index)) return index;
7398
+ for (let index = 0; index < count(); index += 1) if (!disabled.has(index)) return index;
6749
7399
  return 0;
6750
7400
  };
6751
7401
  const initial = options.defaultSelected ?? firstEnabled();
6752
7402
  const [selected, request] = createControllable({
6753
7403
  value: () => options.value,
6754
- initial: initial >= 0 && initial < items && !disabled.has(initial) ? initial : firstEnabled(),
7404
+ initial: initial >= 0 && initial < count() && !disabled.has(initial) ? initial : firstEnabled(),
6755
7405
  onChange: () => options.onValueChange
6756
7406
  });
6757
7407
  /** The tab that carries `tabindex=0`: the selected one, or the first enabled. */
6758
7408
  const roving = () => disabled.has(selected()) ? firstEnabled() : selected();
6759
7409
  const select = (index) => {
6760
- if (index < 0 || index >= items || disabled.has(index)) return;
7410
+ if (index < 0 || index >= count() || disabled.has(index)) return;
6761
7411
  request(index);
6762
7412
  };
6763
7413
  const move = (index) => {
@@ -6765,14 +7415,59 @@ const createTabs = (options = {}) => {
6765
7415
  if (activation() === "automatic") select(index);
6766
7416
  };
6767
7417
  const step = (from, delta) => {
6768
- for (let offset = 1; offset <= items; offset += 1) {
6769
- const candidate = ((from + delta * offset) % items + items) % items;
7418
+ for (let offset = 1; offset <= count(); offset += 1) {
7419
+ const candidate = ((from + delta * offset) % count() + count()) % count();
6770
7420
  if (!disabled.has(candidate)) return candidate;
6771
7421
  }
6772
7422
  return from;
6773
7423
  };
6774
7424
  const [focused, setFocused] = createSignal(-1);
7425
+ /**
7426
+ * The tab that takes focus once `onClose(index)`'s removal lands: the tab at
7427
+ * `index` in the shortened list (the following tab slid into it), or the
7428
+ * previous one when the last tab closed.
7429
+ */
7430
+ const focusAfterClose = (index) => index < count() - 1 ? index : Math.max(0, count() - 2);
7431
+ const close = (index) => {
7432
+ if (!closable() || count() <= 1 || index < 0 || index >= count()) return;
7433
+ const next = focusAfterClose(index);
7434
+ setFocused(next);
7435
+ if (selected() === index) select(next);
7436
+ options.onClose?.(index);
7437
+ };
7438
+ const [editVersion, setEditVersion] = createSignal(0);
7439
+ let editingIndex = null;
7440
+ const editing = () => {
7441
+ editVersion();
7442
+ return editingIndex;
7443
+ };
7444
+ const setEditing = (index) => {
7445
+ editingIndex = index;
7446
+ setEditVersion((version) => version + 1);
7447
+ };
7448
+ const [draft, setDraft] = createSignal("");
7449
+ const begin = (index) => {
7450
+ if (!renamable() || disabled.has(index) || index < 0 || index >= count()) return;
7451
+ setEditing(index);
7452
+ setDraft(options.label?.(index) ?? "");
7453
+ };
7454
+ const cancel = () => {
7455
+ if (editing() === null) return;
7456
+ setEditing(null);
7457
+ setDraft("");
7458
+ };
7459
+ /** Enter / blur commit; an empty label is not a name, so it cancels. */
7460
+ const commit = (label) => {
7461
+ const index = editing();
7462
+ if (index === null) return;
7463
+ setEditing(null);
7464
+ setDraft("");
7465
+ const trimmed = label.trim();
7466
+ if (trimmed === "") return;
7467
+ options.onRename?.(index, trimmed);
7468
+ };
6775
7469
  const keyDown = (key) => {
7470
+ if (editing() !== null) return;
6776
7471
  const current = focused();
6777
7472
  if (current < 0) return;
6778
7473
  const forward = orientation() === "horizontal" ? "ArrowRight" : "ArrowDown";
@@ -6788,27 +7483,34 @@ const createTabs = (options = {}) => {
6788
7483
  move(firstEnabled());
6789
7484
  break;
6790
7485
  case "End": {
6791
- let last = items - 1;
7486
+ let last = count() - 1;
6792
7487
  while (last > 0 && disabled.has(last)) last -= 1;
6793
7488
  move(last);
6794
7489
  break;
6795
7490
  }
6796
7491
  case "Enter":
6797
7492
  case " ":
6798
- case "Space": select(current);
7493
+ case "Space":
7494
+ select(current);
7495
+ break;
7496
+ case "Delete": close(current);
6799
7497
  }
6800
7498
  };
6801
7499
  return {
6802
- items,
7500
+ get items() {
7501
+ return count();
7502
+ },
6803
7503
  selected,
6804
7504
  focused,
7505
+ editing,
6805
7506
  select,
6806
7507
  setSelected: (index) => {
6807
- if (index >= 0 && index < items) request(index);
7508
+ if (index >= 0 && index < count()) request(index);
6808
7509
  },
6809
7510
  focusTab: (index) => {
6810
- if (index >= 0 && index < items) setFocused(index);
7511
+ if (index >= 0 && index < count()) setFocused(index);
6811
7512
  },
7513
+ focusAfterClose,
6812
7514
  keyDown,
6813
7515
  tablistProps: {
6814
7516
  role: "tablist",
@@ -6841,6 +7543,12 @@ const createTabs = (options = {}) => {
6841
7543
  setFocused(index);
6842
7544
  },
6843
7545
  onKeyDown: (event) => {
7546
+ if (editing() !== null) return;
7547
+ if (event.key === "Delete" && closable()) {
7548
+ event.preventDefault();
7549
+ close(index);
7550
+ return;
7551
+ }
6844
7552
  const before = focused();
6845
7553
  keyDown(event.key);
6846
7554
  if (focused() !== before || event.key === "Enter" || event.key === " ") event.preventDefault();
@@ -6856,6 +7564,43 @@ const createTabs = (options = {}) => {
6856
7564
  get hidden() {
6857
7565
  return selected() !== index;
6858
7566
  }
7567
+ }),
7568
+ closeProps: (index) => ({
7569
+ get "aria-label"() {
7570
+ return `Close ${labelOf(index)}`;
7571
+ },
7572
+ tabindex: -1,
7573
+ onClick: () => close(index)
7574
+ }),
7575
+ renameProps: (index) => ({
7576
+ onDblClick: () => begin(index),
7577
+ editing: () => editing() === index,
7578
+ begin,
7579
+ commit,
7580
+ cancel,
7581
+ inputProps: {
7582
+ type: "text",
7583
+ get value() {
7584
+ return draft();
7585
+ },
7586
+ get "aria-label"() {
7587
+ return `Rename ${labelOf(index)}`;
7588
+ },
7589
+ onInput: (event) => {
7590
+ const target = event.currentTarget;
7591
+ if (target !== null) setDraft(target.value);
7592
+ },
7593
+ onKeyDown: (event) => {
7594
+ if (event.key === "Enter") {
7595
+ event.preventDefault();
7596
+ commit(draft());
7597
+ } else if (event.key === "Escape") {
7598
+ event.preventDefault();
7599
+ cancel();
7600
+ }
7601
+ },
7602
+ onBlur: () => commit(draft())
7603
+ }
6859
7604
  })
6860
7605
  };
6861
7606
  };
@@ -6936,11 +7681,10 @@ const shrink = (x) => x <= MIN_WIDTH ? x : x - Math.floor(x / 4) < MIN_WIDTH ? M
6936
7681
  const grow = (x) => x >= DOMAIN_MAX ? x : x + Math.floor(x / 4) > DOMAIN_MAX ? DOMAIN_MAX : x + Math.floor(x / 4);
6937
7682
  const clampStart = (start, w) => start < DOMAIN_MIN ? DOMAIN_MIN : start + w > DOMAIN_MAX ? DOMAIN_MAX - w : start;
6938
7683
  /**
6939
- * Headless span timeline on Solid 2. The exported accessors are signals; the
6940
- * prop objects are getter-backed, so spreading them keeps every attribute
6941
- * reactive.
7684
+ * The demo tree: five fixed spans, collapsing, zoom. Its oracle is
7685
+ * `specs/apg/timeline.qnt` through the differential walk.
6942
7686
  */
6943
- const createTimeline = (options = {}) => {
7687
+ const createDemoTimeline = (options = {}) => {
6944
7688
  const initialExpanded = (options.defaultExpanded ?? []).reduce((mask, node) => setBit(mask, node, true), 0);
6945
7689
  const [expanded, setExpanded] = createSignal(initialExpanded);
6946
7690
  const [selected, requestSelected] = createControllable({
@@ -7095,6 +7839,172 @@ const createTimeline = (options = {}) => {
7095
7839
  }
7096
7840
  };
7097
7841
  };
7842
+ /** A zero-length span still paints: at least half a percent of the track. */
7843
+ const MIN_BAR_WIDTH = .5;
7844
+ /** Aim for about five ticks, whatever the extent. */
7845
+ const TICK_TARGET = 5;
7846
+ const percent = (value) => `${Math.round(value * 1e3) / 1e3}%`;
7847
+ /** 1, 2 or 5 times a power of ten, the step that makes the fewest labels fit. */
7848
+ const niceStep = (span) => {
7849
+ const raw = span / TICK_TARGET;
7850
+ const magnitude = 10 ** Math.floor(Math.log10(raw));
7851
+ const normalized = raw / magnitude;
7852
+ return (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
7853
+ };
7854
+ const formatTick = (value, step, unit) => {
7855
+ const decimals = step >= 1 ? 0 : Math.min(6, Math.ceil(-Math.log10(step)));
7856
+ return `${value.toFixed(decimals)} ${unit}`;
7857
+ };
7858
+ /**
7859
+ * The rows mode: real span data on one axis. Everything hangs off `options.rows`
7860
+ * read at attribute-read time, so the host may pass a getter and a trace may
7861
+ * arrive after mount.
7862
+ */
7863
+ const createRowsTimeline = (options) => {
7864
+ const rows = () => options.rows;
7865
+ const rowOf = (id) => {
7866
+ const row = rows().find((candidate) => candidate.id === id);
7867
+ if (row === void 0) throw new Error(`createTimeline: no row ${id}`);
7868
+ return row;
7869
+ };
7870
+ const [selectedId, requestSelected] = createControllable({
7871
+ value: () => options.selected,
7872
+ initial: void 0,
7873
+ onChange: () => (id) => {
7874
+ if (id !== void 0) options.onSelect?.(id);
7875
+ }
7876
+ });
7877
+ const [focus, setFocus] = createSignal(void 0);
7878
+ const currentFocus = () => focus() ?? rows()[0]?.id;
7879
+ const select = (id) => {
7880
+ requestSelected(id);
7881
+ };
7882
+ const extent = () => {
7883
+ let start = Number.POSITIVE_INFINITY;
7884
+ let end = Number.NEGATIVE_INFINITY;
7885
+ for (const row of rows()) {
7886
+ start = Math.min(start, row.start);
7887
+ end = Math.max(end, row.end);
7888
+ }
7889
+ return start <= end ? {
7890
+ start,
7891
+ end
7892
+ } : {
7893
+ start: 0,
7894
+ end: 0
7895
+ };
7896
+ };
7897
+ const ticks = () => {
7898
+ if (rows().length === 0) return [];
7899
+ const { start, end } = extent();
7900
+ const unit = options.axis?.unit ?? "ms";
7901
+ const span = end - start;
7902
+ if (!(span > 0)) return [{
7903
+ value: start,
7904
+ left: 0,
7905
+ label: formatTick(start, 1, unit)
7906
+ }];
7907
+ const step = niceStep(span);
7908
+ const out = [];
7909
+ for (let value = Math.ceil(start / step) * step; value <= end; value += step) out.push({
7910
+ value,
7911
+ left: (value - start) / span * 100,
7912
+ label: formatTick(value, step, unit)
7913
+ });
7914
+ return out;
7915
+ };
7916
+ const barStyle = (id) => {
7917
+ const row = rowOf(id);
7918
+ const depth = String(row.depth ?? 0);
7919
+ const { start, end } = extent();
7920
+ const span = end - start;
7921
+ if (!(span > 0)) return {
7922
+ left: "0%",
7923
+ width: percent(MIN_BAR_WIDTH),
7924
+ "--depth": depth
7925
+ };
7926
+ const left = Math.max(0, (row.start - start) / span * 100);
7927
+ const width = Math.max(MIN_BAR_WIDTH, (row.end - row.start) / span * 100);
7928
+ return {
7929
+ left: percent(left),
7930
+ width: percent(width),
7931
+ "--depth": depth
7932
+ };
7933
+ };
7934
+ const onKeyDown = (id, event) => {
7935
+ const list = rows();
7936
+ const index = list.findIndex((row) => row.id === id);
7937
+ switch (event.key) {
7938
+ case "ArrowDown":
7939
+ setFocus(list[Math.min(index + 1, list.length - 1)]?.id);
7940
+ break;
7941
+ case "ArrowUp":
7942
+ setFocus(list[Math.max(index - 1, 0)]?.id);
7943
+ break;
7944
+ case "Home":
7945
+ setFocus(list[0]?.id);
7946
+ break;
7947
+ case "End":
7948
+ setFocus(list[list.length - 1]?.id);
7949
+ break;
7950
+ case "Enter":
7951
+ case " ":
7952
+ select(id);
7953
+ break;
7954
+ default: return;
7955
+ }
7956
+ event.preventDefault();
7957
+ };
7958
+ return {
7959
+ extent,
7960
+ depthOf: (id) => rowOf(id).depth ?? 0,
7961
+ listProps: {
7962
+ role: "list",
7963
+ "aria-label": "Span timeline"
7964
+ },
7965
+ rowProps: (id) => ({
7966
+ role: "listitem",
7967
+ get "aria-level"() {
7968
+ return (rowOf(id).depth ?? 0) + 1;
7969
+ },
7970
+ get "aria-current"() {
7971
+ return selectedId() === id ? "true" : void 0;
7972
+ },
7973
+ get "data-status"() {
7974
+ return rowOf(id).status ?? "unset";
7975
+ },
7976
+ get tabindex() {
7977
+ return currentFocus() === id ? 0 : -1;
7978
+ },
7979
+ onClick: () => {
7980
+ setFocus(id);
7981
+ select(id);
7982
+ },
7983
+ onKeyDown: (event) => onKeyDown(id, event)
7984
+ }),
7985
+ barProps: (id) => ({
7986
+ "aria-hidden": "true",
7987
+ get style() {
7988
+ return barStyle(id);
7989
+ }
7990
+ }),
7991
+ labelProps: (id) => ({ get style() {
7992
+ return { "--depth": String(rowOf(id).depth ?? 0) };
7993
+ } }),
7994
+ rulerProps: {
7995
+ role: "presentation",
7996
+ "aria-hidden": "true",
7997
+ get ticks() {
7998
+ return ticks();
7999
+ }
8000
+ },
8001
+ select,
8002
+ selectedId
8003
+ };
8004
+ };
8005
+ function createTimeline(options = {}) {
8006
+ return "rows" in options ? createRowsTimeline(options) : createDemoTimeline(options);
8007
+ }
7098
8008
  // src/toast.ts
7099
8009
  const DEFAULT_TIMEOUT_MS = 5e3;
7100
8010
  const DEFAULT_MAX = 3;
@@ -8825,4 +9735,4 @@ const createRearrangeableListbox = (options) => {
8825
9735
  })
8826
9736
  };
8827
9737
  };
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 };
9738
+ 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, createCommandPalette, createConfirm, createDialogModal, createDisclosure, createDrawer, createDropZone, createFeed, createField, createGrid, createHotkeys, createKnob, 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 };