@n-uf/placement 26.11.7 → 26.11.9

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,44 @@
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.9
7
+
8
+ - `@n-uf/placement/panels`:
9
+ - Tabs: `Column.show` (`"stack"`, the default, or `"tabs"`) and `Column.active`. A tabbed column draws a strip
10
+ `options.tabs` tall (new chrome size, default 28; `PlacedColumn.strip`); its active panel takes the rest and the
11
+ others are `hidden`, each with its `tab` rect in the strip. It folds as a whole: its active panel folded shows only
12
+ the strip, every panel folded goes to the rail (or rides). `showTabs(state, side, index, on)`, `activate(state, id)`
13
+ (a use). A drop `into` a tabbed column adds a tab at the end and shows it, and so does `open` on a panel in one; a
14
+ panel that leaves takes its `active` mark with it. With no `active` (or one that isn't there), the column shows its
15
+ first panel that isn't closed.
16
+ - Height give-way, opt-in: `PanelDef.height` (`{ min, natural }`). In a stack column where an open section has one,
17
+ sections get their natural heights and the last open one the rest (never under its min); sections without one
18
+ share what is left, never under a bar's height. The others' natural heights give way toward their mins first;
19
+ still short of room, open sections fold to bars in give-way policy order (kept last, never the last open one, never
20
+ one whose min is no more than a bar), note `section-folded`. It never writes `folded`. Columns without `height`
21
+ defs are unchanged.
22
+ - The stored form carries a column's `show` and `active` (optional v1 fields); an `active` that isn't in its column
23
+ is dropped as `"column.active:<id>"`, an unknown `show` as `"column.show"`.
24
+ - New entry `@n-uf/placement/desk`: presets as data on `./panels` (`docks`, `dockTear`, `navigator`; `PRESETS`,
25
+ `presetLayout`, `presetTargets`, `presetDrop`, `presetInput`). It imports only `./panels`' public index (a guard test
26
+ holds it to that) and carries no titles, words or toolbar wiring. `./dock` is unchanged.
27
+
28
+ ## 26.11.8
29
+
30
+ - `@n-uf/placement/panels`:
31
+ - Folded floats: a float folded by hand lays out as a title bar (`kind` `"bar"`, `folded` `"hand"`) at its own x, y
32
+ and width, `options.bar` tall, clamped as a bar, pinned or not. It is still a float to targets and drags; a drop
33
+ moves it and keeps its full size, and unfolding brings the full rect back (the stored rect never changes). A
34
+ folded docked panel dropped on the float target stays folded: it lands as a title bar at its natural float size.
35
+ - `options.fold`: `"rail"` (default, as before) or `"ride"`. Riding, a column folded by hand on a side that still
36
+ has an open column becomes title bars stacked on top of the side's first open column (`rides`: its index), as
37
+ wide as it; the open columns lie as if the folded one were gone, and its stored place and width are untouched.
38
+ When every column on the side is folded, the side shows its rail as before. Give-way's folds always go to the rail.
39
+ - `PlacedPanel` gains `floating` (open or folded float) and `rides`; `PlacedColumn` gains `rides`.
40
+ - `pin(…, shown)` takes only the position of `shown` and keeps the stored size, so pinning a folded or clamped
41
+ float keeps its full size.
42
+ - A column too short for its bars gives its open panels a height of 0, never a negative one.
43
+
6
44
  ## 26.11.7
7
45
 
8
46
  - New entry `@n-uf/placement/panels`: tool panels with string ids, as an app's side columns work. `./dock` is
