@cssints/headless 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ A primitive that has a seam honours an optional `prefs` entry (a `Partial<Prefs>
65
65
  - **menu / menubar** — `typeaheadDelayMs` (the typeahead buffer timeout).
66
66
  - **tooltip** — `tooltipShowDelayMs` / `tooltipHideDelayMs`, exposed as `showDelayMs()` / `hideDelayMs()` for the host to wire its `setTimeout` from (the spec keeps timers as events).
67
67
 
68
- Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, dialog-modal, disclosure, feed, grid, link, listbox, meter, player, radio, slider, slider-multithumb, spinbutton, switch, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
68
+ Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, dialog-modal, disclosure, feed, grid, link, listbox, meter, player, radio, resizable-panels, slider, slider-multithumb, spinbutton, switch, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
69
69
 
70
70
  `motionDurationMs` is **not consumed**: canvas tweens are tick-driven, not millisecond-driven, and the look layer owns durations. It stays in `Prefs` only so a host's preference record fits the type.
71
71
 
@@ -89,6 +89,40 @@ Membership, sizes and z-order are **not modelled** in `pointer.qnt` (its node se
89
89
 
90
90
  **Nested canvases** are two instances the host composes: a `#canvas` node holds a second `createCanvas`, and the inner viewport stops propagation of its own pointer, wheel and key events. The innermost canvas under the pointer owns the event, so a press or zoom inside it never starts an outer gesture or moves the outer camera. A nested canvas is a single tab stop in its parent.
91
91
 
92
+ ## Resizable panels
93
+
94
+ `createResizablePanels` is a group of N panes and N-1 separators: react-resizable-panels' layout on the APG window splitter's keyboard. Its behaviour is `packages/apg-specs/specs/patterns/resizable-panels.qnt` — not an APG pattern, so it lives under `patterns/` — and the DOM is checked against it by the differential walk. The spec is the three-pane instance (10/80 bounds, 10% steps, pane 0 collapsible at 0); the primitive takes N panes and any bounds.
95
+
96
+ **The contract.** Sizes are percentages of the group and always sum to 100. Each pane has `min` / `max`; a `collapsible` pane sits at `collapsedSize` (0) while collapsed. A move — an arrow, Home/End, a drag — changes only the two panes around the separator, clamped by both bounds. Collapsing hands the pane's space to the nearest pane that can take it and cascades outward; restoring takes it back in the same order, so a restore always returns the remembered size.
97
+
98
+ **The separator is the widget**: `role=separator`, `tabindex=0`, `aria-orientation`, `aria-valuenow` = the size of the pane it resizes, `aria-valuemin` / `-max` that pane's bounds, `aria-controls` it. Arrows move by `step` (10%), Home/End jump the pair to its bounds, Enter collapses and restores. A separator next to a collapsed pane is inert (only Enter restores).
99
+
100
+ **The drag is snapshot + delta**: pointerdown remembers the layout, every move re-solves from it, so dragging back past a clamp recovers exactly. It uses pointer capture and ends on pointerup / pointercancel; a host write of `sizes` ends it too. Keyboard input is a no-op while a drag runs.
101
+
102
+ ```tsx
103
+ const panels = createResizablePanels({
104
+ panes: [{ label: "Sidebar", collapsible: true }, { label: "Content" }, { label: "Inspector" }],
105
+ defaultSizes: [20, 40, 40],
106
+ storageKey: "app-layout",
107
+ onSizesChange: (sizes) => save(sizes), // ignored while uncontrolled
108
+ });
109
+
110
+ <div {...panels.groupProps} style={{ display: "flex" }}>
111
+ {[0, 1, 2].map((index) => (
112
+ <>
113
+ <div {...panels.paneProps(index)} style={{ flex: `${panels.sizes()[index]}%` }}>
114
+ …
115
+ </div>
116
+ {index < 2 && <div {...panels.separatorProps(index)} />}
117
+ </>
118
+ ))}
119
+ </div>;
120
+ ```
121
+
122
+ Controlled is `sizes` + `onSizesChange` (the request), like every other primitive. `storageKey` persists the layout in localStorage — host storage, so the key belongs to the host's namespace.
123
+
124
+ Left to the host: pane bounds in pixels (percent only), nested groups, and F6 (with three panes a Tab is enough). Not modelled: react-resizable-panels' collapse-on-drag threshold (a pane collapses on Enter or a host write, never by dragging through MIN) and restoring a pane that was mounted already collapsed (it returns to its own `min`).
125
+
92
126
  ## Commands
93
127
 
94
128
  ```sh
@@ -104,7 +138,7 @@ The specs, bridges and scenarios the tests replay are [`@cssints/apg-specs`](../
104
138
 
105
139
  ## Status
106
140
 
107
- Not on npm yet: the package is in the Changesets `ignore` list until it joins the `fixed` group (RELEASING.md). Its manifest follows the other packages (`development` condition on `src`, `dist` for consumers, `publishConfig.directory`).
141
+ Published with the Changesets `fixed` group (one version with the other packages, RELEASING.md). Its manifest follows the other packages (`development` condition on `src`, `dist` for consumers, `publishConfig.directory`).
108
142
 
109
143
  Solid 2 reminders: setters apply on the microtask flush (the DOM driver waits a frame after each step); pass props objects/getters, never destructure options.
110
144
 
package/dist/index.d.mts CHANGED
@@ -2076,6 +2076,101 @@ export interface RadioGroup<T = unknown> {
2076
2076
  */
2077
2077
  export declare const createRadioGroup: <T = unknown>(options: RadioGroupOptions<T>) => RadioGroup<T>;
2078
2078
  //#endregion
2079
+ //#region src/resizable-panels.d.ts
2080
+ export type PanelsOrientation = "vertical" | "horizontal";
2081
+ /** One pane of the group. Sizes are percentages of the group (0..100). */
2082
+ export interface ResizablePaneOptions {
2083
+ readonly id?: string;
2084
+ /** Smallest expanded size, in percent. */
2085
+ readonly min?: number;
2086
+ /** Largest size, in percent. */
2087
+ readonly max?: number;
2088
+ /** The uncontrolled start; defaults to an equal split. */
2089
+ readonly defaultSize?: number;
2090
+ /** Collapses to `collapsedSize` on Enter and restores the remembered size. */
2091
+ readonly collapsible?: boolean;
2092
+ readonly collapsedSize?: number;
2093
+ /** Accessible name fragment: the separator becomes "Resize <label>". */
2094
+ readonly label?: string;
2095
+ }
2096
+ /** Options are read lazily: pass an object with getters, never destructured values. */
2097
+ export interface ResizablePanelsOptions {
2098
+ readonly panes: ReadonlyArray<ResizablePaneOptions>;
2099
+ readonly orientation?: PanelsOrientation;
2100
+ /** Controlled: the host owns the layout. A new array per change. */
2101
+ readonly sizes?: ReadonlyArray<number> | undefined;
2102
+ /** The uncontrolled start; ignored while `sizes` is set. */
2103
+ readonly defaultSizes?: ReadonlyArray<number>;
2104
+ /** Keyboard step, in percent. */
2105
+ readonly step?: number;
2106
+ /** Base for the generated group / pane / separator ids. */
2107
+ readonly id?: string;
2108
+ /** Accessible name of the group. */
2109
+ readonly label?: string | undefined;
2110
+ /** Persist the layout in localStorage under this key (host storage, not modelled). */
2111
+ readonly storageKey?: string | undefined;
2112
+ /** A request to change the layout the host may ignore. */
2113
+ readonly onSizesChange?: (sizes: ReadonlyArray<number>) => void;
2114
+ }
2115
+ export interface ResizablePanelsGroupProps {
2116
+ readonly role: "group";
2117
+ readonly "aria-label": string | undefined;
2118
+ }
2119
+ export interface ResizablePaneProps {
2120
+ readonly id: string;
2121
+ }
2122
+ /** Spread onto the focusable `role=separator` bar between pane `index` and `index + 1`. */
2123
+ export interface ResizableSeparatorProps {
2124
+ readonly id: string;
2125
+ readonly role: "separator";
2126
+ readonly "aria-orientation": PanelsOrientation;
2127
+ readonly "aria-valuenow": number;
2128
+ readonly "aria-valuemin": number;
2129
+ readonly "aria-valuemax": number;
2130
+ /** The pane this separator resizes (the one on its left). */
2131
+ readonly "aria-controls": string;
2132
+ readonly "aria-label": string;
2133
+ readonly tabindex: 0;
2134
+ readonly onKeyDown: (event: KeyboardEvent) => void;
2135
+ readonly onFocus: () => void;
2136
+ readonly onBlur: () => void;
2137
+ readonly onPointerDown: (event: PointerEvent) => void;
2138
+ readonly onPointerMove: (event: PointerEvent) => void;
2139
+ readonly onPointerUp: (event: PointerEvent) => void;
2140
+ readonly onPointerCancel: (event: PointerEvent) => void;
2141
+ }
2142
+ export interface ResizablePanels {
2143
+ /** The committed layout, in percent; always sums to 100. */
2144
+ readonly sizes: Accessor<ReadonlyArray<number>>;
2145
+ readonly collapsed: (index: number) => boolean;
2146
+ /** The separator being dragged, or `null`. */
2147
+ readonly resizing: Accessor<number | null>;
2148
+ readonly focusSeparator: Accessor<number | null>;
2149
+ readonly setFocusSeparator: (index: number | null) => void;
2150
+ readonly keyDown: (index: number, key: string) => void;
2151
+ readonly groupProps: ResizablePanelsGroupProps;
2152
+ readonly paneProps: (index: number) => ResizablePaneProps;
2153
+ readonly separatorProps: (index: number) => ResizableSeparatorProps;
2154
+ }
2155
+ /**
2156
+ * A group of N panes and N-1 separators, react-resizable-panels' model on the
2157
+ * APG window splitter's keyboard: sizes in percent of the group, always summing
2158
+ * to 100, each pane within `min..max`, a collapsible pane landing on
2159
+ * `collapsedSize`. The separator is the focusable widget (`role=separator`,
2160
+ * its `aria-valuenow` is the pane on its left); arrows/Home/End move the pair
2161
+ * around a separator and Enter collapses / restores, all as requests.
2162
+ *
2163
+ * A move only ever changes the separator's two neighbours, clamped by both
2164
+ * bounds. Collapsing hands the pane's space to the nearest pane that can take
2165
+ * it and cascades outward; restoring takes it back in the same order. The drag
2166
+ * is snapshot + delta: pointerdown remembers the layout, every move re-solves
2167
+ * from it, so dragging back past a clamp recovers exactly.
2168
+ *
2169
+ * Behaviour is `packages/apg-specs/specs/patterns/resizable-panels.qnt`; the
2170
+ * DOM is checked against it by the differential walk.
2171
+ */
2172
+ export declare const createResizablePanels: (options: ResizablePanelsOptions) => ResizablePanels;
2173
+ //#endregion
2079
2174
  //#region src/slider.d.ts
2080
2175
  export type SliderOrientation = "horizontal" | "vertical";
2081
2176
  export type SliderDirection = "ltr" | "rtl";
package/dist/index.mjs CHANGED
@@ -131,7 +131,7 @@ const DEFAULT_VIEWPORT = {
131
131
  width: 800,
132
132
  height: 600
133
133
  };
134
- const DEFAULT_STEP = 10;
134
+ const DEFAULT_STEP$1 = 10;
135
135
  const DEFAULT_SHIFT_MULTIPLIER = 5;
136
136
  const PRIMARY_BUTTON = 0;
137
137
  const DEFAULT_MESSAGES = {
@@ -761,7 +761,7 @@ const createCanvas = (options) => {
761
761
  const arrow = ARROWS.get(event.key);
762
762
  if (arrow !== void 0) {
763
763
  event.preventDefault();
764
- const step = (options.keyboardStep ?? DEFAULT_STEP) * (event.shiftKey ? options.keyboardShiftMultiplier ?? DEFAULT_SHIFT_MULTIPLIER : 1);
764
+ const step = (options.keyboardStep ?? DEFAULT_STEP$1) * (event.shiftKey ? options.keyboardShiftMultiplier ?? DEFAULT_SHIFT_MULTIPLIER : 1);
765
765
  moveFrom(index, arrow[0] * step, arrow[1] * step, arrow[2]);
766
766
  } else if (event.key === "Enter") {
767
767
  event.preventDefault();
@@ -982,8 +982,9 @@ const createControllable = (options) => {
982
982
  version();
983
983
  return options.value() ?? local;
984
984
  };
985
+ const same = options.same ?? Object.is;
985
986
  const request = (next) => {
986
- if (next === current()) return;
987
+ if (same(next, current())) return;
987
988
  if (options.value() === void 0) {
988
989
  local = next;
989
990
  setVersion((count) => count + 1);
@@ -1052,7 +1053,7 @@ const RATE_PERCENTS = [
1052
1053
  const SURFACE_SEEK_SECONDS = 5;
1053
1054
  const SEEK_STEP_SECONDS = 1;
1054
1055
  const SEEK_PAGE_SECONDS = 15;
1055
- const clamp$1 = (value, max) => value < 0 ? 0 : value > max ? max : value;
1056
+ const clamp$2 = (value, max) => value < 0 ? 0 : value > max ? max : value;
1056
1057
  const mss = (seconds) => `${Math.floor(seconds / 60)}:${String(Math.floor(seconds) % 60).padStart(2, "0")}`;
1057
1058
  /**
1058
1059
  * APG media player primitive on Solid 2: playback state + attribute objects.
@@ -1089,7 +1090,7 @@ const createPlayer = (options) => {
1089
1090
  const isActive = () => stage() === "playing" || stage() === "buffering";
1090
1091
  const canPlay = () => stage() === "ready" || stage() === "paused" || stage() === "ended" || stage() === "buffering";
1091
1092
  const moveTo = (seconds) => {
1092
- setPosition(clamp$1(seconds, duration()));
1093
+ setPosition(clamp$2(seconds, duration()));
1093
1094
  };
1094
1095
  const play = () => {
1095
1096
  if (!canPlay()) return;
@@ -1110,14 +1111,14 @@ const createPlayer = (options) => {
1110
1111
  };
1111
1112
  const commit = (seconds) => {
1112
1113
  if (!known()) return;
1113
- const target = clamp$1(seconds, duration());
1114
+ const target = clamp$2(seconds, duration());
1114
1115
  if (stage() === "ended") setStage("paused");
1115
1116
  moveTo(target);
1116
1117
  options.onSeek?.(target);
1117
1118
  options.adapter.seek(target);
1118
1119
  };
1119
1120
  const setVolume = (value) => {
1120
- requestVolume(clamp$1(value, 100));
1121
+ requestVolume(clamp$2(value, 100));
1121
1122
  };
1122
1123
  const toggleMute = () => {
1123
1124
  requestMuted(!muted());
@@ -4249,7 +4250,7 @@ const createMenubar = (options) => {
4249
4250
  };
4250
4251
  //#endregion
4251
4252
  //#region src/meter.ts
4252
- const clamp = (value, lo, hi) => value < lo ? lo : value > hi ? hi : value;
4253
+ const clamp$1 = (value, lo, hi) => value < lo ? lo : value > hi ? hi : value;
4253
4254
  /**
4254
4255
  * APG Meter (https://www.w3.org/WAI/ARIA/apg/patterns/meter/): a graphical
4255
4256
  * display of a numeric value within a range. Platform: native (`<meter>`), so
@@ -4262,7 +4263,7 @@ const createMeter = (options = {}) => {
4262
4263
  const value = () => {
4263
4264
  const lo = min();
4264
4265
  const hi = max();
4265
- return hi <= lo ? lo : clamp(options.value ?? 0, lo, hi);
4266
+ return hi <= lo ? lo : clamp$1(options.value ?? 0, lo, hi);
4266
4267
  };
4267
4268
  const percent = () => {
4268
4269
  const width = span();
@@ -4484,6 +4485,289 @@ const createRadioGroup = (options) => {
4484
4485
  };
4485
4486
  };
4486
4487
  //#endregion
4488
+ //#region src/resizable-panels.ts
4489
+ const DEFAULT_MIN = 10;
4490
+ const DEFAULT_MAX = 80;
4491
+ const DEFAULT_STEP = 10;
4492
+ const FULL = 100;
4493
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
4494
+ /** Percentages are reported at a hundredth: a pointer delta is never exact. */
4495
+ const round2 = (value) => Math.round(value * 100) / 100;
4496
+ const sameSizes = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
4497
+ const KEY_TAGS$1 = /* @__PURE__ */ new Set([
4498
+ "ArrowLeft",
4499
+ "ArrowRight",
4500
+ "ArrowUp",
4501
+ "ArrowDown",
4502
+ "Home",
4503
+ "End",
4504
+ "Enter"
4505
+ ]);
4506
+ const capture = (element, pointerId) => {
4507
+ try {
4508
+ element.setPointerCapture(pointerId);
4509
+ } catch {}
4510
+ };
4511
+ const release = (element, pointerId) => {
4512
+ try {
4513
+ element.releasePointerCapture(pointerId);
4514
+ } catch {}
4515
+ };
4516
+ const readStored = (key, count) => {
4517
+ if (typeof localStorage === "undefined") return;
4518
+ try {
4519
+ const raw = localStorage.getItem(key);
4520
+ if (raw === null) return;
4521
+ const parsed = JSON.parse(raw);
4522
+ if (Array.isArray(parsed) && parsed.length === count && parsed.every((value) => typeof value === "number" && Number.isFinite(value))) return parsed;
4523
+ } catch {}
4524
+ };
4525
+ const writeStored = (key, sizes) => {
4526
+ if (typeof localStorage === "undefined") return;
4527
+ try {
4528
+ localStorage.setItem(key, JSON.stringify(sizes));
4529
+ } catch {}
4530
+ };
4531
+ /**
4532
+ * A group of N panes and N-1 separators, react-resizable-panels' model on the
4533
+ * APG window splitter's keyboard: sizes in percent of the group, always summing
4534
+ * to 100, each pane within `min..max`, a collapsible pane landing on
4535
+ * `collapsedSize`. The separator is the focusable widget (`role=separator`,
4536
+ * its `aria-valuenow` is the pane on its left); arrows/Home/End move the pair
4537
+ * around a separator and Enter collapses / restores, all as requests.
4538
+ *
4539
+ * A move only ever changes the separator's two neighbours, clamped by both
4540
+ * bounds. Collapsing hands the pane's space to the nearest pane that can take
4541
+ * it and cascades outward; restoring takes it back in the same order. The drag
4542
+ * is snapshot + delta: pointerdown remembers the layout, every move re-solves
4543
+ * from it, so dragging back past a clamp recovers exactly.
4544
+ *
4545
+ * Behaviour is `packages/apg-specs/specs/patterns/resizable-panels.qnt`; the
4546
+ * DOM is checked against it by the differential walk.
4547
+ */
4548
+ const createResizablePanels = (options) => {
4549
+ const panes = () => options.panes;
4550
+ const count = () => panes().length;
4551
+ const orientation = () => options.orientation ?? "vertical";
4552
+ const step = () => options.step ?? DEFAULT_STEP;
4553
+ const base = options.id ?? createUniqueId();
4554
+ const paneId = (index) => panes()[index]?.id ?? `${base}-pane-${index}`;
4555
+ const collapsedSize = (index) => panes()[index]?.collapsible === true ? panes()[index]?.collapsedSize ?? 0 : 0;
4556
+ /** The smallest expanded size (a move never collapses through it). */
4557
+ const minOf = (index) => panes()[index]?.min ?? DEFAULT_MIN;
4558
+ /** The smallest committed size (a collapsible pane may sit at collapsedSize). */
4559
+ const lowOf = (index) => panes()[index]?.collapsible === true ? collapsedSize(index) : minOf(index);
4560
+ const highOf = (index) => panes()[index]?.max ?? DEFAULT_MAX;
4561
+ /** Clamp every pane, then push the residual into the panes in order. */
4562
+ const fit = (raw) => {
4563
+ const next = panes().map((_, index) => clamp(round2(raw[index] ?? FULL / Math.max(count(), 1)), lowOf(index), highOf(index)));
4564
+ let residual = round2(FULL - next.reduce((sum, value) => sum + value, 0));
4565
+ for (let index = 0; index < next.length && residual !== 0; index += 1) {
4566
+ const room = residual > 0 ? highOf(index) - (next[index] ?? 0) : (next[index] ?? 0) - lowOf(index);
4567
+ const move = residual > 0 ? Math.min(residual, room) : Math.max(residual, -room);
4568
+ next[index] = round2((next[index] ?? 0) + move);
4569
+ residual = round2(residual - move);
4570
+ }
4571
+ return next;
4572
+ };
4573
+ /** Move `amount` percent into pane `index` (negative: out of it), taking from or
4574
+ * giving to its neighbours nearest-first (right, then left), each within bounds. */
4575
+ const shift = (sizes, index, amount) => {
4576
+ const next = [...sizes];
4577
+ let left = round2(amount);
4578
+ const order = [];
4579
+ for (let other = index + 1; other < next.length; other += 1) order.push(other);
4580
+ for (let other = index - 1; other >= 0; other -= 1) order.push(other);
4581
+ for (const other of order) {
4582
+ if (left === 0) break;
4583
+ const value = next[other] ?? 0;
4584
+ const room = left > 0 ? Math.max(0, value - minOf(other)) : Math.max(0, highOf(other) - value);
4585
+ const move = left > 0 ? Math.min(left, room) : Math.max(left, -room);
4586
+ next[other] = round2(value - move);
4587
+ left = round2(left - move);
4588
+ }
4589
+ next[index] = round2((next[index] ?? 0) + (round2(amount) - left));
4590
+ return next;
4591
+ };
4592
+ /** Move the boundary between pane `index` and `index + 1` by `delta`. */
4593
+ const movePair = (sizes, index, delta) => {
4594
+ const a = sizes[index] ?? 0;
4595
+ const b = sizes[index + 1] ?? 0;
4596
+ const low = Math.max(minOf(index), round2(a + b - highOf(index + 1)));
4597
+ const high = Math.min(highOf(index), round2(a + b - minOf(index + 1)));
4598
+ const nextA = round2(clamp(a + delta, low, high));
4599
+ const next = [...sizes];
4600
+ next[index] = nextA;
4601
+ next[index + 1] = round2(a + b - nextA);
4602
+ return next;
4603
+ };
4604
+ const initialSizes = () => {
4605
+ const stored = options.storageKey === void 0 ? void 0 : readStored(options.storageKey, count());
4606
+ return fit(stored ?? options.defaultSizes ?? panes().map((pane) => pane.defaultSize ?? round2(FULL / Math.max(count(), 1))));
4607
+ };
4608
+ const start = initialSizes();
4609
+ const [sizes, request] = createControllable({
4610
+ value: () => options.sizes,
4611
+ initial: start,
4612
+ onChange: () => options.onSizesChange,
4613
+ same: sameSizes
4614
+ });
4615
+ const [focusSeparator, setFocusSeparator] = createSignal(null);
4616
+ const [resizing, setResizing] = createSignal(null);
4617
+ const lastExpanded = start.map((value, index) => Math.max(value, minOf(index)));
4618
+ createEffect(() => sizes(), (now) => {
4619
+ now.forEach((value, index) => {
4620
+ if (value > collapsedSize(index)) lastExpanded[index] = value;
4621
+ });
4622
+ });
4623
+ const collapsed = (index) => panes()[index]?.collapsible === true && (sizes()[index] ?? 0) <= collapsedSize(index);
4624
+ const movable = (index) => index >= 0 && index + 1 < count() && !collapsed(index) && !collapsed(index + 1);
4625
+ let asked;
4626
+ const commit = (next) => {
4627
+ const fitted = fit(next);
4628
+ asked = fitted;
4629
+ request(fitted);
4630
+ };
4631
+ const move = (index, delta) => {
4632
+ if (drag !== void 0 || !movable(index)) return;
4633
+ commit(movePair(sizes(), index, delta));
4634
+ };
4635
+ const toggle = (index) => {
4636
+ const pane = panes()[index];
4637
+ if (drag !== void 0 || pane?.collapsible !== true) return;
4638
+ if (collapsed(index)) commit(shift(sizes(), index, (lastExpanded[index] ?? lowOf(index)) - (sizes()[index] ?? 0)));
4639
+ else commit(shift(sizes(), index, collapsedSize(index) - (sizes()[index] ?? 0)));
4640
+ };
4641
+ const keyDown = (index, key) => {
4642
+ if (!KEY_TAGS$1.has(key)) return;
4643
+ const vertical = orientation() === "vertical";
4644
+ switch (key) {
4645
+ case "ArrowLeft":
4646
+ if (vertical) move(index, 0 - step());
4647
+ break;
4648
+ case "ArrowRight":
4649
+ if (vertical) move(index, step());
4650
+ break;
4651
+ case "ArrowUp":
4652
+ if (!vertical) move(index, 0 - step());
4653
+ break;
4654
+ case "ArrowDown":
4655
+ if (!vertical) move(index, step());
4656
+ break;
4657
+ case "Home":
4658
+ move(index, -100);
4659
+ break;
4660
+ case "End":
4661
+ move(index, FULL);
4662
+ break;
4663
+ default: toggle(index);
4664
+ }
4665
+ };
4666
+ let drag;
4667
+ const axisOf = (event, axis) => axis === "x" ? event.clientX : event.clientY;
4668
+ const lengthOf = (element) => {
4669
+ const group = element.parentElement;
4670
+ if (group === null) return 0;
4671
+ const rect = group.getBoundingClientRect();
4672
+ return orientation() === "vertical" ? rect.width : rect.height;
4673
+ };
4674
+ const onPointerDown = (index, event) => {
4675
+ if (event.button !== 0) return;
4676
+ if (drag !== void 0) {
4677
+ release(drag.element, drag.pointerId);
4678
+ drag = void 0;
4679
+ setResizing(null);
4680
+ }
4681
+ setFocusSeparator(index);
4682
+ if (!movable(index)) return;
4683
+ const length = lengthOf(event.currentTarget);
4684
+ if (length <= 0) return;
4685
+ const axis = orientation() === "vertical" ? "x" : "y";
4686
+ drag = {
4687
+ separator: index,
4688
+ element: event.currentTarget,
4689
+ pointerId: event.pointerId,
4690
+ start: axisOf(event, axis),
4691
+ axis,
4692
+ from: sizes(),
4693
+ length
4694
+ };
4695
+ setResizing(index);
4696
+ capture(event.currentTarget, event.pointerId);
4697
+ event.preventDefault();
4698
+ };
4699
+ const onPointerMove = (event) => {
4700
+ if (drag === void 0 || drag.pointerId !== event.pointerId) return;
4701
+ const delta = (axisOf(event, drag.axis) - drag.start) / drag.length * FULL;
4702
+ commit(movePair(drag.from, drag.separator, delta));
4703
+ };
4704
+ const onPointerEnd = (event) => {
4705
+ if (drag === void 0 || drag.pointerId !== event.pointerId) return;
4706
+ drag = void 0;
4707
+ setResizing(null);
4708
+ release(event.currentTarget, event.pointerId);
4709
+ };
4710
+ createEffect(() => options.sizes, (now) => {
4711
+ if (now === void 0 || asked !== void 0 && sameSizes(now, asked)) return;
4712
+ drag = void 0;
4713
+ setResizing(null);
4714
+ });
4715
+ if (options.storageKey !== void 0) {
4716
+ const storageKey = options.storageKey;
4717
+ createEffect(() => sizes(), (now) => writeStored(storageKey, now));
4718
+ }
4719
+ const separatorProps = (index) => ({
4720
+ id: `${base}-separator-${index}`,
4721
+ role: "separator",
4722
+ get "aria-orientation"() {
4723
+ return orientation();
4724
+ },
4725
+ get "aria-valuenow"() {
4726
+ return round2(sizes()[index] ?? 0);
4727
+ },
4728
+ get "aria-valuemin"() {
4729
+ return panes()[index]?.min ?? DEFAULT_MIN;
4730
+ },
4731
+ get "aria-valuemax"() {
4732
+ return highOf(index);
4733
+ },
4734
+ get "aria-controls"() {
4735
+ return paneId(index);
4736
+ },
4737
+ get "aria-label"() {
4738
+ const label = panes()[index]?.label;
4739
+ return label === void 0 ? "Resize" : `Resize ${label}`;
4740
+ },
4741
+ tabindex: 0,
4742
+ onKeyDown: (event) => {
4743
+ if (KEY_TAGS$1.has(event.key)) event.preventDefault();
4744
+ keyDown(index, event.key);
4745
+ },
4746
+ onFocus: () => setFocusSeparator(index),
4747
+ onBlur: () => setFocusSeparator(null),
4748
+ onPointerDown: (event) => onPointerDown(index, event),
4749
+ onPointerMove,
4750
+ onPointerUp: onPointerEnd,
4751
+ onPointerCancel: onPointerEnd
4752
+ });
4753
+ return {
4754
+ sizes,
4755
+ collapsed,
4756
+ resizing,
4757
+ focusSeparator,
4758
+ setFocusSeparator,
4759
+ keyDown,
4760
+ groupProps: {
4761
+ role: "group",
4762
+ get "aria-label"() {
4763
+ return options.label;
4764
+ }
4765
+ },
4766
+ paneProps: (index) => ({ id: paneId(index) }),
4767
+ separatorProps
4768
+ };
4769
+ };
4770
+ //#endregion
4487
4771
  //#region src/slider.ts
4488
4772
  const valueTextFor$1 = (value) => value <= 3 ? "low" : value <= 7 ? "medium" : "high";
4489
4773
  /**
@@ -6991,4 +7275,4 @@ const createRearrangeableListbox = (options) => {
6991
7275
  };
6992
7276
  };
6993
7277
  //#endregion
6994
- export { 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, createDialogModal, createDisclosure, createFeed, createGrid, createLink, createListbox, createMenu, createMenubar, createMeter, createPlayer, createRadioGroup, createRearrangeableListbox, createSlider, createSliderMultiThumb, createSpinbutton, createSwitch, createTabs, createTimeline, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
7278
+ export { 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, createDialogModal, createDisclosure, createFeed, createGrid, createLink, createListbox, createMenu, createMenubar, createMeter, createPlayer, createRadioGroup, createRearrangeableListbox, createResizablePanels, createSlider, createSliderMultiThumb, createSpinbutton, createSwitch, createTabs, createTimeline, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cssints/headless",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Headless APG widgets on Solid 2: state plus attribute objects, no elements, no styling, each conformance-checked against its Quint spec",
5
5
  "license": "MIT",
6
6
  "repository": {