@n-uf/placement 26.11.6 → 26.11.8

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/CHANGELOG.md CHANGED
@@ -3,6 +3,54 @@
3
3
  Versions are calendar versions, `YY.M.R` (year, month of the release, counter within the month), shared with
4
4
  `@n-uf/motion`: both packages carry the same version, and a release bumps both.
5
5
 
6
+ ## 26.11.8
7
+
8
+ - `@n-uf/placement/panels`:
9
+ - Folded floats: a float folded by hand lays out as a title bar (`kind` `"bar"`, `folded` `"hand"`) at its own x, y
10
+ and width, `options.bar` tall, clamped as a bar, pinned or not. It is still a float to targets and drags; a drop
11
+ moves it and keeps its full size, and unfolding brings the full rect back (the stored rect never changes). A
12
+ folded docked panel dropped on the float target stays folded: it lands as a title bar at its natural float size.
13
+ - `options.fold`: `"rail"` (default, as before) or `"ride"`. Riding, a column folded by hand on a side that still
14
+ has an open column becomes title bars stacked on top of the side's first open column (`rides`: its index), as
15
+ wide as it; the open columns lie as if the folded one were gone, and its stored place and width are untouched.
16
+ When every column on the side is folded, the side shows its rail as before. Give-way's folds always go to the rail.
17
+ - `PlacedPanel` gains `floating` (open or folded float) and `rides`; `PlacedColumn` gains `rides`.
18
+ - `pin(…, shown)` takes only the position of `shown` and keeps the stored size, so pinning a folded or clamped
19
+ float keeps its full size.
20
+ - A column too short for its bars gives its open panels a height of 0, never a negative one.
21
+
22
+ ## 26.11.7
23
+
24
+ - New entry `@n-uf/placement/panels`: tool panels with string ids, as an app's side columns work. `./dock` is
25
+ unchanged.
26
+ - Model: `PanelDef` (width limits, float size limits, the sides it may dock on, whether it may float; no title:
27
+ words are the consumer's),
28
+ `PanelsState` (`sides` of full-height `Column`s from the outer edge inward, `floats`, `folded`, `kept`,
29
+ `closed`, a logical `clock` of opened and used), `createPanels(defs, start)`.
30
+ - `layoutPanels(defs, state, stage, options?)`: geometry and structure only. Each panel's rect, layer, `kind`
31
+ (`column`, `bar`, `segment`, `float`), folded, kept and pinned flags; the columns; the rails; the pages' rect;
32
+ notes as a `code` plus data (`column-shrunk`, `column-folded`, `kept-folded`, `pages-under-min`,
33
+ `float-resized`, `float-clamped`). Options: `giveWay` (`{ order: "oldest" | "newest", by: "used" | "opened" }`,
34
+ default oldest by use), `floats` (`"clamp"` or `"free"`), `floatVisible`, `pagesMin`, `view` (the page's offset
35
+ and scale), and the chrome sizes `bar`, `rail`, `segment`, `railInset`.
36
+ - Give-way shrinks every column toward its minimum, then folds whole columns to the rail: unkept columns first, in
37
+ policy order across both sides, then columns holding a kept panel, only when the pages still don't fit. It never
38
+ writes `folded`, so a wider stage brings a column back.
39
+ - Floats clamp at layout time (the state keeps the stored rect) so their screen rect stays in the view at any
40
+ scale: a pinned float where it is, an unpinned one as it shows at pan 0, so it still scrolls with the page.
41
+ - Changes: `open`, `close`, `focus`, `toggleFold`, `keep`, `dock`, `float`, `pin` (with `shown`, a clamped float
42
+ pins where it shows), `resizeColumn`, `resizeFloat`, `liftPanel`. Opening, focusing, moving, resizing and
43
+ unfolding are uses (they move the `used` clock). `samePlaces(a, b, defs?)` compares widths as laid out;
44
+ `columnWidth(defs, column)`.
45
+ - Targets: `panelTargets(input)` (`column:<side>:<i>`, `into:<side>:<i>`, `rail:<side>`, `float`) in the shape
46
+ `@n-uf/motion/drag` takes, each with a structured `place` and no words; a place the panel may not take and its
47
+ own place are no targets. `panelDrop(input, target, point, grab)` lays out a drop. `homePlace`, `panelStops`.
48
+ - `storePanels(state)` / `readPanels(data, defs?)`: a versioned stored form (`{ v: 1, … }`); reading never throws,
49
+ drops unknown or repeated ids and clamps to the defs. Version 1 only gains optional fields: a field the reader
50
+ doesn't know is skipped and listed in `dropped`.
51
+ - `@n-uf/placement/targets`: `keyboardStops` also takes panel targets (with `{ home: { place, state }, floats? }`)
52
+ and gives panel stops in reading order.
53
+
6
54
  ## 26.11.6
7
55
 
8
56
  - `@n-uf/placement/targets`:
package/README.md CHANGED
@@ -3,14 +3,16 @@
3
3
  > [!CAUTION]
4
4
  > ## Not yet stable
5
5
  >
6
- > **@n-uf/placement is young and still changing. Its API (the desk constructions, the toolbar model and the drop targets) is not final.**
6
+ > **@n-uf/placement is young and still changing. Its API (the desk constructions, the panel columns, the toolbar model and the drop targets) is not final.**
7
7
  >
8
8
  > Version numbers are dates (YY.M.R), not semantic versions, so any release may break things. Pin an exact version and read the [CHANGELOG](https://github.com/n-uf/motion/blob/main/packages/placement/CHANGELOG.md) before you upgrade.
9
9
 
10
- Where things go. State in, rects and drop targets out: a desk of docks, rails and palettes, toolbar groups in hosts
11
- and zones, and the drop targets for each. A tiling entry (a split tree with ratios, groups and workspaces) is
12
- planned. Pure functions over plain data: no DOM,
13
- no framework, no motion, no dependencies.
10
+ Site and live demos: [motion.n-uf.com](https://motion.n-uf.com)
11
+
12
+ Where things go. State in, rects and drop targets out: a desk of docks, rails and palettes, panels in columns per
13
+ side that give way by age, toolbar groups in hosts and zones, and the drop targets for each. A tiling entry (a split
14
+ tree with ratios, groups and workspaces) is planned. Pure functions over plain data: no DOM, no framework, no
15
+ motion, no dependencies.
14
16
 
15
17
  ```sh
16
18
  npm i @n-uf/placement
@@ -20,17 +22,21 @@ Its sibling [`@n-uf/motion`](https://www.npmjs.com/package/@n-uf/motion) owns ho
20
22
  without importing each other: a target list from `@n-uf/placement/targets` is exactly what
21
23
  `@n-uf/motion/drag`'s drag intent takes, and a rect from any layout here is what `@n-uf/motion/layout` glides to.
22
24
 
23
- - **`@n-uf/placement`**: the shapes (`Rect`, `Size`, `Point`) and a popup positioner (anchor, gravity, offset,
24
- then flip, slide, resize, as Wayland's xdg-positioner).
25
+ - **`@n-uf/placement`**: the shapes (`Rect`, `Size`, `Point`) and a popup positioner, as Wayland's xdg-positioner: an anchor
26
+ point, gravity and offset, then flip, slide and resize, each only when the rule allows it.
25
27
  - **`/dock`**: a desk of documents and tool panels. Side docks and a bottom dock (between the side docks or across
26
28
  the full width), rails, natural-height sections whose last one takes the rest, fold and unfold, the give-way
27
29
  rules (sections fold least recently used first, docks fold to their rails), floating palettes with corner stacks,
28
30
  tear-off and dock-back; and nine constructions built from these parts, as presets (`createDesk`).
29
31
  - **`/toolbar`**: toolbar groups in five hosts (title, top, bottom, left, right) and three zones (start, centre,
30
- end), floating groups, insertion points (before all, between, after all, an empty zone), trailing-first overflow,
31
- and "own place is home".
32
- - **`/targets`**: drop targets (`{ id, priority, hit, slot?, label?, free? }`) for the desk's constructions and
33
- toolbar hosts.
32
+ end), floating groups, insertion points (before all, between, after all, an empty zone), collapse when a host
33
+ runs out of room (lowest keep first, trailing groups first among equal keeps), and "own place is home".
34
+ - **`/targets`**: drop targets (`{ id, priority, hit, slot?, label?, free? }`) for the desk's constructions,
35
+ toolbar hosts (each with its `slot`, and `allows` to leave out places a group may not take) and pane zones; and
36
+ `keyboardStops`, one ordered list of places for the arrow keys and a "Move to" menu.
37
+ - **`/panels`**: tool panels with string ids: full-height columns per side, each with its own width; folding to a
38
+ title bar or a rail; give-way by age when the pages run out of room; resizable floats, some pinned to the view;
39
+ their drop targets; and a stored form.
34
40
 
35
41
  Rendering stays in the app: it draws the rects, measures its groups, and feeds pointer samples to a drag intent.
36
42
 
@@ -149,6 +155,106 @@ const stops = keyboardStops(targets, { home: { place: hostOf(a, "share")!, arran
149
155
  console.log(stops.map((s) => (s.home ? "home" : s.id))); // [ 'g:top:start:0', 'g:top:start:1', 'g:top:start:2', 'g:top:center:0', 'home' ]
150
156
  ```
151
157
 
158
+ ## Panels
159
+
160
+ `@n-uf/placement/panels` lays out tool panels the way an app's side columns work: each side holds full-height
161
+ columns from the outer edge inward, each column one or more panels and its own width (clamped to its panels'
162
+ `min` and `max`). A panel folded by hand is a title bar; a column whose panels are all folded becomes segments of
163
+ its side's rail, or with `fold: "ride"` bars on the side's first open column. Panels may float instead, in page space
164
+ (they move with the page's offset and scale) or pinned to the view; a folded float is a title bar where it floats, and
165
+ a folded panel torn off stays folded. The output is geometry and structure only: each panel's rect, layer, `kind`
166
+ (`column`, `bar`, `segment`, `float`; a folded float is a `bar`) and its folded, kept, pinned, floating and rides
167
+ flags, the columns, the rails, the pages' rect and coded notes. Chrome sizes
168
+ (`bar`, `rail`, `segment`) are options.
169
+
170
+ ```ts
171
+ import { createPanels, keep, layoutPanels, readPanels, storePanels, type PanelDef } from "@n-uf/placement/panels";
172
+
173
+ const defs: PanelDef[] = [
174
+ { id: "files", width: { min: 180, natural: 220, max: 360 } },
175
+ { id: "outline", width: { min: 160, natural: 200, max: 320 } },
176
+ { id: "notes", width: { min: 200, natural: 260, max: 420 } },
177
+ ];
178
+ // Files was used first, so it is the oldest.
179
+ const state = createPanels(defs, { left: [["files"], ["outline"]], right: [["notes"]] });
180
+ const narrow = layoutPanels(defs, state, { width: 780, height: 600 });
181
+ console.log(narrow.notes.map((n) => n.code), narrow.items.get("files")!.kind); // [ 'column-shrunk', 'column-folded' ] segment
182
+ const kept = layoutPanels(defs, keep(state, "files", true), { width: 780, height: 600 });
183
+ console.log(kept.items.get("files")!.kind, kept.items.get("outline")!.kind); // column segment
184
+ const read = readPanels(JSON.parse(JSON.stringify(storePanels(state))), defs);
185
+ console.log(read.ok); // true
186
+ ```
187
+
188
+ **Give-way** (`options.giveWay`, default `{ order: "oldest", by: "used" }`): when the open columns, the rails and
189
+ `pagesMin` (default 320) don't fit the stage width, columns shrink toward their `min`, then whole columns fold to the
190
+ rail, in the policy's order, until the pages fit. A column's age is its newest panel's (`used` counts opening,
191
+ focusing, moving, resizing and unfolding; `opened` only opening). Ties go to the outer column, then the left side.
192
+ Every column shrinks before any folds; then the unkept columns fold before any column holding a kept panel
193
+ (`keep(state, id, true)`), each group in the policy's order across both sides. When only kept columns are left and the
194
+ pages still don't fit, they fold too. Give-way never writes `folded`, so widening the stage brings a column back.
195
+
196
+ **Floats** (`options.floats`, default `"clamp"`): a float that fits the view stays wholly inside it; one that doesn't
197
+ even at its minimum size keeps its title bar and `floatVisible` (default 48) px of its width in the view. A float's
198
+ size is screen px and never scales. A pinned float is clamped where it is; an unpinned one has its page-space position
199
+ clamped so that its screen rect at pan 0 fits the view, so it still scrolls with the page. The clamp runs at layout
200
+ time and the state keeps the stored rect, so a larger view brings a float back where it was. `"free"` never moves a
201
+ float. A float folded by hand (`toggleFold`) lays out as a title bar: `kind` `"bar"`, `folded` `"hand"`, at its own x,
202
+ y and width and `options.bar` tall, clamped as a bar. It is still a float (`floating: true`) to its targets and drags,
203
+ pinned or not, and unfolding brings its full rect back: the stored rect never changes.
204
+
205
+ **Folded columns** (`options.fold`, default `"rail"`): a column folded by hand goes to its side's rail. With `"ride"`,
206
+ it rides instead while its side still has an open column: its panels become title bars (`kind` `"bar"`, `rides`: the
207
+ host's index) stacked in column order on top of the side's first open column (outer edge first), as wide as it, and
208
+ the host's open panels share the rest of the height. The open columns lie as if the folded one were gone (the pages
209
+ gain its room), and its stored place and width are untouched, so unfolding puts it back as its own column. Only when
210
+ every column on a side is folded does the side show its rail. Give-way's folds (`column-folded`, `kept-folded`) go to
211
+ the rail in both modes.
212
+
213
+ **Changes** are pure: `open`, `close`, `focus`, `toggleFold`, `keep`, `dock(state, id, side, index, { into?, width? })`,
214
+ `float`, `pin(state, id, on, view?, shown?)` (pass the layout's rect as `shown` so a clamped float pins where it
215
+ shows), `resizeColumn`, `resizeFloat`.
216
+
217
+ **Drop targets**: `panelTargets({ defs, state, id, stage, options?, allows? })` gives `column:<side>:<i>` (a new
218
+ column as wide as the panel's own width), `into:<side>:<i>` (stack into a column), `rail:<side>` (dock folded) and
219
+ `float` (free), in the shape `@n-uf/motion/drag`'s `createDragIntent` takes. Each carries a structured `place`, not
220
+ words. A place the panel may not take (its def's `sides` and `floats`, or `allows`) and its own place are no targets.
221
+ `panelDrop(input, target, point, grab)` lays out the drop: its `slot` is where the panel lands. `keyboardStops` from
222
+ `@n-uf/placement/targets` puts panel targets in reading order, with `homePlace(state, id)` as `home`:
223
+
224
+ ```ts
225
+ import { createPanels, homePlace, panelDrop, panelTargets, type PanelDef } from "@n-uf/placement/panels";
226
+ import { keyboardStops } from "@n-uf/placement/targets";
227
+
228
+ const defs: PanelDef[] = [
229
+ { id: "files", width: { min: 180, natural: 220, max: 360 } },
230
+ { id: "outline", width: { min: 160, natural: 200, max: 320 }, floats: false },
231
+ ];
232
+ const state = createPanels(defs, { left: [["files"], ["outline"]] });
233
+ const input = { defs, state, id: "outline", stage: { width: 1200, height: 600 } };
234
+ const targets = panelTargets(input); // hand these to createDragIntent
235
+ console.log(targets.map((t) => t.id)); // [ 'rail:left', 'column:left:0', 'into:left:0', 'column:right:0', 'rail:right' ]
236
+ const drop = panelDrop(input, targets[3]!, { x: 1190, y: 300 }, { x: 0, y: 0 });
237
+ console.log(drop.slot); // { x: 1000, y: 0, width: 200, height: 600 }
238
+ const stops = keyboardStops(targets, { home: { place: homePlace(state, "outline")!, state } });
239
+ console.log(stops.map((s) => (s.home ? "home" : s.id))); // [ 'rail:left', 'column:left:0', 'into:left:0', 'home', 'column:right:0', 'rail:right' ]
240
+ ```
241
+
242
+ `storePanels(state)` gives `{ v: 1, sides, floats, folded, kept, closed, clock }`; `readPanels(data, defs?)` reads
243
+ parsed data back and never throws. Anything but an object of version 1 is `{ ok: false, error }`; inside one, unknown
244
+ or repeated ids and malformed entries are dropped and listed in `dropped`, and widths and float sizes are clamped to
245
+ the defs. Version 1 only gains optional fields: a field this reader doesn't know is skipped and listed in `dropped`
246
+ too (`"theme"`, `"sides.bottom"`, `"column.<field>"`, `"float.<field>"`, `"clock.<id>"`). The note codes
247
+ (`PanelsNoteCode`):
248
+
249
+ | Code | Says | Also |
250
+ |---|---|---|
251
+ | `column-shrunk` | a column narrowed toward its minimum | `side`, `column`, `items`, `from`, `to` |
252
+ | `column-folded` | a column folded to its side's rail | `side`, `column`, `items` |
253
+ | `kept-folded` | a kept column folded: the pages needed the room | `side`, `column`, `items` |
254
+ | `pages-under-min` | the pages are under `pagesMin` with every column folded | `width`, `min` |
255
+ | `float-resized` | a float shrank to fit the view | `items` |
256
+ | `float-clamped` | a float moved to stay in the view | `items` |
257
+
152
258
  ## Pane zones
153
259
 
154
260
  `paneZone(rect, point, previous, options?)` says which of a pane's five zones a point is in: the centre rectangle
@@ -0,0 +1,448 @@
1
+ import {
2
+ rect
3
+ } from "./chunk-QMTNB2C3.js";
4
+
5
+ // src/panels/model.ts
6
+ var SIDES = ["left", "right"];
7
+ var IDENTITY_VIEW = { x: 0, y: 0, scale: 1 };
8
+ var DEFAULT_PANELS_OPTIONS = {
9
+ giveWay: { order: "oldest", by: "used" },
10
+ floats: "clamp",
11
+ fold: "rail",
12
+ floatVisible: 48,
13
+ pagesMin: 320,
14
+ view: IDENTITY_VIEW,
15
+ bar: 28,
16
+ rail: 36,
17
+ segment: 36,
18
+ railInset: 4
19
+ };
20
+ var FLOAT_HEIGHT = 360;
21
+ var FLOAT_MIN = { width: 160, height: 120 };
22
+ var without = (list, x) => list.filter((y) => y !== x);
23
+ var withOnce = (list, x) => list.includes(x) ? [...list] : [...list, x];
24
+ function whereIs(state, id) {
25
+ for (const side of SIDES) {
26
+ const index = state.sides[side].findIndex((c) => c.panels.includes(id));
27
+ if (index >= 0) return { kind: "column", side, index };
28
+ }
29
+ const float2 = state.floats.find((f) => f.id === id);
30
+ return float2 ? { kind: "float", float: float2 } : { kind: "none" };
31
+ }
32
+ var tick = (state) => Object.values(state.clock).reduce((m, c) => Math.max(m, c.opened, c.used), 0) + 1;
33
+ var bump = (state, id, opened) => {
34
+ const t = tick(state);
35
+ const was = state.clock[id] ?? { opened: 0, used: 0 };
36
+ return { ...state, clock: { ...state.clock, [id]: { opened: opened ? t : was.opened, used: t } } };
37
+ };
38
+ var defOf = (defs, id) => defs.find((d) => d.id === id);
39
+ function floatSize(def) {
40
+ if (def?.float) return { min: def.float.min, natural: def.float.natural };
41
+ const width = Math.max(FLOAT_MIN.width, def?.width.natural ?? FLOAT_MIN.width);
42
+ return { min: FLOAT_MIN, natural: { width, height: FLOAT_HEIGHT } };
43
+ }
44
+ function ownWidth(defs, state, id) {
45
+ const def = defOf(defs, id);
46
+ const w = whereIs(state, id);
47
+ const lo = def?.width.min ?? 0;
48
+ const hi = Math.max(lo, def?.width.max ?? Infinity);
49
+ if (w.kind === "column") return Math.min(Math.max(state.sides[w.side][w.index].width, lo), hi);
50
+ return def?.width.natural ?? lo;
51
+ }
52
+ function createPanels(defs, start) {
53
+ const column = (ids) => {
54
+ const ds = ids.map((id) => defOf(defs, id));
55
+ const lo = Math.max(0, ...ds.map((d) => d?.width.min ?? 0));
56
+ const hi = Math.max(lo, Math.min(...ds.map((d) => d?.width.max ?? Infinity)));
57
+ const natural = Math.max(0, ...ds.map((d) => d?.width.natural ?? 0));
58
+ return { panels: [...ids], width: Math.min(Math.max(natural, lo), hi) };
59
+ };
60
+ const floats = (start.floats ?? []).map((f) => {
61
+ const n = floatSize(defOf(defs, f.id)).natural;
62
+ return { id: f.id, rect: { x: f.x, y: f.y, width: f.width ?? n.width, height: f.height ?? n.height }, ...f.pinned ? { pinned: true } : {} };
63
+ });
64
+ const sides = { left: (start.left ?? []).map(column), right: (start.right ?? []).map(column) };
65
+ const order = [...sides.left, ...sides.right].flatMap((c) => c.panels).concat(floats.map((f) => f.id));
66
+ const clock = {};
67
+ order.forEach((id, i) => clock[id] = { opened: i + 1, used: i + 1 });
68
+ return { sides, floats, folded: [...start.folded ?? []], kept: [...start.kept ?? []], closed: [...start.closed ?? []], clock };
69
+ }
70
+ function liftPanel(state, id) {
71
+ const lift = (cols) => cols.map((c) => ({ ...c, panels: without(c.panels, id) })).filter((c) => c.panels.length > 0);
72
+ return { ...state, sides: { left: lift(state.sides.left), right: lift(state.sides.right) }, floats: state.floats.filter((f) => f.id !== id) };
73
+ }
74
+ function open(state, id, at) {
75
+ let s = { ...state, closed: without(state.closed, id) };
76
+ if (at && whereIs(s, id).kind === "none") s = { ...s, sides: { ...s.sides, [at.side]: [...s.sides[at.side], { panels: [id], width: at.width }] } };
77
+ return bump(s, id, true);
78
+ }
79
+ var close = (state, id) => ({ ...state, closed: withOnce(state.closed, id), kept: without(state.kept, id) });
80
+ function focus(state, id) {
81
+ const f = state.floats.find((x) => x.id === id);
82
+ const s = f ? { ...state, floats: [...state.floats.filter((x) => x !== f), f] } : state;
83
+ return bump(s, id, false);
84
+ }
85
+ function toggleFold(state, id) {
86
+ if (state.folded.includes(id)) return bump({ ...state, folded: without(state.folded, id) }, id, false);
87
+ return { ...state, folded: [...state.folded, id] };
88
+ }
89
+ function keep(state, id, on) {
90
+ if (!on) return { ...state, kept: without(state.kept, id) };
91
+ return whereIs(state, id).kind === "column" ? { ...state, kept: withOnce(state.kept, id) } : state;
92
+ }
93
+ function dock(state, id, side, index, options = {}) {
94
+ const w = whereIs(state, id);
95
+ const from = w.kind === "column" ? state.sides[w.side][w.index].width : 0;
96
+ const s = liftPanel(state, id);
97
+ const cols = [...s.sides[side]];
98
+ if (options.into && cols[index]) cols[index] = { ...cols[index], panels: [...cols[index].panels, id] };
99
+ else {
100
+ const at = Math.max(0, Math.min(index, cols.length));
101
+ cols.splice(at, 0, { panels: [id], width: options.width ?? from });
102
+ }
103
+ return bump({ ...s, sides: { ...s.sides, [side]: cols }, closed: without(s.closed, id) }, id, false);
104
+ }
105
+ function float(state, id, rect2, pinned) {
106
+ const was = state.floats.find((f2) => f2.id === id);
107
+ const on = pinned ?? !!was?.pinned;
108
+ const s = liftPanel(state, id);
109
+ const f = { id, rect: { ...rect2 }, ...on ? { pinned: true } : {} };
110
+ return bump({ ...s, floats: [...s.floats, f], kept: without(s.kept, id), closed: without(s.closed, id) }, id, false);
111
+ }
112
+ var toView = (r, v) => ({ ...r, x: v.x + r.x * v.scale, y: v.y + r.y * v.scale });
113
+ var toPage = (r, v) => ({ ...r, x: (r.x - v.x) / v.scale, y: (r.y - v.y) / v.scale });
114
+ function pin(state, id, on, view = IDENTITY_VIEW, shown) {
115
+ return {
116
+ ...state,
117
+ floats: state.floats.map((f) => {
118
+ if (f.id !== id || !!f.pinned === on) return f;
119
+ const seen = shown ? { ...f.rect, x: shown.x, y: shown.y } : f.pinned ? { ...f.rect } : toView(f.rect, view);
120
+ return on ? { id, rect: seen, pinned: true } : { id, rect: toPage(seen, view) };
121
+ })
122
+ };
123
+ }
124
+ function resizeColumn(state, side, index, width) {
125
+ const col = state.sides[side][index];
126
+ if (!col || !Number.isFinite(width)) return state;
127
+ const t = tick(state);
128
+ const clock = { ...state.clock };
129
+ for (const id of col.panels) clock[id] = { opened: clock[id]?.opened ?? 0, used: t };
130
+ return { ...state, sides: { ...state.sides, [side]: state.sides[side].map((c, i) => i === index ? { ...c, width: Math.max(0, width) } : c) }, clock };
131
+ }
132
+ function resizeFloat(state, id, rect2) {
133
+ if (!state.floats.some((f) => f.id === id)) return state;
134
+ return bump({ ...state, floats: state.floats.map((f) => f.id === id ? { ...f, rect: { ...rect2 } } : f) }, id, false);
135
+ }
136
+ function columnWidth(defs, col) {
137
+ if (!defs) return col.width;
138
+ const ds = col.panels.map((id) => defOf(defs, id));
139
+ const lo = Math.max(0, ...ds.map((d) => d?.width.min ?? 0));
140
+ const hi = Math.max(lo, Math.min(...ds.map((d) => d?.width.max ?? Infinity)));
141
+ return Math.min(Math.max(Number.isFinite(col.width) ? col.width : lo, lo), hi);
142
+ }
143
+ function samePlaces(a, b, defs) {
144
+ const cols = (s) => SIDES.map((side) => s.sides[side].map((c) => `${c.panels.join(",")}@${Math.round(columnWidth(defs, c))}`).join("|")).join("\u2016");
145
+ const fl = (s) => s.floats.map((f) => `${f.id}@${Math.round(f.rect.x)},${Math.round(f.rect.y)},${Math.round(f.rect.width)},${Math.round(f.rect.height)}${f.pinned ? "p" : ""}`).join("|");
146
+ const set = (l) => [...l].sort().join(",");
147
+ return cols(a) === cols(b) && fl(a) === fl(b) && set(a.folded) === set(b.folded) && set(a.kept) === set(b.kept) && set(a.closed) === set(b.closed);
148
+ }
149
+
150
+ // src/panels/layout.ts
151
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
152
+ function giveWayOrder(cols, order) {
153
+ return [...cols].sort((a, b) => Number(a.kept) - Number(b.kept) || (order === "oldest" ? a.age - b.age : b.age - a.age) || a.index - b.index || SIDES.indexOf(a.side) - SIDES.indexOf(b.side));
154
+ }
155
+ function layoutPanels(defs, state, stage, options = {}) {
156
+ const opt = { ...DEFAULT_PANELS_OPTIONS, ...options };
157
+ const W = stage.width;
158
+ const H = stage.height;
159
+ const closed = new Set(state.closed);
160
+ const folded = new Set(state.folded);
161
+ const kept = new Set(state.kept);
162
+ const by = opt.giveWay.by;
163
+ const ride = opt.fold === "ride";
164
+ const cols = [];
165
+ for (const side of SIDES) {
166
+ state.sides[side].forEach((c, index) => {
167
+ const panels = c.panels.filter((id) => !closed.has(id));
168
+ if (panels.length === 0) return;
169
+ const ds = panels.map((id) => defOf(defs, id));
170
+ const min = Math.max(0, ...ds.map((d) => d?.width.min ?? 0));
171
+ const max = Math.max(min, Math.min(...ds.map((d) => d?.width.max ?? Infinity)));
172
+ const width = clamp(Number.isFinite(c.width) ? c.width : min, min, max);
173
+ const age = Math.max(...panels.map((id) => state.clock[id]?.[by] ?? 0));
174
+ cols.push({ side, index, panels, min, max, width, hand: panels.every((id) => folded.has(id)), kept: panels.some((id) => kept.has(id)), age });
175
+ });
176
+ }
177
+ const order = giveWayOrder(
178
+ cols.filter((c) => !c.hand),
179
+ opt.giveWay.order
180
+ );
181
+ const fit = (k) => {
182
+ const auto = new Set(order.slice(0, k));
183
+ const open2 = (side) => cols.some((c) => c.side === side && !c.hand && !auto.has(c));
184
+ const railed = (side) => cols.some((c) => c.side === side && (auto.has(c) || c.hand && !(ride && open2(side))));
185
+ const widths = new Map(order.filter((c) => !auto.has(c)).map((c) => [c, c.width]));
186
+ let excess = [...widths.values()].reduce((a, b) => a + b, 0) + SIDES.filter(railed).length * opt.rail + opt.pagesMin - W;
187
+ for (const c of order) {
188
+ if (excess <= 0 || auto.has(c)) continue;
189
+ const take = Math.min(excess, c.width - c.min);
190
+ if (take > 0) {
191
+ widths.set(c, c.width - take);
192
+ excess -= take;
193
+ }
194
+ }
195
+ return { auto, widths, excess };
196
+ };
197
+ let g = fit(0);
198
+ for (let k = 1; g.excess > 0 && k <= order.length; k++) g = fit(k);
199
+ const notes = [];
200
+ for (const c of order) {
201
+ const w = g.widths.get(c);
202
+ if (w !== void 0 && w < c.width) notes.push({ code: "column-shrunk", side: c.side, column: c.index, items: c.panels, from: c.width, to: w });
203
+ }
204
+ for (const c of order) if (g.auto.has(c)) notes.push({ code: c.kept ? "kept-folded" : "column-folded", side: c.side, column: c.index, items: c.panels });
205
+ const items = /* @__PURE__ */ new Map();
206
+ const placedCols = [];
207
+ const rails = {};
208
+ let x0 = 0;
209
+ let x1 = W;
210
+ for (const side of SIDES) {
211
+ const mine = cols.filter((c) => c.side === side);
212
+ const open2 = mine.filter((c) => !c.hand && !g.auto.has(c));
213
+ const host = ride ? open2[0] : void 0;
214
+ const riders = host ? mine.filter((c) => c.hand) : [];
215
+ const inRail = mine.filter((c) => c.hand && !riders.includes(c) || g.auto.has(c));
216
+ if (inRail.length) {
217
+ const r = side === "left" ? rect(0, 0, opt.rail, H) : rect(W - opt.rail, 0, opt.rail, H);
218
+ const segments = inRail.flatMap((c) => c.panels).map((id, i) => ({ id, rect: rect(r.x, opt.railInset + i * opt.segment, opt.rail, opt.segment) }));
219
+ rails[side] = { side, rect: r, segments };
220
+ for (const s of segments) {
221
+ const c = inRail.find((x) => x.panels.includes(s.id));
222
+ items.set(s.id, { rect: s.rect, kind: "segment", layer: 0, hidden: true, folded: c.hand ? "hand" : "auto", kept: kept.has(s.id), pinned: false, floating: false, side, column: c.index });
223
+ }
224
+ if (side === "left") x0 = opt.rail;
225
+ else x1 = W - opt.rail;
226
+ }
227
+ const at = /* @__PURE__ */ new Map();
228
+ for (const c of mine) {
229
+ const w = g.widths.get(c);
230
+ if (w === void 0 || riders.includes(c)) continue;
231
+ if (side === "left") {
232
+ at.set(c, rect(x0, 0, w, H));
233
+ x0 += w;
234
+ } else {
235
+ x1 -= w;
236
+ at.set(c, rect(x1, 0, w, H));
237
+ }
238
+ }
239
+ const riding = riders.flatMap((c) => c.panels.map((id) => ({ id, c })));
240
+ const top = riding.length * opt.bar;
241
+ for (const c of mine) {
242
+ const w = g.widths.get(c);
243
+ const folded1 = c.hand ? "hand" : g.auto.has(c) ? "auto" : null;
244
+ const base = { side, index: c.index, width: c.width, min: c.min, max: c.max, folded: folded1, kept: c.kept, panels: c.panels };
245
+ if (riders.includes(c)) {
246
+ const hr = at.get(host);
247
+ const first = riding.findIndex((x) => x.c === c);
248
+ c.panels.forEach(
249
+ (id, i) => items.set(id, { rect: rect(hr.x, (first + i) * opt.bar, hr.width, opt.bar), kind: "bar", layer: 0, hidden: false, folded: "hand", kept: kept.has(id), pinned: false, floating: false, side, column: c.index, rides: host.index })
250
+ );
251
+ placedCols.push({ ...base, rect: rect(hr.x, first * opt.bar, hr.width, c.panels.length * opt.bar), shrunk: false, rides: host.index });
252
+ continue;
253
+ }
254
+ const r = at.get(c) ?? rect(side === "left" ? x0 : x1, 0, 0, H);
255
+ placedCols.push({ ...base, rect: r, shrunk: w !== void 0 && w < c.width });
256
+ if (w === void 0) continue;
257
+ const y0 = c === host ? top : 0;
258
+ const bars = c.panels.filter((id) => folded.has(id)).length;
259
+ const share = Math.max(0, (H - y0 - bars * opt.bar) / (c.panels.length - bars));
260
+ let y = y0;
261
+ for (const id of c.panels) {
262
+ const bar = folded.has(id);
263
+ const h = bar ? opt.bar : share;
264
+ items.set(id, { rect: rect(r.x, y, r.width, h), kind: bar ? "bar" : "column", layer: 0, hidden: false, folded: bar ? "hand" : null, kept: kept.has(id), pinned: false, floating: false, side, column: c.index });
265
+ y += h;
266
+ }
267
+ }
268
+ }
269
+ const pages = rect(x0, 0, Math.max(0, x1 - x0), H);
270
+ if (pages.width < opt.pagesMin) notes.push({ code: "pages-under-min", width: pages.width, min: opt.pagesMin });
271
+ state.floats.forEach((f, i) => {
272
+ if (closed.has(f.id)) return;
273
+ const shaded = folded.has(f.id);
274
+ const at = shaded ? { ...f.rect, height: opt.bar } : f.rect;
275
+ const k = f.pinned ? 1 : opt.view.scale;
276
+ const r = { ...at };
277
+ if (opt.floats === "clamp") {
278
+ const lim = floatSize(defOf(defs, f.id)).min;
279
+ r.width = Math.max(lim.width, Math.min(r.width, W));
280
+ if (!shaded) r.height = Math.max(lim.height, Math.min(r.height, H));
281
+ if (r.width !== at.width || r.height !== at.height) notes.push({ code: "float-resized", items: [f.id] });
282
+ r.x = r.width <= W ? clamp(r.x, 0, (W - r.width) / k) : clamp(r.x, (opt.floatVisible - r.width) / k, (W - opt.floatVisible) / k);
283
+ r.y = r.height <= H ? clamp(r.y, 0, (H - r.height) / k) : clamp(r.y, 0, (H - opt.bar) / k);
284
+ if (r.x !== at.x || r.y !== at.y) notes.push({ code: "float-clamped", items: [f.id] });
285
+ }
286
+ const clamped = r.x !== at.x || r.y !== at.y || r.width !== at.width || r.height !== at.height;
287
+ items.set(f.id, {
288
+ rect: f.pinned ? r : toView(r, opt.view),
289
+ kind: shaded ? "bar" : "float",
290
+ layer: 10 + i,
291
+ hidden: false,
292
+ folded: shaded ? "hand" : null,
293
+ kept: false,
294
+ pinned: !!f.pinned,
295
+ floating: true,
296
+ ...clamped ? { clamped: true } : {}
297
+ });
298
+ });
299
+ return { items, columns: placedCols, rails, notes, pages };
300
+ }
301
+
302
+ // src/panels/targets.ts
303
+ var COLUMN_REACH = 22;
304
+ var RAIL_EDGE = 10;
305
+ function placeRank(place) {
306
+ switch (place.kind) {
307
+ case "float":
308
+ return 3e3;
309
+ case "rail":
310
+ return place.side === "left" ? 0 : 1999;
311
+ case "column":
312
+ return place.side === "left" ? 1 + 2 * place.index : 1998 - 2 * place.index;
313
+ case "into":
314
+ return place.side === "left" ? 2 + 2 * place.index : 1997 - 2 * place.index;
315
+ }
316
+ }
317
+ function homePlace(state, id) {
318
+ const w = whereIs(state, id);
319
+ if (w.kind === "float") return { kind: "float" };
320
+ if (w.kind === "none") return null;
321
+ return state.sides[w.side][w.index].panels.length === 1 ? { kind: "column", side: w.side, index: w.index } : { kind: "into", side: w.side, index: w.index };
322
+ }
323
+ var cut = (x0, x1, lo, hi) => [Math.max(x0, lo), Math.min(x1, hi)];
324
+ function panelTargets(input) {
325
+ const { defs, state, id, stage } = input;
326
+ const opt = { ...DEFAULT_PANELS_OPTIONS, ...input.options };
327
+ const def = defOf(defs, id);
328
+ const s0 = liftPanel(state, id);
329
+ const l0 = layoutPanels(defs, s0, stage, opt);
330
+ const width = ownWidth(defs, state, id);
331
+ const W = stage.width;
332
+ const H = stage.height;
333
+ const R = COLUMN_REACH;
334
+ const out = [];
335
+ const add = (place, priority, hit, make, free = false) => {
336
+ if (hit.width <= 0 || hit.height <= 0) return;
337
+ if (input.allows && !input.allows(id, place)) return;
338
+ if (place.kind !== "float" && samePlaces(make({ x: 0, y: 0 }, { x: 0, y: 0 }), state, defs)) return;
339
+ out.push({ id: place.kind === "float" ? "float" : place.kind === "rail" ? `rail:${place.side}` : `${place.kind}:${place.side}:${place.index}`, priority, hit, panel: id, place, order: placeRank(place), make, ...free ? { free: true } : {} });
340
+ };
341
+ for (const side of SIDES) {
342
+ if (def?.sides && !def.sides.includes(side)) continue;
343
+ const left = side === "left";
344
+ const rail = l0.rails[side];
345
+ const edge = rail ? rail.rect.width : RAIL_EDGE;
346
+ const railHit = rail ? rail.rect : left ? rect(0, 0, RAIL_EDGE, H) : rect(W - RAIL_EDGE, 0, RAIL_EDGE, H);
347
+ add({ kind: "rail", side }, 1, railHit, () => {
348
+ const s = dock(state, id, side, 0, { width });
349
+ return { ...s, folded: [...s.folded.filter((x) => x !== id), id] };
350
+ });
351
+ const open2 = l0.columns.filter((c) => c.side === side && c.folded === null);
352
+ const lo = left ? edge : 0;
353
+ const hi = left ? W : W - edge;
354
+ const band = (x, outer, i) => {
355
+ const [a, b] = outer ? left ? cut(x, x + 2 * R, lo, hi) : cut(x - 2 * R, x, lo, hi) : cut(x - R, x + R, lo, hi);
356
+ add({ kind: "column", side, index: i }, 2, rect(a, 0, b - a, H), () => dock(state, id, side, i, { width }));
357
+ };
358
+ if (open2.length === 0) band(left ? l0.pages.x : l0.pages.x + l0.pages.width, true, s0.sides[side].length);
359
+ open2.forEach((c, k) => {
360
+ band(left ? c.rect.x : c.rect.x + c.rect.width, k === 0, c.index);
361
+ add({ kind: "into", side, index: c.index }, 2, c.rect, () => dock(state, id, side, c.index, { into: true }));
362
+ });
363
+ const last = open2.at(-1);
364
+ if (last) band(left ? last.rect.x + last.rect.width : last.rect.x, false, last.index + 1);
365
+ }
366
+ if (def?.floats !== false) {
367
+ const was = state.floats.find((f) => f.id === id);
368
+ const size = was ? { width: was.rect.width, height: was.rect.height } : floatSize(def).natural;
369
+ const pinned = !!was?.pinned;
370
+ add(
371
+ { kind: "float" },
372
+ 3,
373
+ rect(0, 0, W, H),
374
+ (p, grab) => {
375
+ const at = rect(p.x - grab.x, p.y - grab.y, size.width, size.height);
376
+ const s = float(state, id, pinned ? at : toPage(at, opt.view), pinned);
377
+ const laid = layoutPanels(defs, s, stage, opt).items.get(id).rect;
378
+ const shown = s.folded.includes(id) ? { ...laid, width: at.width, height: at.height } : laid;
379
+ return float(state, id, pinned ? shown : toPage(shown, opt.view), pinned);
380
+ },
381
+ true
382
+ );
383
+ }
384
+ return out.sort((a, b) => a.order - b.order);
385
+ }
386
+ function panelDrop(input, target, p, grab) {
387
+ const opt = { ...DEFAULT_PANELS_OPTIONS, ...input.options };
388
+ const state = target.make(p, grab);
389
+ const layout = layoutPanels(input.defs, state, input.stage, opt);
390
+ const placed = layout.items.get(input.id);
391
+ const key = target.free ? `float:${Math.round(placed.rect.x)},${Math.round(placed.rect.y)}` : target.id;
392
+ return { target: target.id, place: target.place, key, state, layout, slot: placed.rect, hidden: placed.hidden, free: !!target.free };
393
+ }
394
+ function panelStops(targets, options = {}) {
395
+ const stops = [];
396
+ for (const t of targets) {
397
+ if (t.place.kind === "float") {
398
+ if (!options.floats) continue;
399
+ const at = options.floats === true ? { x: t.hit.x + t.hit.width / 2, y: t.hit.y + t.hit.height / 2 } : options.floats.at;
400
+ const grab = options.floats === true ? { x: 0, y: 0 } : options.floats.grab ?? { x: 0, y: 0 };
401
+ stops.push({ id: t.id, place: t.place, state: t.make(at, grab), home: false, order: t.order });
402
+ continue;
403
+ }
404
+ stops.push({ id: t.id, place: t.place, state: t.make({ x: 0, y: 0 }, { x: 0, y: 0 }), home: false, order: t.order });
405
+ }
406
+ const h = options.home;
407
+ if (h) stops.push({ id: "home", place: h.place, state: h.state, home: true, order: placeRank(h.place) + (h.place.kind === "float" ? 1 : 0.5) });
408
+ return stops.sort((a, b) => a.order - b.order).map(({ order: _order, ...s }) => s);
409
+ }
410
+
411
+ export {
412
+ SIDES,
413
+ IDENTITY_VIEW,
414
+ DEFAULT_PANELS_OPTIONS,
415
+ FLOAT_HEIGHT,
416
+ FLOAT_MIN,
417
+ whereIs,
418
+ tick,
419
+ defOf,
420
+ floatSize,
421
+ ownWidth,
422
+ createPanels,
423
+ liftPanel,
424
+ open,
425
+ close,
426
+ focus,
427
+ toggleFold,
428
+ keep,
429
+ dock,
430
+ float,
431
+ toView,
432
+ toPage,
433
+ pin,
434
+ resizeColumn,
435
+ resizeFloat,
436
+ columnWidth,
437
+ samePlaces,
438
+ giveWayOrder,
439
+ layoutPanels,
440
+ COLUMN_REACH,
441
+ RAIL_EDGE,
442
+ placeRank,
443
+ homePlace,
444
+ panelTargets,
445
+ panelDrop,
446
+ panelStops
447
+ };
448
+ //# sourceMappingURL=chunk-QR7MGDX6.js.map