package/README.md CHANGED
@@ -3,16 +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
10
  Site and live demos: [motion.n-uf.com](https://motion.n-uf.com)
11
11
 
12
- Where things go. State in, rects and drop targets out: a desk of docks, rails and palettes, toolbar groups in hosts
13
- and zones, and the drop targets for each. A tiling entry (a split tree with ratios, groups and workspaces) is
14
- planned. Pure functions over plain data: no DOM,
15
- no framework, no motion, no dependencies.
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.
16
16
 
17
17
  ```sh
18
18
  npm i @n-uf/placement
@@ -34,9 +34,11 @@ without importing each other: a target list from `@n-uf/placement/targets` is ex
34
34
  - **`/targets`**: drop targets (`{ id, priority, hit, slot?, label?, free? }`) for the desk's constructions,
35
35
  toolbar hosts (each with its `slot`, and `allows` to leave out places a group may not take) and pane zones; and
36
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.
37
+ - **`/panels`**: tool panels with string ids: full-height columns per side, each with its own width, shown as a
38
+ stack of sections or as tabs; folding to a title bar or a rail; give-way by age when the pages run out of room (and,
39
+ opt-in, when a column runs out of height); resizable floats, some pinned to the view; their drop targets; and a
40
+ stored form.
41
+ - **`/desk`**: desk presets (`docks`, `dockTear`, `navigator`) as data on `/panels`: options and a start state.
40
42
 
41
43
  Rendering stays in the app: it draws the rects, measures its groups, and feeds pointer samples to a drag intent.
42
44
 
@@ -160,9 +162,11 @@ console.log(stops.map((s) => (s.home ? "home" : s.id))); // [ 'g:top:start:0', '
160
162
  `@n-uf/placement/panels` lays out tool panels the way an app's side columns work: each side holds full-height
161
163
  columns from the outer edge inward, each column one or more panels and its own width (clamped to its panels'
162
164
  `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
165
+ its side's rail, or with `fold: "ride"` bars on the side's first open column. Panels may float instead, in page space
166
+ (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
167
+ a folded panel torn off stays folded. The output is geometry and structure only: each panel's rect, layer, `kind`
168
+ (`column`, `bar`, `segment`, `float`; a folded float is a `bar`) and its folded, kept, pinned, floating and rides
169
+ flags, the columns, the rails, the pages' rect and coded notes. Chrome sizes
166
170
  (`bar`, `rail`, `segment`) are options.
167
171
 
168
172
  ```ts
@@ -196,11 +200,38 @@ even at its minimum size keeps its title bar and `floatVisible` (default 48) px
196
200
  size is screen px and never scales. A pinned float is clamped where it is; an unpinned one has its page-space position
197
201
  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
202
  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.
203
+ float. A float folded by hand (`toggleFold`) lays out as a title bar: `kind` `"bar"`, `folded` `"hand"`, at its own x,
204
+ y and width and `options.bar` tall, clamped as a bar. It is still a float (`floating: true`) to its targets and drags,
205
+ pinned or not, and unfolding brings its full rect back: the stored rect never changes.
206
+
207
+ **Folded columns** (`options.fold`, default `"rail"`): a column folded by hand goes to its side's rail. With `"ride"`,
208
+ it rides instead while its side still has an open column: its panels become title bars (`kind` `"bar"`, `rides`: the
209
+ host's index) stacked in column order on top of the side's first open column (outer edge first), as wide as it, and
210
+ the host's open panels share the rest of the height. The open columns lie as if the folded one were gone (the pages
211
+ gain its room), and its stored place and width are untouched, so unfolding puts it back as its own column. Only when
212
+ every column on a side is folded does the side show its rail. Give-way's folds (`column-folded`, `kept-folded`) go to
213
+ the rail in both modes.
214
+
215
+ **Tabs**: a column with `show: "tabs"` shows one panel at a time, its `active` one (default, or when `active` isn't
216
+ one of them: the first panel in the column that isn't closed), under a strip
217
+ `options.tabs` tall (default 28). The strip is `PlacedColumn.strip`, and each panel's `tab` is its header's rect in it
218
+ (panel order, equal widths); the others are `hidden`. A tabbed column folds as a whole: with its active panel in
219
+ `folded` it shows only its strip (the active panel is a `"bar"` there); with every panel folded it goes to the rail,
220
+ or rides, as a stack column does. `showTabs(state, side, index, on)` switches a column; `activate(state, id)` shows a
221
+ tab and counts as a use. A drop `into` a tabbed column adds a tab at the end and shows it, and `open` on a panel in a
222
+ tabbed column shows its tab too.
223
+
224
+ **Section heights** (opt-in): a `PanelDef.height` (`{ min, natural }`) gives a section of a stack column its natural
225
+ height; the column's last open section takes the rest, never under its min, and sections without one share what is
226
+ left equally, never under a bar's height (`options.bar`). The others' natural heights give way toward their mins
227
+ first. When the open sections' mins and the bars still don't fit, open sections fold to bars by the give-way policy
228
+ (kept last, never the last open one, and never one whose min is no more than a bar, since folding it gains nothing),
229
+ each with a `section-folded` note; that never writes `folded`, so a taller stage unfolds them. Columns without
230
+ `height` defs lay out as before.
200
231
 
201
232
  **Changes** are pure: `open`, `close`, `focus`, `toggleFold`, `keep`, `dock(state, id, side, index, { into?, width? })`,
202
233
  `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`.
234
+ shows), `resizeColumn`, `resizeFloat`, `showTabs`, `activate`.
204
235
 
205
236
  **Drop targets**: `panelTargets({ defs, state, id, stage, options?, allows? })` gives `column:<side>:<i>` (a new
206
237
  column as wide as the panel's own width), `into:<side>:<i>` (stack into a column), `rail:<side>` (dock folded) and
@@ -231,18 +262,53 @@ console.log(stops.map((s) => (s.home ? "home" : s.id))); // [ 'rail:left', 'colu
231
262
  parsed data back and never throws. Anything but an object of version 1 is `{ ok: false, error }`; inside one, unknown
232
263
  or repeated ids and malformed entries are dropped and listed in `dropped`, and widths and float sizes are clamped to
233
264
  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`):
265
+ too (`"theme"`, `"sides.bottom"`, `"column.<field>"`, `"float.<field>"`, `"clock.<id>"`). A column's `show` and
266
+ `active` are carried; an `active` that isn't one of the column's panels is dropped as `"column.active:<id>"`. The note
267
+ codes (`PanelsNoteCode`):
236
268
 
237
269
  | Code | Says | Also |
238
270
  |---|---|---|
239
271
  | `column-shrunk` | a column narrowed toward its minimum | `side`, `column`, `items`, `from`, `to` |
240
272
  | `column-folded` | a column folded to its side's rail | `side`, `column`, `items` |
241
273
  | `kept-folded` | a kept column folded: the pages needed the room | `side`, `column`, `items` |
274
+ | `section-folded` | a section of a stack column folded to a bar: the column was short | `side`, `column`, `items` |
242
275
  | `pages-under-min` | the pages are under `pagesMin` with every column folded | `width`, `min` |
243
276
  | `float-resized` | a float shrank to fit the view | `items` |
244
277
  | `float-clamped` | a float moved to stay in the view | `items` |
245
278
 
279
+ ## Desk presets
280
+
281
+ `@n-uf/placement/desk` holds desk presets as data on `./panels`: each is panels options and a start state. It imports
282
+ only the panels entry, carries no titles or words and wires no toolbar; the app names the presets and draws them.
283
+
284
+ | Preset | Columns and floats |
285
+ |---|---|
286
+ | `docks` | one stack column per side, sections at their natural heights when the defs give `height`, folding to the rail; no floats |
287
+ | `dockTear` | as `docks`, and panels may float (tear-off and dock-back); one panel floats at the start |
288
+ | `navigator` | a tabbed column on the left, a stack column on the right; no floats |
289
+
290
+ ```ts
291
+ import { PRESETS, presetLayout, presetTargets } from "@n-uf/placement/desk";
292
+ import type { PanelDef } from "@n-uf/placement/panels";
293
+
294
+ const defs: PanelDef[] = [
295
+ { id: "files", width: { min: 200, natural: 240, max: 360 }, height: { min: 80, natural: 160 } },
296
+ { id: "outline", width: { min: 200, natural: 240, max: 360 }, height: { min: 80, natural: 200 } },
297
+ { id: "history", width: { min: 200, natural: 240, max: 360 } },
298
+ ];
299
+ const nav = PRESETS.navigator;
300
+ const state = nav.start(defs, { left: ["files", "outline"] });
301
+ const layout = presetLayout(nav, defs, state, { width: 1200, height: 600 });
302
+ console.log(layout.columns[0]!.strip, layout.items.get("outline")!.hidden); // { x: 0, y: 0, width: 240, height: 28 } true
303
+ const targets = presetTargets(nav, { defs, state, id: "history", stage: { width: 1200, height: 600 } });
304
+ console.log(targets.some((t) => t.id === "float")); // false
305
+ ```
306
+
307
+ `presetLayout(preset, defs, state, stage, options?)` is `layoutPanels` with the preset's options under the caller's;
308
+ `presetTargets(preset, input)` and `presetDrop(preset, input, target, point, grab)` are `panelTargets` and `panelDrop`
309
+ the same way, offering only the places the preset allows (`presetInput` gives that input). `./dock` stays as it is for
310
+ now.
311
+
246
312
  ## Pane zones
247
313
 
248
314
  `paneZone(rect, point, previous, options?)` says which of a pane's five zones a point is in: the centre rectangle
@@ -8,13 +8,15 @@ var IDENTITY_VIEW = { x: 0, y: 0, scale: 1 };
8
8
  var DEFAULT_PANELS_OPTIONS = {
9
9
  giveWay: { order: "oldest", by: "used" },
10
10
  floats: "clamp",
11
+ fold: "rail",
11
12
  floatVisible: 48,
12
13
  pagesMin: 320,
13
14
  view: IDENTITY_VIEW,
14
15
  bar: 28,
15
16
  rail: 36,
16
17
  segment: 36,
17
- railInset: 4
18
+ railInset: 4,
19
+ tabs: 28
18
20
  };
19
21
  var FLOAT_HEIGHT = 360;
20
22
  var FLOAT_MIN = { width: 160, height: 120 };
@@ -67,12 +69,18 @@ function createPanels(defs, start) {
67
69
  return { sides, floats, folded: [...start.folded ?? []], kept: [...start.kept ?? []], closed: [...start.closed ?? []], clock };
68
70
  }
69
71
  function liftPanel(state, id) {
70
- const lift = (cols) => cols.map((c) => ({ ...c, panels: without(c.panels, id) })).filter((c) => c.panels.length > 0);
72
+ const out = (c) => {
73
+ const { active: _a, ...rest } = c;
74
+ return { ...c.active === id ? rest : c, panels: without(c.panels, id) };
75
+ };
76
+ const lift = (cols) => cols.map(out).filter((c) => c.panels.length > 0);
71
77
  return { ...state, sides: { left: lift(state.sides.left), right: lift(state.sides.right) }, floats: state.floats.filter((f) => f.id !== id) };
72
78
  }
73
79
  function open(state, id, at) {
74
80
  let s = { ...state, closed: without(state.closed, id) };
75
81
  if (at && whereIs(s, id).kind === "none") s = { ...s, sides: { ...s.sides, [at.side]: [...s.sides[at.side], { panels: [id], width: at.width }] } };
82
+ const w = whereIs(s, id);
83
+ if (w.kind === "column" && s.sides[w.side][w.index].show === "tabs") s = { ...s, sides: { ...s.sides, [w.side]: s.sides[w.side].map((c, i) => i === w.index ? { ...c, active: id } : c) } };
76
84
  return bump(s, id, true);
77
85
  }
78
86
  var close = (state, id) => ({ ...state, closed: withOnce(state.closed, id), kept: without(state.kept, id) });
@@ -94,13 +102,25 @@ function dock(state, id, side, index, options = {}) {
94
102
  const from = w.kind === "column" ? state.sides[w.side][w.index].width : 0;
95
103
  const s = liftPanel(state, id);
96
104
  const cols = [...s.sides[side]];
97
- if (options.into && cols[index]) cols[index] = { ...cols[index], panels: [...cols[index].panels, id] };
105
+ if (options.into && cols[index]) cols[index] = { ...cols[index], panels: [...cols[index].panels, id], ...cols[index].show === "tabs" ? { active: id } : {} };
98
106
  else {
99
107
  const at = Math.max(0, Math.min(index, cols.length));
100
108
  cols.splice(at, 0, { panels: [id], width: options.width ?? from });
101
109
  }
102
110
  return bump({ ...s, sides: { ...s.sides, [side]: cols }, closed: without(s.closed, id) }, id, false);
103
111
  }
112
+ function showTabs(state, side, index, on) {
113
+ const col = state.sides[side][index];
114
+ if (!col) return state;
115
+ const { show: _s, ...stack } = col;
116
+ return { ...state, sides: { ...state.sides, [side]: state.sides[side].map((c, i) => i === index ? on ? { ...col, show: "tabs" } : stack : c) } };
117
+ }
118
+ function activate(state, id) {
119
+ const w = whereIs(state, id);
120
+ if (w.kind !== "column") return state;
121
+ const sides = { ...state.sides, [w.side]: state.sides[w.side].map((c, i) => i === w.index ? { ...c, active: id } : c) };
122
+ return bump({ ...state, sides }, id, false);
123
+ }
104
124
  function float(state, id, rect2, pinned) {
105
125
  const was = state.floats.find((f2) => f2.id === id);
106
126
  const on = pinned ?? !!was?.pinned;
@@ -115,7 +135,7 @@ function pin(state, id, on, view = IDENTITY_VIEW, shown) {
115
135
  ...state,
116
136
  floats: state.floats.map((f) => {
117
137
  if (f.id !== id || !!f.pinned === on) return f;
118
- const seen = shown ? { ...shown } : f.pinned ? { ...f.rect } : toView(f.rect, view);
138
+ const seen = shown ? { ...f.rect, x: shown.x, y: shown.y } : f.pinned ? { ...f.rect } : toView(f.rect, view);
119
139
  return on ? { id, rect: seen, pinned: true } : { id, rect: toPage(seen, view) };
120
140
  })
121
141
  };
@@ -140,7 +160,9 @@ function columnWidth(defs, col) {
140
160
  return Math.min(Math.max(Number.isFinite(col.width) ? col.width : lo, lo), hi);
141
161
  }
142
162
  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");
163
+ const cols = (s) => SIDES.map(
164
+ (side) => s.sides[side].map((c) => `${c.panels.join(",")}@${Math.round(columnWidth(defs, c))}${c.show === "tabs" ? `/tabs:${c.active ?? c.panels[0]}` : ""}`).join("|")
165
+ ).join("\u2016");
144
166
  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
167
  const set = (l) => [...l].sort().join(",");
146
168
  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);
@@ -159,6 +181,7 @@ function layoutPanels(defs, state, stage, options = {}) {
159
181
  const folded = new Set(state.folded);
160
182
  const kept = new Set(state.kept);
161
183
  const by = opt.giveWay.by;
184
+ const ride = opt.fold === "ride";
162
185
  const cols = [];
163
186
  for (const side of SIDES) {
164
187
  state.sides[side].forEach((c, index) => {
@@ -169,16 +192,74 @@ function layoutPanels(defs, state, stage, options = {}) {
169
192
  const max = Math.max(min, Math.min(...ds.map((d) => d?.width.max ?? Infinity)));
170
193
  const width = clamp(Number.isFinite(c.width) ? c.width : min, min, max);
171
194
  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 });
195
+ const tabs = c.show === "tabs" ? { show: "tabs", active: c.active !== void 0 && panels.includes(c.active) ? c.active : panels[0] } : {};
196
+ cols.push({ side, index, panels, min, max, width, hand: panels.every((id) => folded.has(id)), kept: panels.some((id) => kept.has(id)), age, ...tabs });
173
197
  });
174
198
  }
199
+ const sectionHeights = (c, room) => {
200
+ const out = /* @__PURE__ */ new Map();
201
+ const open0 = c.panels.filter((id) => !folded.has(id));
202
+ const hOf = (id) => defOf(defs, id)?.height;
203
+ const auto = /* @__PURE__ */ new Set();
204
+ if (open0.some((id) => hOf(id))) {
205
+ const last = open0.at(-1);
206
+ const minOf = (id) => hOf(id)?.min ?? opt.bar;
207
+ const fits = () => (c.panels.length - open0.length + auto.size) * opt.bar + open0.filter((id) => !auto.has(id)).reduce((a, id) => a + minOf(id), 0) <= room;
208
+ const rank = (ids) => [...ids].sort((a, b) => {
209
+ const age = (id) => state.clock[id]?.[by] ?? 0;
210
+ return Number(kept.has(a)) - Number(kept.has(b)) || (opt.giveWay.order === "oldest" ? age(a) - age(b) : age(b) - age(a)) || c.panels.indexOf(a) - c.panels.indexOf(b);
211
+ });
212
+ for (const id of rank(open0.filter((x) => x !== last))) {
213
+ if (fits()) break;
214
+ if (minOf(id) > opt.bar) auto.add(id);
215
+ }
216
+ const open2 = open0.filter((id) => !auto.has(id));
217
+ const space = room - (c.panels.length - open2.length) * opt.bar;
218
+ const fixed = open2.filter((id) => id !== last && hOf(id));
219
+ const sharers = open2.filter((id) => id === last || !hOf(id));
220
+ const h = new Map(fixed.map((id) => [id, hOf(id).natural]));
221
+ let rest = space - fixed.reduce((a, id) => a + h.get(id), 0);
222
+ const need = sharers.reduce((a, id) => a + minOf(id), 0);
223
+ for (const id of rank(fixed)) {
224
+ if (rest >= need) break;
225
+ const take = Math.min(need - rest, h.get(id) - minOf(id));
226
+ h.set(id, h.get(id) - take);
227
+ rest += take;
228
+ }
229
+ if (rest >= need) {
230
+ let pool = [...sharers];
231
+ let left = rest;
232
+ for (; ; ) {
233
+ const share2 = left / pool.length;
234
+ const under = pool.filter((id) => minOf(id) > share2);
235
+ if (under.length === 0) {
236
+ for (const id of pool) h.set(id, share2);
237
+ break;
238
+ }
239
+ for (const id of under) h.set(id, minOf(id));
240
+ left -= under.reduce((a, id) => a + minOf(id), 0);
241
+ pool = pool.filter((id) => !under.includes(id));
242
+ }
243
+ } else {
244
+ const lastH = Math.max(0, Math.min(minOf(last), rest));
245
+ for (const id of sharers) h.set(id, id === last ? lastH : Math.max(0, rest - lastH) / (sharers.length - 1));
246
+ }
247
+ for (const id of c.panels) out.set(id, folded.has(id) ? { height: opt.bar, folded: "hand" } : auto.has(id) ? { height: opt.bar, folded: "auto" } : { height: Math.max(0, h.get(id)), folded: null });
248
+ return out;
249
+ }
250
+ const bars = c.panels.length - open0.length;
251
+ const share = Math.max(0, (room - bars * opt.bar) / open0.length);
252
+ for (const id of c.panels) out.set(id, folded.has(id) ? { height: opt.bar, folded: "hand" } : { height: share, folded: null });
253
+ return out;
254
+ };
175
255
  const order = giveWayOrder(
176
256
  cols.filter((c) => !c.hand),
177
257
  opt.giveWay.order
178
258
  );
179
259
  const fit = (k) => {
180
260
  const auto = new Set(order.slice(0, k));
181
- const railed = (side) => cols.some((c) => c.side === side && (c.hand || auto.has(c)));
261
+ const open2 = (side) => cols.some((c) => c.side === side && !c.hand && !auto.has(c));
262
+ const railed = (side) => cols.some((c) => c.side === side && (auto.has(c) || c.hand && !(ride && open2(side))));
182
263
  const widths = new Map(order.filter((c) => !auto.has(c)).map((c) => [c, c.width]));
183
264
  let excess = [...widths.values()].reduce((a, b) => a + b, 0) + SIDES.filter(railed).length * opt.rail + opt.pagesMin - W;
184
265
  for (const c of order) {
@@ -206,60 +287,108 @@ function layoutPanels(defs, state, stage, options = {}) {
206
287
  let x1 = W;
207
288
  for (const side of SIDES) {
208
289
  const mine = cols.filter((c) => c.side === side);
209
- const inRail = mine.filter((c) => c.hand || g.auto.has(c));
290
+ const open2 = mine.filter((c) => !c.hand && !g.auto.has(c));
291
+ const host = ride ? open2[0] : void 0;
292
+ const riders = host ? mine.filter((c) => c.hand) : [];
293
+ const inRail = mine.filter((c) => c.hand && !riders.includes(c) || g.auto.has(c));
210
294
  if (inRail.length) {
211
295
  const r = side === "left" ? rect(0, 0, opt.rail, H) : rect(W - opt.rail, 0, opt.rail, H);
212
296
  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
297
  rails[side] = { side, rect: r, segments };
214
298
  for (const s of segments) {
215
299
  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 });
300
+ 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 });
217
301
  }
218
302
  if (side === "left") x0 = opt.rail;
219
303
  else x1 = W - opt.rail;
220
304
  }
305
+ const at = /* @__PURE__ */ new Map();
221
306
  for (const c of mine) {
222
307
  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);
308
+ if (w === void 0 || riders.includes(c)) continue;
309
+ if (side === "left") {
310
+ at.set(c, rect(x0, 0, w, H));
227
311
  x0 += w;
228
312
  } else {
229
313
  x1 -= w;
230
- r = rect(x1, 0, w, H);
314
+ at.set(c, rect(x1, 0, w, H));
315
+ }
316
+ }
317
+ const riding = riders.flatMap((c) => c.panels.map((id) => ({ id, c })));
318
+ const top = riding.length * opt.bar;
319
+ for (const c of mine) {
320
+ const w = g.widths.get(c);
321
+ const folded1 = c.hand ? "hand" : g.auto.has(c) ? "auto" : null;
322
+ const base = { side, index: c.index, width: c.width, min: c.min, max: c.max, folded: folded1, kept: c.kept, panels: c.panels };
323
+ if (riders.includes(c)) {
324
+ const hr = at.get(host);
325
+ const first = riding.findIndex((x) => x.c === c);
326
+ c.panels.forEach(
327
+ (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 })
328
+ );
329
+ placedCols.push({ ...base, rect: rect(hr.x, first * opt.bar, hr.width, c.panels.length * opt.bar), shrunk: false, rides: host.index });
330
+ continue;
231
331
  }
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 });
332
+ const r = at.get(c) ?? rect(side === "left" ? x0 : x1, 0, 0, H);
333
+ const y0 = c === host ? top : 0;
334
+ const tabbed = c.show === "tabs";
335
+ const strip = rect(r.x, y0, r.width, opt.tabs);
336
+ const shown = tabbed && w !== void 0 ? { strip, active: c.active } : {};
337
+ placedCols.push({ ...base, rect: r, shrunk: w !== void 0 && w < c.width, ...tabbed ? { show: "tabs" } : {}, ...shown });
233
338
  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;
