@n-uf/placement 26.11.9 → 26.11.11

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,53 @@
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.11
7
+
8
+ - `@n-uf/placement/panels`, drops into a stacked column at a position (opt-in; without it every target is what it was):
9
+ - `DockOptions.at`: with `into`, the panel goes before the column's member at that index (counted with the panel
10
+ lifted out; clamped; left out or not a number: the bottom). Into a tabbed column it is the tab's position.
11
+ - `PanelTargetInput.stackSlots`: a stacked column also offers `into:<side>:<i>:<k>`, before member k, its area
12
+ split at the members' middles; `into:<side>:<i>` keeps the part below the last member's middle. These targets
13
+ carry `PanelTarget.marker`, the line where the panel's top lands after the drop (from the drop's layout; zero
14
+ tall, the column's width). k counts closed members, which keep their place; only members shown get slots. Tabbed
15
+ columns keep their one `into` target.
16
+ - Typing: the `into` place gains an optional `at` (no new `kind`, so exhaustive switches still hold);
17
+ `PanelTarget.marker?` is new. `homePlace(state, id, { stackSlots })` gives a stacked panel's `at`, and
18
+ `keyboardStops` puts such a home at its position among the slots.
19
+
20
+ ## 26.11.10
21
+
22
+ - `@n-uf/placement/panels`, bands and reserved room (all opt-in; a state and options without them lay out as before,
23
+ and the store stays v1):
24
+ - Bands: a top and a bottom dock (`options.bands`, `{ top?, bottom? }`, each `{ mode: "reserved" | "overlay",
25
+ span: "between" | "full" }`). Its panels sit side by side and share its width (`kind` `"band"`, `edge`, `index`);
26
+ a stored height clamped to `PanelDef.band` (default `BAND_SIZE`, min 120, max 480, natural 200). It folds as a
27
+ whole, to one bar. `layout.bands` (`PlacedBand`). New: `Edge`, `EDGES`, `Place`, `Band`, `BandOption`,
28
+ `PanelsState.bands`, `PanelsStart.top` / `bottom`, `dockBand`, `resizeBand`, `bandSize`, `bandHeight`;
29
+ `open(state, id, { edge, height? })`; `Where` and `PanelPlace` gain a `band` kind; `PanelDef.sides` may name
30
+ edges.
31
+ - Height give-way for bands: `options.pagesMinHeight` (default 240). Reserved bands shrink, then fold to a bar,
32
+ unkept before kept, policy order, bottom first on a tie, until the pages keep the min; then the overlay bands,
33
+ together, until they leave the min of the pages uncovered. Notes `band-shrunk`, `band-folded`, `kept-folded` with
34
+ `edge`; `pages-short` when the pages' rect is still under the min (overlay bands never cause it); `band-off` for a
35
+ band on an edge the options don't enable.
36
+ - Reserved room: `options.reserve` (`{ top?, right?, bottom?, left? }`) keeps px clear along the pages' edges for an
37
+ app's toolbar; `layout.reserved` has the rects. `pagesMin` and `pagesMinHeight` count after it. An overlay band
38
+ lies next to the reserve on its edge, on the pages' side, never over it.
39
+ - Targets `band:<edge>:<i>` and `band:<edge>` (an empty enabled edge). The stored form carries `bands` (an optional
40
+ v1 field).
41
+ - Typing: `PanelKind`, `PanelPlace`, `Where` and `PanelsNote` gain members, and `@n-uf/placement/desk`'s `PresetId`
42
+ gains `"docksFull"` (so `PRESETS` has a fourth key), so an exhaustive `switch` over them, or a `Record` keyed by
43
+ `PresetId`, in a consumer needs the new cases.
44
+ - `@n-uf/placement/desk`: `docks` and `dockTear` get a bottom band (reserved, between the columns); a new preset
45
+ `docksFull` (the bottom band full width). `DeskHint.bottom` starts the band; else the last def whose `sides` names
46
+ `"bottom"`. `navigator` has no band. Because the band is enabled, `docks` and `dockTear` now always offer a
47
+ `band:bottom` drop strip along the pages' bottom, even when the band is empty (a drop there used to be a float in
48
+ `dockTear`, and no drop in `docks`).
49
+ - Known limit: with `pagesMinHeight` below about 100 px on a very short stage, a column can be shorter than its title
50
+ bars, which then run past it (column bars were always a fixed `options.bar` tall). Keep the default 240, or at least
51
+ a few bars' height, when bands are on.
52
+
6
53
  ## 26.11.9
