@n-uf/placement 26.11.6 → 26.11.7

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