339
+ const panel = (id, to, kind, f, more = {}) => {
340
+ items.set(id, { rect: to, kind, layer: 0, hidden: more.hidden ?? false, folded: f, kept: kept.has(id), pinned: false, floating: false, side, column: c.index, ...more.tab ? { tab: more.tab } : {} });
341
+ };
342
+ if (tabbed) {
343
+ const body = rect(r.x, y0 + opt.tabs, r.width, Math.max(0, H - y0 - opt.tabs));
344
+ const shade = folded.has(c.active);
345
+ const tw = r.width / c.panels.length;
346
+ c.panels.forEach((id, i) => {
347
+ const tab = rect(r.x + i * tw, y0, tw, opt.tabs);
348
+ if (id === c.active) panel(id, shade ? strip : body, shade ? "bar" : "column", shade ? "hand" : null, { tab });
349
+ else panel(id, body, "column", folded.has(id) ? "hand" : null, { tab, hidden: true });
350
+ });
351
+ continue;
352
+ }
353
+ const hs = sectionHeights(c, H - y0);
354
+ let y = y0;
237
355
  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;
356
+ const s = hs.get(id);
357
+ panel(id, rect(r.x, y, r.width, s.height), s.folded ? "bar" : "column", s.folded);
358
+ y += s.height;
242
359
  }
