@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 +36 -2
- package/dist/index.d.mts +95 -0
- package/dist/index.mjs +294 -10
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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": {
|