7
54
 
8
55
  - `@n-uf/placement/panels`:
package/README.md CHANGED
@@ -36,9 +36,10 @@ without importing each other: a target list from `@n-uf/placement/targets` is ex
36
36
  `keyboardStops`, one ordered list of places for the arrow keys and a "Move to" menu.
37
37
  - **`/panels`**: tool panels with string ids: full-height columns per side, each with its own width, shown as a
38
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.
39
+ opt-in, when a column runs out of height); resizable floats, some pinned to the view; opt-in top and bottom bands;
40
+ room kept clear for a toolbar the app draws; their drop targets; and a stored form.
41
+ - **`/desk`**: desk presets (`docks`, `docksFull`, `dockTear`, `navigator`) as data on `/panels`: options and a
42
+ start state.
42
43
 
43
44
  Rendering stays in the app: it draws the rects, measures its groups, and feeds pointer samples to a drag intent.
44
45
 
@@ -229,9 +230,79 @@ first. When the open sections' mins and the bars still don't fit, open sections
229
230
  each with a `section-folded` note; that never writes `folded`, so a taller stage unfolds them. Columns without
230
231
  `height` defs lay out as before.
231
232
 
232
- **Changes** are pure: `open`, `close`, `focus`, `toggleFold`, `keep`, `dock(state, id, side, index, { into?, width? })`,
233
+ **Changes** are pure: `open`, `close`, `focus`, `toggleFold`, `keep`, `dock(state, id, side, index, { into?, at?, width? })` (`at` with `into`: before that member, else at the bottom),
233
234
  `float`, `pin(state, id, on, view?, shown?)` (pass the layout's rect as `shown` so a clamped float pins where it
234
- shows), `resizeColumn`, `resizeFloat`, `showTabs`, `activate`.
235
+ shows), `resizeColumn`, `resizeFloat`, `showTabs`, `activate`, and for bands `dockBand`, `resizeBand`.
236
+
237
+ ### Bands
238
+
239
+ A band is a horizontal dock along the stage's top or bottom edge (opt-in: `options.bands`, default none). Its panels
240
+ sit side by side, left to right, and share its width equally; it has one stored height, clamped to its panels' limits
241
+ (`PanelDef.band`, default `BAND_SIZE`: min 120, max 480, natural 200; the largest min and the smallest max win, as a
242
+ column's width does). Each enabled edge says how its band lies:
243
+
244
+ - `mode: "reserved"` shortens the pages; `"overlay"` lies over the pages' upper or lower part (layer 5, over columns'
245
+ layer 0 and under floats) and leaves the pages' rect as it is.
246
+ - `span: "full"` takes its height from the stage first, across the whole width, and the columns and rails stand on it
247
+ (or hang from it); `"between"` runs between the innermost open column (or rail) on each side.
248
+
249
+ A band's open panels are `kind` `"band"` (with `edge` and `index`); `layout.bands` has each band as laid out
250
+ (`PlacedBand`: `rect`, `height`, `min`, `max`, `folded`, `shrunk`, `kept`, `mode`, `span`, `panels`). It folds as a
251
+ whole: `toggleFold` on any of its panels folds or unfolds them all, and a folded band is one bar, `options.bar` tall,
252
+ each panel a `"bar"` side by side. `keep` works on band panels.
253
+
254
+ **Height give-way**: when the pages would be shorter than `pagesMinHeight` (default 240; counted after the top and
255
+ bottom reserves), reserved bands shrink toward their min, then fold to a bar, unkept before kept, in the give-way
256
+ policy's order, bottom before top on a tie (`band-shrunk`, `band-folded`, or `kept-folded` with `edge`); they leave the
257
+ pages room for the overlay bands' bars at least. Then the overlay bands give way the same way, together, until they leave
258
+ `pagesMinHeight` of the pages uncovered, so a top and a bottom one never meet. Give-way never writes `folded`, so a
259
+ taller stage brings a band back. If the pages' rect is still under the min: `pages-short` (overlay bands don't change
260
+ the pages' rect, so they never cause it; how far one covers the pages is its own `band-shrunk` or `band-folded`).
261
+ Columns never give way by height to a band.
262
+
263
+ `dockBand(state, id, edge, index, defs?)` puts a panel into a band (0 the leftmost, past the end the rightmost; a new
264
+ band takes the panel's natural band height); `open(state, id, { edge, height? })` puts a panel placed nowhere at a
265
+ band's right end; `liftPanel`, `dock` and `float` take a panel out, and an emptied band goes from the state.
266
+ `createPanels` takes `top` and `bottom` (the clock counts left, right, top, bottom, floats). A band in the state on an
267
+ edge the options don't enable is left out of `items` (`band-off`). A def's `sides` may name edges: without `sides` a
268
+ panel may go to every enabled edge; with them, only where they say. The drop targets add `band:<edge>:<i>` (the slots
269
+ at a band's ends and between its panels, the band split at its panels' middles) and, on an empty enabled edge,
270
+ `band:<edge>` (a strip `2 × bar` tall along it inside the pages); a drop's `slot` is the panel's share of the band.
271
+
272
+ ```ts
273
+ import { createPanels, layoutPanels, panelTargets, type PanelDef } from "@n-uf/placement/panels";
274
+
275
+ const defs: PanelDef[] = [
276
+ { id: "files", width: { min: 180, natural: 220, max: 360 } },
277
+ { id: "history", width: { min: 200, natural: 240, max: 360 }, band: { min: 100, max: 400, natural: 180 } },
278
+ ];
279
+ const state = createPanels(defs, { left: [["files"]], bottom: ["history"] });
280
+ const options = { bands: { bottom: { mode: "reserved", span: "between" } } } as const;
281
+ const tall = layoutPanels(defs, state, { width: 1200, height: 700 }, options);
282
+ console.log(tall.bands.bottom!.rect, tall.pages); // { x: 220, y: 520, width: 980, height: 180 } { x: 220, y: 0, width: 980, height: 520 }
283
+ const short = layoutPanels(defs, state, { width: 1200, height: 300 }, options);
284
+ console.log(short.notes.map((n) => n.code), short.items.get("history")!.kind); // [ 'band-folded' ] bar
285
+ const targets = panelTargets({ defs, state, id: "files", stage: { width: 1200, height: 700 }, options });
286
+ console.log(targets.filter((t) => t.place.kind === "band").map((t) => t.id)); // [ 'band:bottom:0', 'band:bottom:1' ]
287
+ ```
288
+
289
+ ### Reserved room
290
+
291
+ `options.reserve` (`{ top?, right?, bottom?, left? }`, px, default none) keeps room clear along the pages' edges for a
292
+ toolbar the app draws; `./toolbar` stays the toolbar's model, and placement neither draws nor names it. The reserve
293
+ comes off the pages' area after the columns and bands: `layout.reserved.top` is the pages' full width and `reserve.top`
294
+ tall at their top (bottom likewise), and the left and right reserves are as tall as what the top and bottom reserves
295
+ (and overlay bands) leave. `pagesMin` counts after the left and right reserves, `pagesMinHeight` after the top and
296
+ bottom ones; a reserve never shrinks or folds. An overlay band lies next to the reserve on its edge, on the pages' side, never over it.
297
+
298
+ ```ts
299
+ // The defs and state above; without band options the band is off (a `band-off` note).
300
+ const l = layoutPanels(defs, state, { width: 1200, height: 700 }, { reserve: { top: 40 } });
301
+ console.log(l.reserved.top, l.pages); // { x: 220, y: 0, width: 980, height: 40 } { x: 220, y: 40, width: 980, height: 660 }
302
+ ```
303
+
304
+ Without band options, no `bands` in the state and no reserve, every layout, target and note is what it was before
305
+ bands.
235
306
 
236
307
  **Drop targets**: `panelTargets({ defs, state, id, stage, options?, allows? })` gives `column:<side>:<i>` (a new
237
308
  column as wide as the panel's own width), `into:<side>:<i>` (stack into a column), `rail:<side>` (dock folded) and
@@ -258,13 +329,25 @@ const stops = keyboardStops(targets, { home: { place: homePlace(state, "outline"
258
329
  console.log(stops.map((s) => (s.home ? "home" : s.id))); // [ 'rail:left', 'column:left:0', 'into:left:0', 'home', 'column:right:0', 'rail:right' ]
259
330
  ```
260
331
 
332
+ **Into a stack at a position** (opt-in): with `stackSlots: true` in the input, a stacked column offers
333
+ `into:<side>:<i>:<k>` before each member k as well as `into:<side>:<i>` at its bottom. The column's area is split at
334
+ its members' middles, and each of these targets carries a `marker`: the line where the panel's top lands after the
335
+ drop (the column re-shares its height, so `marker` comes from the drop's layout), zero tall and as wide as the column,
336
+ for the app to draw; `panelDrop(...).slot` is the whole rect. Its `place` is `{ kind: "into", side, index, at }` (`at`
337
+ left out for the bottom), and a drop calls `dock(…, { into: true, at })`. `at` counts closed members, which keep their
338
+ place in the column's list; only members shown get slots. A tabbed column keeps its one `into` target.
339
+ `homePlace(state, id, { stackSlots: true })` says a stacked panel's position as `at`, so `keyboardStops` puts home
340
+ among the column's slots. Without `stackSlots` the targets are exactly as before.
341
+
261
342
  `storePanels(state)` gives `{ v: 1, sides, floats, folded, kept, closed, clock }`; `readPanels(data, defs?)` reads
262
343
  parsed data back and never throws. Anything but an object of version 1 is `{ ok: false, error }`; inside one, unknown
263
344
  or repeated ids and malformed entries are dropped and listed in `dropped`, and widths and float sizes are clamped to
264
345
  the defs. Version 1 only gains optional fields: a field this reader doesn't know is skipped and listed in `dropped`
265
346
  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`):
347
+ `active` are carried; an `active` that isn't one of the column's panels is dropped as `"column.active:<id>"`. `bands`
348
+ is carried too, only when the state has one: an unknown edge is dropped as `"bands.<key>"`, a band without a panels
349
+ list or a numeric height (or whose panels are all dropped) as `"bands.<edge>"`, an unknown field in one as
350
+ `"band.<field>"`, and heights are clamped to the defs. The note codes (`PanelsNoteCode`):
268
351
 
269
352
  | Code | Says | Also |
270
353
  |---|---|---|
@@ -275,17 +358,27 @@ codes (`PanelsNoteCode`):
275
358
  | `pages-under-min` | the pages are under `pagesMin` with every column folded | `width`, `min` |
276
359
  | `float-resized` | a float shrank to fit the view | `items` |
277
360
  | `float-clamped` | a float moved to stay in the view | `items` |
361
+ | `band-shrunk` | a band lowered toward its min: the pages needed the height | `edge`, `items`, `from`, `to` |
362
+ | `band-folded` | a band folded to a bar: the pages needed the height | `edge`, `items` |
363
+ | `kept-folded` | (on a band) a kept band folded, last | `edge`, `items` |
364
+ | `band-off` | a band on an edge the options don't enable: its panels aren't laid out | `edge`, `items` |
365
+ | `pages-short` | the pages' rect is under `pagesMinHeight` with every reserved band folded (or by the reserves alone) | `height`, `min` |
278
366
 
279
367
  ## Desk presets
280
368
 
281
369
  `@n-uf/placement/desk` holds desk presets as data on `./panels`: each is panels options and a start state. It imports
282
370
  only the panels entry, carries no titles or words and wires no toolbar; the app names the presets and draws them.
283
371
 
284
- | Preset | Columns and floats |
372
+ | Preset | Columns, bands and floats |
285
373
  |---|---|
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 |
374
+ | `docks` | one stack column per side, sections at their natural heights when the defs give `height`, folding to the rail; a bottom band between the columns (reserved), folding to a bar on a short stage; no floats |
375
+ | `docksFull` | as `docks`, with the bottom band full width: the columns stand on it |
376
+ | `dockTear` | as `docks` (with its bottom band), and panels may float (tear-off and dock-back); one panel floats at the start |
377
+ | `navigator` | a tabbed column on the left, a stack column on the right; no band, no floats |
378
+
379
+ The bottom band starts with the hint's `bottom` panels (`start(defs, { bottom: [...] })`), else with the last def whose
380
+ `sides` names `"bottom"`; a def without `sides` may be dropped into the band but doesn't start there. `presetTargets`
381
+ and `presetDrop` include the band's targets.
289
382
 
290
383
  ```ts
291
384
  import { PRESETS, presetLayout, presetTargets } from "@n-uf/placement/desk";
@@ -1,8 +1,10 @@
1
1
  import {
2
+ EDGES,
2
3
  SIDES,
4
+ bandSize,
3
5
  defOf,
4
6
  floatSize
5
- } from "./chunk-PWQWD2CS.js";
7
+ } from "./chunk-CJA6LRHN.js";
6
8
 
7
9
  // src/panels/store.ts
8
10
  function storePanels(state) {
@@ -13,9 +15,15 @@ function storePanels(state) {
13
15
  ...c.active !== void 0 ? { active: c.active } : {}
14
16
  });
15
17
  const sides = Object.fromEntries(SIDES.map((s) => [s, state.sides[s].map(column)]));
18
+ const bands = {};
19
+ for (const e of EDGES) {
20
+ const b = state.bands?.[e];
21
+ if (b) bands[e] = { panels: [...b.panels], height: b.height };
22
+ }
16
23
  return {
17
24
  v: 1,
18
25
  sides,
26
+ ...EDGES.some((e) => bands[e]) ? { bands } : {},
19
27
  floats: state.floats.map((f) => ({ id: f.id, rect: { x: f.rect.x, y: f.rect.y, width: f.rect.width, height: f.rect.height }, pinned: !!f.pinned })),
20
28
  folded: [...state.folded],
21
29
  kept: [...state.kept],
@@ -56,7 +64,7 @@ function read(data, defs) {
56
64
  const extra = (o, known, prefix) => {
57
65
  for (const k of Object.keys(o)) if (!known.includes(k)) drop(`${prefix}${k}`);
58
66
  };
59
- extra(data, ["v", "sides", "floats", "folded", "kept", "closed", "clock"], "");
67
+ extra(data, ["v", "sides", "bands", "floats", "folded", "kept", "closed", "clock"], "");
60
68
  extra(data.sides, SIDES, "sides.");
61
69
  const sides = { left: [], right: [] };
62
70
  for (const side of SIDES) {
@@ -86,6 +94,31 @@ function read(data, defs) {
86
94
  sides[side].push({ panels, width: defs ? Math.min(Math.max(width, lo), hi) : Math.max(0, width), ...tabs, ...active });
87
95
  }
88
96
  }
97
+ const bands = {};
98
+ if (data.bands !== void 0) {
99
+ if (!isObject(data.bands)) drop("bands");
100
+ else {
101
+ extra(data.bands, EDGES, "bands.");
102
+ for (const e of EDGES) {
103
+ const b = data.bands[e];
104
+ if (b === void 0) continue;
105
+ if (!isObject(b) || !Array.isArray(b.panels) || !finite(b.height)) {
106
+ drop(`bands.${e}`);
107
+ continue;
108
+ }
109
+ extra(b, ["panels", "height"], "band.");
110
+ const panels = b.panels.map(take).filter((x) => x !== null);
111
+ if (panels.length === 0) {
112
+ drop(`bands.${e}`);
113
+ continue;
114
+ }
115
+ const ds = defs ? panels.map((id) => bandSize(defOf(defs, id))) : [];
116
+ const lo = Math.max(0, ...ds.map((d) => d.min));
117
+ const hi = Math.max(lo, Math.min(...ds.map((d) => d.max)));
118
+ bands[e] = { panels, height: defs ? Math.min(Math.max(b.height, lo), hi) : Math.max(0, b.height) };
119
+ }
120
+ }
121
+ }
89
122
  const floats = [];
90
123
  for (const f of Array.isArray(data.floats) ? data.floats : []) {
91
124
  const r = isObject(f) ? f.rect : void 0;
@@ -128,11 +161,12 @@ function read(data, defs) {
128
161
  clock[id] = isObject(c) && finite(c.opened) && finite(c.used) ? { opened: c.opened, used: c.used } : { opened: 0, used: 0 };
129
162
  }
130
163
  }
131
- return { ok: true, state: { sides, floats, folded: placed("folded"), kept, closed: placed("closed"), clock }, dropped };
164
+ const state = { sides, floats, folded: placed("folded"), kept, closed: placed("closed"), clock };
165
+ return { ok: true, state: EDGES.some((e) => bands[e]) ? { ...state, bands } : state, dropped };
132
166
  }
133
167
 
134
168
  export {
135
169
  storePanels,
136
170
  readPanels
137
171
  };
138
- //# sourceMappingURL=chunk-XUV6MJ2P.js.map
172
+ //# sourceMappingURL=chunk-2H3CSPEH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/panels/store.ts"],"sourcesContent":["/**\n * The panels' stored form: `storePanels(state)` gives plain JSON-safe data (`{ v: 1, … }`); `readPanels(data, defs?)`\n * reads parsed data back and never throws. As with the toolbar's `readArrangement`: anything but an object of\n * version 1 is `{ ok: false, error }`. Inside one, it is lenient: an unknown id (with `defs`), a repeated one or a\n * malformed entry is dropped and listed in `dropped`; widths and float sizes are clamped to the defs. A panel the\n * consumer has but the stored state lacks is no error: the consumer places it (`open` with a place, say).\n *\n * Version 1 only gains optional fields. A field this reader doesn't know (a newer writer's, say) is no error either:\n * it is skipped and reported in `dropped` (`\"<field>\"` at the top, `\"sides.<key>\"`, `\"column.<field>\"`,\n * `\"float.<field>\"`; a clock entry for a panel that isn't placed as `\"clock.<id>\"`), so a consumer can tell that\n * something was left out. A column's `show` (\"tabs\") and `active` are such optional fields: an unknown `show` is\n * dropped as `\"column.show\"`, an `active` that isn't one of the column's panels as `\"column.active:<id>\"`.\n *\n * `bands` (26.11.10) is such an optional field too: `{ top?, bottom? }`, each `{ panels, height }`. An unknown edge is\n * dropped as `\"bands.<key>\"`, a band that isn't an object with a panels list and a numeric height (or whose panels are\n * all dropped) as `\"bands.<edge>\"`, an unknown field in one as `\"band.<field>\"`. Heights are clamped to the defs.\n */\nimport type { Rect } from \"../geometry\";\nimport { EDGES, SIDES, bandSize, defOf, floatSize, type Band, type Clock, type Column, type Edge, type FloatAt, type PanelDef, type PanelsState, type Side } from \"./model\";\n\nexport interface StoredPanels {\n readonly v: 1;\n /** A column's `show` and `active` only when it is tabbed / has one (optional fields of v1). */\n readonly sides: Record<Side, { panels: string[]; width: number; show?: \"tabs\"; active?: string }[]>;\n /** The bands, only when the state has one (an optional field of v1). */\n readonly bands?: Partial<Record<Edge, { panels: string[]; height: number }>>;\n readonly floats: { id: string; rect: Rect; pinned: boolean }[];\n readonly folded: string[];\n readonly kept: string[];\n readonly closed: string[];\n readonly clock: Record<string, Clock>;\n}\n\nexport type PanelsRead = { readonly ok: true; readonly state: PanelsState; readonly dropped: readonly string[] } | { readonly ok: false; readonly error: string };\n\nexport function storePanels(state: PanelsState): StoredPanels {\n const column = (c: Column): StoredPanels[\"sides\"][\"left\"][number] => ({\n panels: [...c.panels],\n width: c.width,\n ...(c.show === \"tabs\" ? { show: \"tabs\" as const } : {}),\n ...(c.active !== undefined ? { active: c.active } : {}),\n });\n const sides = Object.fromEntries(SIDES.map((s) => [s, state.sides[s].map(column)])) as StoredPanels[\"sides\"];\n const bands: NonNullable<StoredPanels[\"bands\"]> = {};\n for (const e of EDGES) {\n const b = state.bands?.[e];\n if (b) bands[e] = { panels: [...b.panels], height: b.height };\n }\n return {\n v: 1,\n sides,\n ...(EDGES.some((e) => bands[e]) ? { bands } : {}),\n floats: state.floats.map((f) => ({ id: f.id, rect: { x: f.rect.x, y: f.rect.y, width: f.rect.width, height: f.rect.height }, pinned: !!f.pinned })),\n folded: [...state.folded],\n kept: [...state.kept],\n closed: [...state.closed],\n clock: Object.fromEntries(Object.entries(state.clock).map(([k, c]) => [k, { opened: c.opened, used: c.used }])),\n };\n}\n\nconst isObject = (x: unknown): x is Record<string, unknown> => typeof x === \"object\" && x !== null && !Array.isArray(x);\nconst finite = (x: unknown): x is number => typeof x === \"number\" && Number.isFinite(x);\n\nexport function readPanels(data: unknown, defs?: readonly PanelDef[]): PanelsRead {\n try {\n return read(data, defs);\n } catch (e) {\n // A getter that throws, a proxy: still no throw.\n return { ok: false, error: `unreadable: ${e instanceof Error ? e.message : typeof e}` };\n }\n}\n\nfunction read(data: unknown, defs?: readonly PanelDef[]): PanelsRead {\n if (!isObject(data)) return { ok: false, error: \"not an object\" };\n // Named without serialising it: a BigInt or a circular value must not throw.\n if (data.v !== 1) return { ok: false, error: `unknown version ${typeof data.v === \"number\" || typeof data.v === \"string\" ? String(data.v) : typeof data.v} (this reads version 1)` };\n if (!isObject(data.sides)) return { ok: false, error: \"sides is missing\" };\n const dropped: string[] = [];\n const drop = (what: string): void => {\n if (!dropped.includes(what)) dropped.push(what);\n };\n const seen = new Set<string>();\n // An id the consumer has, not placed yet.\n const take = (x: unknown): string | null => {\n if (typeof x !== \"string\") {\n drop(`a ${typeof x} where an id goes`);\n return null;\n }\n if ((defs && !defOf(defs, x)) || seen.has(x)) {\n drop(x);\n return null;\n }\n seen.add(x);\n return x;\n };\n // Fields this version doesn't know: skipped, and reported.\n const extra = (o: Record<string, unknown>, known: readonly string[], prefix: string): void => {\n for (const k of Object.keys(o)) if (!known.includes(k)) drop(`${prefix}${k}`);\n };\n extra(data, [\"v\", \"sides\", \"bands\", \"floats\", \"folded\", \"kept\", \"closed\", \"clock\"], \"\");\n extra(data.sides, SIDES, \"sides.\");\n const sides = { left: [] as Column[], right: [] as Column[] };\n for (const side of SIDES) {\n const cols = data.sides[side];\n if (cols === undefined) continue;\n if (!Array.isArray(cols)) {\n drop(`sides.${side}`);\n continue;\n }\n for (const c of cols as unknown[]) {\n if (!isObject(c) || !Array.isArray(c.panels)) {\n drop(`a column on the ${side}`);\n continue;\n }\n extra(c, [\"panels\", \"width\", \"show\", \"active\"], \"column.\");\n const panels = (c.panels as unknown[]).map(take).filter((x): x is string => x !== null);\n if (panels.length === 0) continue;\n const ds = defs ? panels.map((id) => defOf(defs, id)!) : [];\n const lo = Math.max(0, ...ds.map((d) => d.width.min));\n const hi = Math.max(lo, Math.min(...ds.map((d) => d.width.max)));\n const natural = ds.length ? Math.max(...ds.map((d) => d.width.natural)) : 0;\n const width = finite(c.width) ? c.width : natural;\n // `show`: \"tabs\" or \"stack\" (a stack is the default, so not kept); `active`: a panel of this column.\n if (c.show !== undefined && c.show !== \"tabs\" && c.show !== \"stack\") drop(\"column.show\");\n if (c.active !== undefined && !(typeof c.active === \"string\" && panels.includes(c.active))) drop(`column.active:${typeof c.active === \"string\" ? c.active : typeof c.active}`);\n const tabs = c.show === \"tabs\" ? { show: \"tabs\" as const } : {};\n const active = typeof c.active === \"string\" && panels.includes(c.active) ? { active: c.active } : {};\n sides[side].push({ panels, width: defs ? Math.min(Math.max(width, lo), hi) : Math.max(0, width), ...tabs, ...active });\n }\n }\n const bands: Partial<Record<Edge, Band>> = {};\n if (data.bands !== undefined) {\n if (!isObject(data.bands)) drop(\"bands\");\n else {\n extra(data.bands, EDGES, \"bands.\");\n for (const e of EDGES) {\n const b = data.bands[e];\n if (b === undefined) continue;\n if (!isObject(b) || !Array.isArray(b.panels) || !finite(b.height)) {\n drop(`bands.${e}`);\n continue;\n }\n extra(b, [\"panels\", \"height\"], \"band.\");\n const panels = (b.panels as unknown[]).map(take).filter((x): x is string => x !== null);\n if (panels.length === 0) {\n drop(`bands.${e}`);\n continue;\n }\n const ds = defs ? panels.map((id) => bandSize(defOf(defs, id))) : [];\n const lo = Math.max(0, ...ds.map((d) => d.min));\n const hi = Math.max(lo, Math.min(...ds.map((d) => d.max)));\n bands[e] = { panels, height: defs ? Math.min(Math.max(b.height, lo), hi) : Math.max(0, b.height) };\n }\n }\n }\n const floats: FloatAt[] = [];\n for (const f of Array.isArray(data.floats) ? (data.floats as unknown[]) : []) {\n const r = isObject(f) ? f.rect : undefined;\n if (!isObject(f) || !isObject(r) || !finite(r.x) || !finite(r.y) || !finite(r.width) || !finite(r.height)) {\n drop(isObject(f) && typeof f.id === \"string\" ? f.id : \"a float\");\n continue;\n }\n extra(f, [\"id\", \"rect\", \"pinned\"], \"float.\");\n const id = take(f.id);\n if (id === null) continue;\n const min = defs ? floatSize(defOf(defs, id)).min : { width: 0, height: 0 };\n floats.push({ id, rect: { x: r.x, y: r.y, width: Math.max(min.width, r.width), height: Math.max(min.height, r.height) }, ...(f.pinned === true ? { pinned: true } : {}) });\n }\n if (data.floats !== undefined && !Array.isArray(data.floats)) drop(\"floats\");\n // The lists keep only ids that are placed (a closed panel keeps its place).\n const placed = (key: \"folded\" | \"kept\" | \"closed\"): string[] => {\n const x = data[key];\n if (x === undefined) return [];\n if (!Array.isArray(x)) {\n drop(key);\n return [];\n }\n const out: string[] = [];\n for (const id of x as unknown[]) {\n if (typeof id === \"string\" && seen.has(id)) {\n if (!out.includes(id)) out.push(id);\n } else drop(typeof id === \"string\" ? id : `a ${typeof id} in ${key}`);\n }\n return out;\n };\n const floating = new Set(floats.map((f) => f.id));\n // Only a docked panel is kept open.\n const kept = placed(\"kept\").filter((id) => {\n if (floating.has(id)) drop(id);\n return !floating.has(id);\n });\n const clock: Record<string, Clock> = {};\n if (isObject(data.clock)) {\n for (const id of Object.keys(data.clock)) if (!seen.has(id)) drop(`clock.${id}`);\n for (const id of seen) {\n const c = data.clock[id];\n clock[id] = isObject(c) && finite(c.opened) && finite(c.used) ? { opened: c.opened, used: c.used } : { opened: 0, used: 0 };\n }\n }\n const state: PanelsState = { sides, floats, folded: placed(\"folded\"), kept, closed: placed(\"closed\"), clock };\n return { ok: true, state: EDGES.some((e) => bands[e]) ? { ...state, bands } : state, dropped };\n}\n"],"mappings":"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","names":[]}