360
+ for (const id of c.panels) if (hs.get(id).folded === "auto") notes.push({ code: "section-folded", side, column: c.index, items: [id] });
243
361
  }
244
362
  }
245
363
  const pages = rect(x0, 0, Math.max(0, x1 - x0), H);
246
364
  if (pages.width < opt.pagesMin) notes.push({ code: "pages-under-min", width: pages.width, min: opt.pagesMin });
247
365
  state.floats.forEach((f, i) => {
248
366
  if (closed.has(f.id)) return;
249
- const at = f.rect;
367
+ const shaded = folded.has(f.id);
368
+ const at = shaded ? { ...f.rect, height: opt.bar } : f.rect;
250
369
  const k = f.pinned ? 1 : opt.view.scale;
251
370
  const r = { ...at };
252
371
  if (opt.floats === "clamp") {
253
372
  const lim = floatSize(defOf(defs, f.id)).min;
254
373
  r.width = Math.max(lim.width, Math.min(r.width, W));
255
- r.height = Math.max(lim.height, Math.min(r.height, H));
374
+ if (!shaded) r.height = Math.max(lim.height, Math.min(r.height, H));
256
375
  if (r.width !== at.width || r.height !== at.height) notes.push({ code: "float-resized", items: [f.id] });
257
376
  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
377
  r.y = r.height <= H ? clamp(r.y, 0, (H - r.height) / k) : clamp(r.y, 0, (H - opt.bar) / k);
259
378
  if (r.x !== at.x || r.y !== at.y) notes.push({ code: "float-clamped", items: [f.id] });
260
379
  }
261
380
  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 } : {} });
