@pasquelin/panels 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,153 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@pasquelin/panels`.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
6
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] — 2026-09-01
9
+
10
+ Two changes to the same idea: **what a half shows is resolved when it is drawn, not written down
11
+ once**. That is what lets a project declare its panels conditionally without losing anything, and
12
+ what makes a second view cost one prop.
13
+
14
+ ### Added
15
+
16
+ - **Views.** `<Panels view="review">` — a named arrangement. Each view keeps the panels it had
17
+ open to itself; the lengths stay shared, because a column that changed width on the way to
18
+ another view reads as another window. Omitted, everything lands in one view and nothing about
19
+ this is visible. The store gains `view`, `views` and `setView`; the prop is controlled.
20
+ - `DEFAULT_VIEW`, the name a project that never passes one lands in.
21
+ - **`components`**, to draw the chassis' own buttons yourself:
22
+ `<Panels components={{ IconButton: Mine }}>`. The rail's buttons and the close button were the
23
+ one part of the chassis a project could not repaint, so a tooltip — or a shortcut hint, or a
24
+ badge — had nowhere to go but a new prop here. Read once, like `storage`.
25
+ - `shownSpecsIn`, `openOf` and the `Arranged` type, for a project building its own frame on the
26
+ headless core: `shownIn` hands back ids, and every caller was finding the panel again from them.
27
+
28
+ ### Removed
29
+
30
+ - **`PanelsState.open` and `PanelsState.settled`.** One map, `views`, now holds every
31
+ arrangement including the one in front, and having an entry in it IS the record of having been
32
+ settled. Read the view on screen with `openOf(state)`.
33
+ Keeping the front view apart cost three hand-written merges and a `delete`, and the invariant
34
+ they upheld was tenable by nobody: a cold start straight onto a view the stored file had never
35
+ named took it for settled and drew **an empty chassis** — while reaching the same screen from
36
+ another view worked. One map makes the two paths agree by construction.
37
+ - **`shownIn`, `zoneDraws` and `zoneTakesRoom` take `Arranged`** — `{ registry, view, views }` —
38
+ where they took `{ registry, open }`. Passing the whole state still works.
39
+ - **`undraggedSizeOf(zone, spec)`** takes the leading panel rather than a registry and an id.
40
+ - **`readLayout` and `writeLayout` carry `LayoutState`**, which is now `{ views, lengths }`. That
41
+ type was exported, described persistence, and had stopped being true.
42
+ - **`setView` is no longer on `usePanelsActions`.** Inside React the `view` prop is the one path;
43
+ it remains on the store for a project driving the chassis from outside React.
44
+
45
+ ### Changed
46
+
47
+ - **A withdrawn panel no longer empties its half.** It falls back to whatever is still declared
48
+ for that half, and the choice is remembered: declare the panel again and the half is its once
49
+ more. Before, hiding a panel behind a right, a route or a connection lost the arrangement for
50
+ good — while an unknown id read back from storage survived as a half that was neither open nor
51
+ closed. The two cases now answer the same way.
52
+ - **`settle` opens a half without naming a panel.** `ZoneSlots` gains `null` as a third state —
53
+ the key absent is a closed half, `null` an open one that named nobody, an id a real choice.
54
+ Only choices are stored, so an untouched half follows your declarations instead of freezing
55
+ one screen's answer.
56
+ - **`show` on a panel already drawn only focuses it**, rather than writing its name down.
57
+ - **`register` and `unregister` are replaced by `declare(specs)`**, which posts the panels as a
58
+ list. The list IS the order the rail stacks them in, so a panel that goes and comes back
59
+ returns to its place instead of to the end.
60
+ - **Stored layouts are at version 2**, holding one entry per view. A version 1 file is read back
61
+ as the default view — the upgrade costs nobody their layout.
62
+ - **The `view` prop is reconciled on every render**, not on a dependency change. It was
63
+ controlled only when some unrelated prop happened to change identity, so a `defaultOpen`
64
+ written inline and one hoisted into a constant gave opposite contracts.
65
+ - **The layout is written only when the arrangements or the lengths actually moved.** The store
66
+ notifies on every write — a focus, a measure, each `pointermove` of a drag — and each of those
67
+ used to re-serialise the whole file.
68
+
69
+ ### Fixed
70
+
71
+ - **A view named after anything on `Object.prototype`** — `constructor`, `toString`, `__proto__` —
72
+ was taken for settled and drew an empty chassis, then the first click froze the remaining half
73
+ closed for good. `in` answers for the prototype chain; `Object.hasOwn` does not.
74
+ - **A stored view named `__proto__` was lost, and took the map's prototype with it.** `JSON.parse`
75
+ makes it an own property, and assigning it on a plain object fires the setter — so a view named
76
+ `left` would have inherited an arrangement that was not its own. No global pollution, measured.
77
+ - **`setView` and `reset` now settle on the spot.** Reachable only through a render, a view
78
+ arrived at from outside React — a native menu, a socket — stayed unsettled, and a `reset` asked
79
+ for by a button inside the chassis left the frame blank until some ancestor happened to
80
+ re-render. The reset was not written to disk either, so the arrangement being escaped came back
81
+ on reload.
82
+ - **A project bringing its own `store` was never handed the stored layout**, and overwrote the
83
+ file on the first write — losing its arrangement on every launch for having built the store
84
+ itself.
85
+ - **`view` no longer defaults**, so a project that never passes it keeps `setView` for itself. The
86
+ prop claimed the view on every render, undoing an imperative call at the next unrelated render
87
+ of some ancestor.
88
+ - **Replacing a `solo` panel no longer closes the half beside it.** When the solo panel led by
89
+ fallback rather than by choice, showing anything in that zone rebuilt it from nothing.
90
+ - **A zone's divider is re-clamped even when its own length was never dragged.** `resize` and
91
+ `resplit` write different keys, so parting a column without ever moving its edge left the
92
+ divider past the bottom of a shrunken column, squeezing the first half to nothing.
93
+ - **A `defaultOpen` half written `undefined`** was drawn on the first launch and closed after a
94
+ reload: `JSON` drops the key the three-state semantics rests on.
95
+ - **A band drawing nothing still reserved its height.** Two predicates in `clamps.ts` answered
96
+ "is this zone open?" from the stored arrangement alone, without the registry — so a half left
97
+ open on a panel the project no longer declares counted as drawing. The strip held 240 px under
98
+ an empty band, every drag of the top zone was clamped against those 240, and the focus stayed
99
+ on a zone that had stopped drawing. The question is now asked of `zoneTakesRoom`, which knows
100
+ what is declared.
101
+
102
+ ## [0.1.1] — 2026-09-01
103
+
104
+ Everything here is about what leaves the repository. The library itself did not change.
105
+
106
+ ### Fixed
107
+
108
+ - **The published types were empty.** `dist/index.d.ts` contained `export { }` — the package
109
+ typechecked in this repository and gave a consumer nothing at all. Declarations are emitted by
110
+ `tsc` now, not by a bundler plugin.
111
+ - **`@pasquelin/panels/dockview` did not exist.** `exports` promised the subpath and the build
112
+ never produced it; importing it failed at install time.
113
+ - **The stylesheet weighed 137 kB.** Dockview's own sheet had been merged into it, so every
114
+ project paid for tabs it may never open. It belongs to Dockview, and the consumer loads it.
115
+ Now 5.2 kB.
116
+ - **The types named `zustand`.** It is bundled, so no consumer has that package installed and
117
+ their typecheck failed with TS2307 on a package that advertises no dependencies. The store's
118
+ surface is declared directly.
119
+ - **The types named a stylesheet path that does not exist**, failing with TS2882 without
120
+ `skipLibCheck`. The sheet has its own entry and `index.ts` no longer pulls it in.
121
+
122
+ ### Changed
123
+
124
+ - Document tabs follow IA Studio's design, rule for rule: only the visible tab carries a
125
+ background, tabs take the surfaces' radius and are parted by a margin, the title yields and the
126
+ close button never does, and that button is a disc shown under the pointer.
127
+
128
+ ### Added
129
+
130
+ - `--pnl-surface` (between the panel and its hovered state) and `--pnl-tab`, which the tab strip
131
+ needs and the chassis had no equivalent for.
132
+
133
+ ## [0.1.0] — 2026-09-01
134
+
135
+ First release.
136
+
137
+ - Five zones — `left`, `right`, `top`, `bottomLeft`, `bottomRight` — each cut in two halves, with
138
+ icon rails on the edges and a centre that is the project's own.
139
+ - Resize by pointer or keyboard, clamped so the centre keeps its floor whether or not anything
140
+ was ever dragged.
141
+ - The chassis measures its **container**, never the window, so it can live inside a page that
142
+ already exists.
143
+ - Layout persisted to `localStorage` by default, or to any adapter.
144
+ - Headless underneath: every component is built on hooks that render nothing, and each piece is
145
+ exported and replaceable on its own.
146
+ - Every visual value is a CSS custom property, declared at zero specificity so a project's
147
+ palette always wins.
148
+ - Optional `@pasquelin/panels/dockview` entry point for document tabs.
149
+ - No runtime dependencies.
150
+
151
+ [0.2.0]: https://github.com/pasquelin/panels/releases/tag/v0.2.0
152
+ [0.1.1]: https://github.com/pasquelin/panels/releases/tag/v0.1.1
153
+ [0.1.0]: https://github.com/pasquelin/panels/releases/tag/v0.1.0
package/README.md CHANGED
@@ -154,6 +154,19 @@ For coding agents: [`llms.txt`](llms.txt) is the mental model, the API and the t
154
154
  page; [`llms-full.txt`](llms-full.txt) is every English chapter concatenated. Both ship with the
155
155
  package, and `pnpm llms` regenerates the second from the first plus `docs/`.
156
156
 
157
+ ## Releasing
158
+
159
+ `main` is production; `develop` is where work lands. A release is a tag:
160
+
161
+ ```bash
162
+ npm version patch # or minor / major — bumps, commits, tags
163
+ git push --follow-tags
164
+ ```
165
+
166
+ The tag triggers `release.yml`, which checks that it matches `package.json`, replays
167
+ `pnpm validate`, builds, and publishes to npm with a signed provenance attestation — over OIDC,
168
+ with no token anywhere. See [CHANGELOG.md](CHANGELOG.md) for what shipped.
169
+
157
170
  ## Licence
158
171
 
159
172
  **MIT** — see [LICENSE](LICENSE).
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type PanelsProviderProps } from '../core/context';
3
+ import { type PanelsComponents } from '../core/components';
3
4
  import { type PanelsLabels } from './labels';
4
5
  export type PanelsProps<Id extends string = string> = Omit<PanelsProviderProps<Id>, 'children'> & {
5
6
  /** Above the rails, full width. The project's own — the library imposes no chrome. */
@@ -10,6 +11,11 @@ export type PanelsProps<Id extends string = string> = Omit<PanelsProviderProps<I
10
11
  railHeader?: ReactNode;
11
12
  /** Words the chassis says. English by default; pass your own already translated. */
12
13
  labels?: Partial<PanelsLabels>;
14
+ /**
15
+ * Pieces the project draws itself — today the `IconButton` the rail and the panel headers use.
16
+ * Read ONCE, like `storage`: swapping it under a live chassis would re-render every panel.
17
+ */
18
+ components?: Partial<PanelsComponents>;
13
19
  /**
14
20
  * Forces the palette. Left out, the chassis follows the reader's system setting — which is
15
21
  * what a project wants until it offers a switch of its own.
@@ -23,7 +29,7 @@ export type PanelsProps<Id extends string = string> = Omit<PanelsProviderProps<I
23
29
  * The chassis: icon rails stuck to the edges, rounded panels laid over the gutter, a free centre,
24
30
  * and the project's own header and footer around them.
25
31
  */
26
- export declare function Panels<Id extends string = string>({ header, footer, railHeader, labels, theme, className, children, ...provider }: PanelsProps<Id>): import("react").JSX.Element;
32
+ export declare function Panels<Id extends string = string>({ header, footer, railHeader, labels, theme, className, components, children, ...provider }: PanelsProps<Id>): import("react").JSX.Element;
27
33
  export declare namespace Panels {
28
34
  var Panel: typeof import("./Panel").Panel;
29
35
  var Center: typeof import("./Center").Center;
@@ -1,4 +1,4 @@
1
- import { type Lengths, type OpenByZone, type Zone } from './types';
1
+ import { type Lengths, type Zone } from './types';
2
2
  /** Smallest a zone may be dragged to before it is worth closing instead. */
3
3
  export declare const MIN_SIZE = 140;
4
4
  /** Room the centre must keep, whatever the side zones ask for. */
@@ -24,22 +24,24 @@ export declare const OPPOSITE: Record<Zone, Zone>;
24
24
  export declare function fitZoneSize(size: number, available: number, opposite: number): number;
25
25
  /** Same idea one level down: neither half of a zone may swallow the other. */
26
26
  export declare function fitSplit(size: number, available: number): number;
27
- /** True once either half holds something: an empty zone takes no room at all. */
28
- export declare function isZoneOpen(open: OpenByZone, zone: Zone): boolean;
29
- /** The band takes its height as soon as EITHER half holds something: the strip is one strip. */
30
- export declare function isBandOpen(open: OpenByZone): boolean;
31
27
  /**
32
28
  * The room a zone currently takes, or zero when it draws nothing. Read while clamping the
33
29
  * opposite zone: under-report it and the other side may be dragged over room this one is
34
30
  * already drawing in, squeezing the centre past its floor.
31
+ *
32
+ * 🛑 `takesRoom` is asked rather than read off `open`, and that is not a style: whether a half
33
+ * draws depends on the REGISTRY too — one holding a panel the project no longer declares draws
34
+ * whatever is still declared for it, or nothing. This module has no registry, and the two
35
+ * predicates it used to carry answered `true` for a half that drew nothing at all: a band
36
+ * reserving 240 px under an empty strip, and every drag of `top` clamped against those 240.
35
37
  */
36
- export declare function sizeOf(lengths: Lengths, open: OpenByZone, zone: Zone, undragged: (zone: Zone) => number): number;
38
+ export declare function sizeOf(lengths: Lengths, zone: Zone, takesRoom: (zone: Zone) => boolean, undragged: (zone: Zone) => number): number;
37
39
  /**
38
40
  * Every stored length, re-clamped to a container of this size. Sizes are persisted, so a layout
39
41
  * set on a wide screen would otherwise overflow a narrow one — pushing the panels under the
40
42
  * rails and squeezing the centre to nothing.
41
43
  */
42
- export declare function fitted(lengths: Lengths, open: OpenByZone, width: number, height: number, undragged: (zone: Zone) => number): Lengths;
44
+ export declare function fitted(lengths: Lengths, width: number, height: number, takesRoom: (zone: Zone) => boolean, undragged: (zone: Zone) => number): Lengths;
43
45
  /**
44
46
  * The two side zones, bounded so the centre keeps its floor — whether or not either was ever
45
47
  * dragged.
@@ -0,0 +1,18 @@
1
+ import { type ComponentType } from 'react';
2
+ import { type IconButtonProps } from '../components/IconButton';
3
+ /**
4
+ * The pieces a project may draw itself, where the chassis would otherwise draw them in the
5
+ * middle of a rail it owns.
6
+ *
7
+ * One entry today, and the shape is what lets there be more without breaking anyone. The
8
+ * library keeps its promise by NOT growing props for what a design system owns — a tooltip, a
9
+ * shortcut hint, a badge: those live on the button, and the button is replaceable.
10
+ */
11
+ export type PanelsComponents = {
12
+ /** The rail's buttons and the panel header's close button. See `IconButtonProps`. */
13
+ IconButton: ComponentType<IconButtonProps>;
14
+ };
15
+ export declare const ComponentsProvider: import("react").Provider<PanelsComponents>;
16
+ /** Defaults outside a chassis, so `<Rail>` and `<PanelFrame>` stand on their own. */
17
+ export declare function usePanelsComponents(): PanelsComponents;
18
+ export declare function withDefaults(components: Partial<PanelsComponents> | undefined): PanelsComponents;
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type PanelsState, type PanelsStore } from './store';
3
3
  import { type LayoutStorage } from './persistence';
4
- import type { OpenByZone } from './types';
4
+ import { type OpenByZone } from './types';
5
5
  export type PanelsProviderProps<Id extends string> = {
6
6
  /**
7
7
  * A store built by the project. Pass one to drive the chassis from OUTSIDE React — a socket
@@ -13,11 +13,21 @@ export type PanelsProviderProps<Id extends string> = {
13
13
  storageKey?: string;
14
14
  /** Where the layout is kept. `localStorage` by default; `null` disables persistence. */
15
15
  storage?: LayoutStorage | null;
16
- /** Which halves start open, overriding "the first panel declared for that half". */
16
+ /** Which halves start open, overriding "every half something is declared for". */
17
17
  defaultOpen?: OpenByZone<Id>;
18
+ /**
19
+ * The view in front. Each view keeps its OWN open panels, so two parts of one application can
20
+ * arrange their columns differently — and find them as they left them on the way back.
21
+ *
22
+ * The lengths are shared on purpose: a column that changed width on the way to another view
23
+ * reads as another window.
24
+ *
25
+ * Left out, everything lands in one view and nothing about this is visible.
26
+ */
27
+ view?: string;
18
28
  children: ReactNode;
19
29
  };
20
- export declare function PanelsProvider<Id extends string = string>({ store, storageKey, storage, defaultOpen, children, }: PanelsProviderProps<Id>): import("react").JSX.Element;
30
+ export declare function PanelsProvider<Id extends string = string>({ store, storageKey, storage, defaultOpen, view, children, }: PanelsProviderProps<Id>): import("react").JSX.Element;
21
31
  /** The store this chassis runs on. Throws outside a provider rather than answering nothing. */
22
32
  export declare function usePanelsStore<Id extends string = string>(): PanelsStore<Id>;
23
33
  /** A slice of the chassis state, subscribed. */
@@ -1,21 +1,23 @@
1
- import { type OpenByZone, type PanelSpec, type Zone } from '../types';
1
+ import { type Arranged } from '../store';
2
+ import { type PanelSpec, type Zone } from '../types';
2
3
  /**
3
- * What every reader of `shownIn` and `zoneDraws` needs: the declared panels, and which half of
4
- * each zone holds which.
4
+ * What every reader of `shownIn` and `zoneDraws` needs: the declared panels, and the view whose
5
+ * arrangement they are about.
5
6
  *
6
- * One hook because four of them had written the same pair of selectors and rebuilt the same
7
- * object by hand — so the shape those two functions expect could not change without touching
8
- * four files.
7
+ * One hook because four of them had written the same selectors and rebuilt the same object by
8
+ * hand — so the shape those functions expect could not change without touching four files.
9
9
  */
10
- export declare function useArrangement<Id extends string = string>(): {
11
- registry: PanelSpec<Id>[];
12
- open: OpenByZone<Id>;
13
- };
10
+ export declare function useArrangement<Id extends string = string>(): Arranged<Id>;
14
11
  /** What a zone's two halves actually draw — a `solo` panel silences the other. */
15
12
  export declare function useShownIn<Id extends string = string>(zone: Zone): {
16
13
  primary?: Id;
17
14
  secondary?: Id;
18
15
  };
16
+ /** The same answer as specs, for a caller that needs more of the panel than its name. */
17
+ export declare function useShownSpecsIn<Id extends string = string>(zone: Zone): {
18
+ primary?: PanelSpec<Id>;
19
+ secondary?: PanelSpec<Id>;
20
+ };
19
21
  /** Whether a zone draws at all. An empty one takes neither room nor handle. */
20
22
  export declare function useZoneDraws<Id extends string = string>(zone: Zone): boolean;
21
23
  /**
@@ -1,4 +1,4 @@
1
- import { type Lengths, type OpenByZone } from './types';
1
+ import { type LayoutState } from './types';
2
2
  /**
3
3
  * Where a layout is kept. `localStorage` by default, so the library works the moment it is
4
4
  * installed; a project that stores elsewhere — a file, an API, `electron-store` — passes its own.
@@ -13,18 +13,23 @@ export declare const memoryStorage: () => LayoutStorage;
13
13
  * a layout that cannot be saved is not a reason to take the application down.
14
14
  */
15
15
  export declare const browserStorage: () => LayoutStorage;
16
- /** Bumped when the stored shape stops being one this build can restore. */
17
- export declare const LAYOUT_VERSION = 1;
16
+ /**
17
+ * Bumped when the stored shape stops being one this build can restore.
18
+ *
19
+ * 2 keeps the views apart. A version 1 file held one arrangement under `open`, which is read
20
+ * back as the view in front — nobody loses a layout to the upgrade.
21
+ */
22
+ export declare const LAYOUT_VERSION = 2;
18
23
  /**
19
24
  * Reads a stored layout back, dropping anything this build cannot make sense of. Returns
20
25
  * `undefined` rather than a partial answer: a half-read layout is worse than none, since the
21
26
  * project's own defaults are a deliberate arrangement and a corrupted one is not.
27
+ *
28
+ * A view the file does not name is simply absent, which is what tells the store to settle it —
29
+ * so arriving straight at a view nobody has arranged yet opens its halves.
30
+ *
31
+ * `view` is the one starting in front, and it only matters for a version 1 file: that one held a
32
+ * single arrangement, and this says which view inherits it.
22
33
  */
23
- export declare function readLayout<Id extends string>(storage: LayoutStorage, key: string): {
24
- open: OpenByZone<Id>;
25
- lengths: Lengths;
26
- } | undefined;
27
- export declare function writeLayout<Id extends string>(storage: LayoutStorage, key: string, layout: {
28
- open: OpenByZone<Id>;
29
- lengths: Lengths;
30
- }): void;
34
+ export declare function readLayout<Id extends string>(storage: LayoutStorage, key: string, view?: string): LayoutState<Id> | undefined;
35
+ export declare function writeLayout<Id extends string>(storage: LayoutStorage, key: string, layout: LayoutState<Id>): void;
@@ -1,9 +1,22 @@
1
- import { type Lengths, type OpenByZone, type PanelSpec, type Slot, type Zone, type ZoneSlots } from './types';
1
+ import { type Lengths, type OpenByZone, type PanelSpec, type LayoutState, type Slot, type Zone, type ZoneSlots } from './types';
2
2
  export type PanelsState<Id extends string = string> = {
3
- /** Panels declared by the project, in the order they mounted. The rail reads this. */
3
+ /** Panels declared by the project, in the order it declares them. The rail reads this. */
4
4
  registry: PanelSpec<Id>[];
5
- open: OpenByZone<Id>;
5
+ /** Name of the view in front. One view is all a project needs until it has two. */
6
+ view: string;
7
+ /**
8
+ * Every view's arrangement — which half of which zone holds what — the one in front included.
9
+ * A view is settled once it has an entry here, which is what `settle` writes and `reset`
10
+ * clears: no second field can then disagree about which views have been opened.
11
+ */
12
+ views: Record<string, OpenByZone<Id>>;
6
13
  lengths: Lengths;
14
+ /**
15
+ * Halves the project wants open whatever the panels say, as last passed to `settle`. Held so
16
+ * that a view arriving later — or a `reset` — is settled the same way the first one was,
17
+ * without waiting for React to hand them over again.
18
+ */
19
+ defaults?: OpenByZone<Id>;
7
20
  /** Last clicked zone: the one whose rail icon gets accented. */
8
21
  focusedZone: Zone | null;
9
22
  /**
@@ -11,8 +24,6 @@ export type PanelsState<Id extends string = string> = {
11
24
  * by itself days later, on an arrangement nobody remembers making, is not a restoration.
12
25
  */
13
26
  stashed: Partial<Record<Zone, ZoneSlots<Id>>>;
14
- /** Whether the opening arrangement has been settled — see `settle`. */
15
- settled: boolean;
16
27
  /**
17
28
  * The room the zones and the centre share, as last measured. Held in the store rather than
18
29
  * read per component: every zone has to be bounded against the SAME number, and against what
@@ -22,10 +33,22 @@ export type PanelsState<Id extends string = string> = {
22
33
  width: number;
23
34
  height: number;
24
35
  };
25
- register: (spec: PanelSpec<Id>) => void;
26
- unregister: (id: Id) => void;
27
- /** Opens each untouched half on the first panel declared for it. Runs once. */
36
+ /**
37
+ * The panels the project declares, in the order the rail stacks them. Posted whole rather
38
+ * than one at a time: the list IS the order, so a panel that goes and comes back — behind a
39
+ * right, a route, a connection — returns to its place instead of to the end.
40
+ */
41
+ declare: (specs: PanelSpec<Id>[]) => void;
42
+ /**
43
+ * Opens each untouched half of the view in front, naming no panel. Runs once per view — and
44
+ * remembers `defaults`, so the views that come later open the same way.
45
+ */
28
46
  settle: (defaults?: OpenByZone<Id>) => void;
47
+ /**
48
+ * Brings another view forward, putting the one in front away as it stands. Its panels come
49
+ * back untouched when it returns; the lengths are shared, so no column changes width.
50
+ */
51
+ setView: (view: string) => void;
29
52
  show: (id: Id) => void;
30
53
  close: (zone: Zone, slot: Slot) => void;
31
54
  toggle: (id: Id) => void;
@@ -54,17 +77,50 @@ export type PanelsStore<Id extends string = string> = {
54
77
  setState: (partial: Partial<PanelsState<Id>> | ((state: PanelsState<Id>) => Partial<PanelsState<Id>>), replace?: false) => void;
55
78
  subscribe: (listener: (state: PanelsState<Id>, previous: PanelsState<Id>) => void) => () => void;
56
79
  };
80
+ /**
81
+ * What the readers below need of the state: the declared panels, and the arrangement of the view
82
+ * in front. Named because five exported functions take it, and `Pick` written out five times was
83
+ * five places to edit the day the shape moved.
84
+ */
85
+ export type Arranged<Id extends string = string> = {
86
+ registry: PanelSpec<Id>[];
87
+ view: string;
88
+ /**
89
+ * 🛑 Only the entry named by `view` is ever read. Written out rather than `Pick`ed off
90
+ * `PanelsState`, whose own `views` promises EVERY arrangement: a caller handed this one — the
91
+ * hooks build it from the view on screen alone — would have read that promise and believed it.
92
+ */
93
+ views: Record<string, OpenByZone<Id>>;
94
+ };
95
+ /** The arrangement of the view in front. Empty until that view has been settled. */
96
+ export declare function openOf<Id extends string>(state: Arranged<Id>): OpenByZone<Id>;
97
+ /**
98
+ * Whether that view has been opened before. 🛑 `hasOwn`, never `in`: `'constructor' in {}` is
99
+ * TRUE, so a view named after anything on `Object.prototype` was taken for settled and its
100
+ * chassis drew nothing at all.
101
+ */
102
+ export declare function isSettled<Id extends string>(state: Arranged<Id>, view: string): boolean;
57
103
  /**
58
104
  * A panel by its id. Exported because four sites had written this same lookup for want of it,
59
105
  * and one of them had already drifted on how it treated `undefined`. It is also the single place
60
106
  * to change the day the registry stops being a list and becomes a `Map`.
107
+ *
108
+ * Takes `null` as well, which is a half open on no panel in particular — see `ZoneSlots`.
61
109
  */
62
- export declare function specOf<Id extends string>(registry: PanelSpec<Id>[], id: Id | undefined): PanelSpec<Id> | undefined;
110
+ export declare function specOf<Id extends string>(registry: PanelSpec<Id>[], id: Id | null | undefined): PanelSpec<Id> | undefined;
63
111
  /**
64
- * Both halves at once, because one can silence the other: a `solo` panel takes the zone WHOLE.
65
- * Resolved here rather than in each reader, which would contradict it.
112
+ * The SPECS both halves draw, because one can silence the other: a `solo` panel takes the zone
113
+ * WHOLE. Resolved here rather than in each reader, which would contradict it.
114
+ *
115
+ * Rendering it as specs and not as ids is what stops the lookup happening twice: every caller
116
+ * wanted the panel, and each was finding it again from the id this used to hand back.
66
117
  */
67
- export declare function shownIn<Id extends string>(state: Pick<PanelsState<Id>, 'registry' | 'open'>, zone: Zone): {
118
+ export declare function shownSpecsIn<Id extends string>(state: Arranged<Id>, zone: Zone): {
119
+ primary?: PanelSpec<Id>;
120
+ secondary?: PanelSpec<Id>;
121
+ };
122
+ /** The same answer as ids, for the callers that only name a panel. */
123
+ export declare function shownIn<Id extends string>(state: Arranged<Id>, zone: Zone): {
68
124
  primary?: Id;
69
125
  secondary?: Id;
70
126
  };
@@ -76,23 +132,26 @@ export declare function shownIn<Id extends string>(state: Pick<PanelsState<Id>,
76
132
  * half, the top zone was told nothing faced it whenever `bottomRight` happened to be the closed
77
133
  * one — and it could then be dragged over the height `bottomLeft` was already drawing in.
78
134
  */
79
- export declare function zoneTakesRoom<Id extends string>(state: Pick<PanelsState<Id>, 'registry' | 'open'>, zone: Zone): boolean;
135
+ export declare function zoneTakesRoom<Id extends string>(state: Arranged<Id>, zone: Zone): boolean;
80
136
  /** Whether the zone draws at all — an empty one takes neither room nor handle. */
81
- export declare function zoneDraws<Id extends string>(state: Pick<PanelsState<Id>, 'registry' | 'open'>, zone: Zone): boolean;
137
+ export declare function zoneDraws<Id extends string>(state: Arranged<Id>, zone: Zone): boolean;
82
138
  /**
83
139
  * The size a zone opens at, given the panel leading it. A panel may ask for more than the zone's
84
140
  * own default — a conversation at 260 wraps every sentence onto three lines.
85
141
  *
86
- * Takes the LEADING panel rather than resolving it: every caller has already asked `shownIn`,
87
- * and resolving it again here was a second pass over the registry per zone, per drag frame.
142
+ * Takes the leading SPEC rather than its id: every caller has already asked `shownSpecsIn`, and
143
+ * finding the panel again here was a second pass over the registry per zone, per drag frame.
88
144
  */
89
- export declare function undraggedSizeOf<Id extends string>(registry: PanelSpec<Id>[], zone: Zone, leading: Id | undefined): number;
145
+ export declare function undraggedSizeOf<Id extends string>(zone: Zone, leading: PanelSpec<Id> | undefined): number;
90
146
  export declare const EMPTY_LENGTHS: Lengths;
91
147
  export type CreatePanelsStoreOptions<Id extends string> = {
92
- /** Restored layout, if any. Halves it names are taken as chosen and `settle` leaves them be. */
93
- initial?: Partial<{
94
- open: OpenByZone<Id>;
95
- lengths: Lengths;
96
- }>;
148
+ /** The view that starts in front. A project with one view never has to name it. */
149
+ view?: string;
150
+ /**
151
+ * Restored layout, if any. A view it carries is taken as settled and `settle` leaves it be — a
152
+ * view it does NOT carry is settled on first sight, so arriving straight at one nobody has
153
+ * arranged yet opens its halves rather than drawing an empty frame.
154
+ */
155
+ initial?: Partial<LayoutState<Id>>;
97
156
  };
98
157
  export declare function createPanelsStore<Id extends string = string>(options?: CreatePanelsStoreOptions<Id>): PanelsStore<Id>;
@@ -18,6 +18,11 @@ export declare const ZONES: readonly Zone[];
18
18
  */
19
19
  export type Slot = 'primary' | 'secondary';
20
20
  export declare const SLOTS: readonly Slot[];
21
+ /**
22
+ * The view a project that never names one lands in. A view is a named arrangement: two parts of
23
+ * one application each keeping their own open panels, sharing the lengths.
24
+ */
25
+ export declare const DEFAULT_VIEW = "default";
21
26
  /** The band's two halves, in the order they are drawn. */
22
27
  export declare const BOTTOM_ZONES: readonly Zone[];
23
28
  /** Whether the zone is one of the band's halves, which share a height and a resize handle. */
@@ -64,8 +69,15 @@ export type PanelSpec<Id extends string = string> = {
64
69
  /** Takes the zone WHOLE: shown, the other half draws nothing. `primary` only. */
65
70
  solo?: boolean;
66
71
  };
67
- /** Which panel each half of each zone currently shows. Absent means the half is closed. */
68
- export type ZoneSlots<Id extends string = string> = Partial<Record<Slot, Id>>;
72
+ /**
73
+ * Which panel each half of each zone currently shows. Three states, not two: the key absent is
74
+ * a CLOSED half, `null` an open one that has named no panel, an id the panel someone chose.
75
+ *
76
+ * That middle state is what lets a project change the panels it declares without the halves
77
+ * following: an unnamed half draws whatever is declared first for it, resolved at render — see
78
+ * `shownIn`. Stored, it also keeps the arrangement honest, since only a real choice is written.
79
+ */
80
+ export type ZoneSlots<Id extends string = string> = Partial<Record<Slot, Id | null>>;
69
81
  export type OpenByZone<Id extends string = string> = Partial<Record<Zone, ZoneSlots<Id>>>;
70
82
  export type SizesByZone = Partial<Record<Zone, number>>;
71
83
  /**
@@ -81,8 +93,11 @@ export type Lengths = {
81
93
  */
82
94
  bandSplit?: number;
83
95
  };
84
- /** The layout as it is stored and restored. This is the whole of what persistence carries. */
96
+ /**
97
+ * The layout as it is stored and restored. This is the whole of what persistence carries: one
98
+ * arrangement per view, and the lengths, which every view shares.
99
+ */
85
100
  export type LayoutState<Id extends string = string> = {
86
- open: OpenByZone<Id>;
101
+ views: Record<string, OpenByZone<Id>>;
87
102
  lengths: Lengths;
88
103
  };