381
+ items.set(f.id, {
382
+ rect: f.pinned ? r : toView(r, opt.view),
383
+ kind: shaded ? "bar" : "float",
384
+ layer: 10 + i,
385
+ hidden: false,
386
+ folded: shaded ? "hand" : null,
387
+ kept: false,
388
+ pinned: !!f.pinned,
389
+ floating: true,
390
+ ...clamped ? { clamped: true } : {}
391
+ });
263
392
  });
264
393
  return { items, columns: placedCols, rails, notes, pages };
265
394
  }
@@ -339,7 +468,8 @@ function panelTargets(input) {
339
468
  (p, grab) => {
340
469
  const at = rect(p.x - grab.x, p.y - grab.y, size.width, size.height);
341
470
  const s = float(state, id, pinned ? at : toPage(at, opt.view), pinned);
342
- const shown = layoutPanels(defs, s, stage, opt).items.get(id).rect;
471
+ const laid = layoutPanels(defs, s, stage, opt).items.get(id).rect;
472
+ const shown = s.folded.includes(id) ? { ...laid, width: at.width, height: at.height } : laid;
343
473
  return float(state, id, pinned ? shown : toPage(shown, opt.view), pinned);
344
474
  },
345
475
  true
@@ -391,6 +521,8 @@ export {
391
521
  toggleFold,
392
522
  keep,
393
523
  dock,
524
+ showTabs,
525
+ activate,
394
526
  float,
395
527
  toView,
396
528
  toPage,
@@ -409,4 +541,4 @@ export {
409
541
  panelDrop,
410
542
  panelStops
411
543
  };
412
- //# sourceMappingURL=chunk-NNFNMIW4.js.map
544
+ //# sourceMappingURL=chunk-PWQWD2CS.js.map