@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/llms-full.txt CHANGED
@@ -20,14 +20,23 @@
20
20
  provider its own file sits under.
21
21
  - **One store per chassis**, created not imported (`createPanelsStore`). No module singleton —
22
22
  two chassis can live in one application, and a test cannot leak into the next.
23
- - **What is stored is not what is shown.** Stored: which panel each half holds, and the sizes.
24
- Resolved at render: what a zone draws (a `solo` panel silences the other half), the size it
25
- takes (bounded against the opposite zone), whether it takes room at all (the band's two halves
26
- share one height).
27
- - **There are no "surfaces" or sections.** An earlier design carried them; they were removed. In
28
- React the router changes the children there is nothing to solve.
23
+ - **What is stored is not what is shown, and only a CHOICE is stored.** Stored: which panel each
24
+ half was told to hold, per view, and the sizes. A half nobody touched is stored open, naming
25
+ nobody, and a view nobody has opened is simply absent which is what tells the chassis to
26
+ settle it. Resolved at render: which panel a half draws (the one named if it is still declared for
27
+ that half, else the first one that is), whether the zone draws at all, the size it takes
28
+ (bounded against the opposite zone), whether it takes room (the band's two halves share one
29
+ height).
30
+ - **No tooltips, no icon set, no i18n.** The two buttons the chassis draws — the rail's and the
31
+ close — are REPLACED, not configured: `<Panels components={{ IconButton: Mine }}>`. That is
32
+ what keeps `tooltip`, `shortcut` and `badge` from landing here one after the other.
29
33
  - **A panel that should not be offered is a panel you do not declare.** That replaces what a
30
- capability system would be for.
34
+ capability system would be for — and withdrawing one costs nothing: the half falls back, the
35
+ choice is remembered, and declaring the panel again gives it its place back.
36
+ - **Views are named arrangements, and they are opt-in.** `<Panels view="review">`: each view
37
+ keeps the panels it had open to itself, the lengths stay shared. Left out, everything lands in
38
+ one view and nothing about this is visible. Not "surfaces": the library still knows nothing
39
+ about your domain, only that one application can have two screens worth arranging apart.
31
40
 
32
41
  ## Install and shape
33
42
 
@@ -75,8 +84,8 @@ usual missing piece.
75
84
  `PanelHeader` `IconButton` `Separator` `ResizeHandle`
76
85
 
77
86
  ### `<Panels>` props
78
- `store` `storageKey` `storage` `defaultOpen` `header` `footer` `railHeader` `labels` `theme`
79
- `className`
87
+ `store` `storageKey` `storage` `defaultOpen` `view` `header` `footer` `railHeader` `labels`
88
+ `components` `theme` `className`
80
89
 
81
90
  ### `<Panel>` props
82
91
  `id` `zone` `slot?` `title` `icon?` `actions?` `opens?` `solo?`
@@ -84,14 +93,17 @@ usual missing piece.
84
93
  ### Hooks
85
94
  - `usePanels<Id>()` → `{ panels, reveal, close, toggle, isShown, focusedZone, reset }`
86
95
  - `useZone<Id>(zone)` → `{ primary, secondary, draws, size, split, focused }`
87
- - `useArrangement` `useShownIn` `useZoneDraws` `useZoneTakesRoom` `useBandHalves`
96
+ - `useArrangement` `useShownIn` `useShownSpecsIn` `useZoneDraws` `useZoneTakesRoom`
97
+ `useBandHalves`
88
98
  - `useContainerFit(ref)` `usePointerDrag()` `usePanelsState(selector)` `usePanelsActions()`
89
99
 
90
100
  ### Core
91
- `createPanelsStore` `shownIn` `specOf` `zoneDraws` `zoneTakesRoom` `undraggedSizeOf`
101
+ `createPanelsStore` `shownIn` `shownSpecsIn` `openOf` `specOf` `zoneDraws` `zoneTakesRoom`
102
+ `undraggedSizeOf`
92
103
  `fitZoneSize` `fitSplit` `sharedSizes` `sizeKeyOf` `MIN_SIZE` `MIN_CENTER` `MIN_SPLIT`
93
104
  `DEFAULT_SIZES` · `browserStorage` `memoryStorage` `readLayout` `writeLayout` `LAYOUT_VERSION`
94
- · `ZONES` `SLOTS` `BOTTOM_ZONES` `ZONES_BY_SIDE` `isBottom` `isHorizontal` `isLeading` `cx`
105
+ · `ZONES` `SLOTS` `BOTTOM_ZONES` `ZONES_BY_SIDE` `DEFAULT_VIEW` `isBottom` `isHorizontal`
106
+ `isLeading` `cx`
95
107
 
96
108
  ### Optional entry point
97
109
  `@pasquelin/panels/dockview` → `DockviewCenter`, for document tabs. Separate so its weight only
@@ -130,6 +142,14 @@ Gauges: `--pnl-rail` `--pnl-rail-button` `--pnl-rail-inset` `--pnl-gutter` `--pn
130
142
  write, so subscribing to either as an object re-renders on every frame of a drag.
131
143
  - `close(id)` closes THAT panel or nothing. `close(zone, slot)` on the store empties the half
132
144
  whatever stands in it — two panels share a half.
145
+ - `show(id)` on a panel already on screen only FOCUSES it. A half drawing it by declaration
146
+ order has named nobody, and writing the name down would settle for every other view a question
147
+ the click never asked.
148
+ - The `view` prop is CONTROLLED: `setView` on the store is overruled by the next render. It is
149
+ reconciled on every render, not on a dependency change, so the contract does not depend on
150
+ which other props you happened to memoise.
151
+ - Views are never evicted. `view={location.pathname}` grows one entry per URL, for ever. Name
152
+ the handful of screens that own an arrangement, not every route.
133
153
  - `useZoneTakesRoom` ≠ `useZoneDraws`: the band's halves share a height, so either drawing means
134
154
  the strip takes it.
135
155
  - Two chassis in one app need two `storageKey`s, or they overwrite each other.
@@ -309,6 +329,19 @@ For coding agents: [`llms.txt`](llms.txt) is the mental model, the API and the t
309
329
  page; [`llms-full.txt`](llms-full.txt) is every English chapter concatenated. Both ship with the
310
330
  package, and `pnpm llms` regenerates the second from the first plus `docs/`.
311
331
 
332
+ ## Releasing
333
+
334
+ `main` is production; `develop` is where work lands. A release is a tag:
335
+
336
+ ```bash
337
+ npm version patch # or minor / major — bumps, commits, tags
338
+ git push --follow-tags
339
+ ```
340
+
341
+ The tag triggers `release.yml`, which checks that it matches `package.json`, replays
342
+ `pnpm validate`, builds, and publishes to npm with a signed provenance attestation — over OIDC,
343
+ with no token anywhere. See [CHANGELOG.md](CHANGELOG.md) for what shipped.
344
+
312
345
  ## Licence
313
346
 
314
347
  **MIT** — see [LICENSE](LICENSE).
@@ -387,8 +420,8 @@ To open somewhere else, name the halves yourself:
387
420
  <Panels<PanelId> defaultOpen={{ left: { primary: 'search' } }}>
388
421
  ```
389
422
 
390
- `defaultOpen` is read once, and never against a layout that was restored — an arrangement the
391
- reader made outranks a default.
423
+ `defaultOpen` is read once per view, and never against a layout that was restored — an
424
+ arrangement the reader made outranks a default.
392
425
 
393
426
  ## `opens`
394
427
 
@@ -418,9 +451,10 @@ arrangement nobody remembers making, is not a restoration.
418
451
 
419
452
  ## Adding and removing panels
420
453
 
421
- The registry follows your JSX. A panel that stops being declared is unregistered, and the half it
422
- occupied is emptied a stored id naming a panel that no longer exists would otherwise leave a
423
- frame with nothing to draw in it.
454
+ The registry follows your JSX, in the order you declare it. A panel that stops being declared
455
+ leaves the rail, and the half it held **falls back** to whatever is still declared for it — it is
456
+ not closed, and the choice is not forgotten. Declare the panel again and the half is its once
457
+ more, in the place it had.
424
458
 
425
459
  Conditional panels work as you would expect:
426
460
 
@@ -431,7 +465,29 @@ Conditional panels work as you would expect:
431
465
  ```
432
466
 
433
467
  This is how you express what a capability system would otherwise be for: a panel that should not
434
- be offered is a panel you do not declare.
468
+ be offered is a panel you do not declare. And because a withdrawal costs nothing, a panel may go
469
+ and come back as often as a right, a route or a connection does.
470
+
471
+ ## Views
472
+
473
+ Two parts of one application may want their own arrangement — an editor and a review screen, a
474
+ project and a dashboard. Name the one in front, and each keeps the panels it had open:
475
+
476
+ ```tsx
477
+ <Panels<PanelId> view={reviewing ? 'review' : 'edit'}>
478
+ ```
479
+
480
+ Closing a column in one view leaves it open in the other, and coming back finds it as it was
481
+ left. The **lengths are shared**: a column that changed width on the way to another view would
482
+ read as another window.
483
+
484
+ Left out, everything lands in one view and nothing about this is visible. Views cost nothing to
485
+ ignore, and a project that grows into a second one only has a prop to pass.
486
+
487
+ The prop is **controlled**: it is reconciled on every render, so `setView` called behind its back
488
+ is taken over on the next one. And views are never evicted — name the handful of screens that own
489
+ an arrangement rather than every route, or `view={location.pathname}` grows one stored entry per
490
+ URL, for ever.
435
491
 
436
492
  ==============================================================================
437
493
  Layout
@@ -519,7 +575,9 @@ This is what lets it be adopted a piece at a time in an application that already
519
575
 
520
576
  ## Persistence
521
577
 
522
- What is stored: which half holds which panel, and the sizes. That is all.
578
+ What is stored: which half holds which panel, in each view, and the sizes. That is all — and
579
+ only a real choice is written: a half nobody has touched is stored as open, naming nobody, so it
580
+ follows your declarations rather than freezing an answer.
523
581
 
524
582
  ```tsx
525
583
  <Panels
@@ -641,16 +699,24 @@ window.electron?.onMenu(id => store.getState().show(id))
641
699
  ```
642
700
 
643
701
  `store.getState()` gives the same actions the hooks call: `show`, `close`, `toggle`, `focus`,
644
- `resize`, `resplit`, `resplitBand`, `fit`, `reset`.
702
+ `resize`, `resplit`, `resplitBand`, `fit`, `reset` — plus `declare` and `setView`, which the
703
+ chassis drives itself.
704
+
705
+ Inside React, the `view` prop is the only way to change views: it is controlled and reconciled on
706
+ every render, so a `setView` made behind its back is taken over on the next one. `usePanelsActions`
707
+ therefore does not offer it — one path per question.
645
708
 
646
709
  ## `usePanelsState`
647
710
 
648
711
  Any slice, subscribed:
649
712
 
650
713
  ```tsx
651
- const open = usePanelsState<PanelId, OpenByZone<PanelId>>(state => state.open)
714
+ const open = usePanelsState<PanelId, OpenByZone<PanelId>>(state => state.views[state.view])
652
715
  ```
653
716
 
717
+ The arrangement of the view on screen — `openOf(state)` says the same thing off a state you
718
+ already hold.
719
+
654
720
  Prefer scalar selectors. `lengths` and `available` are replaced wholesale on every write, so
655
721
  subscribing to either as an object wakes your component on every frame of a drag.
656
722
 
@@ -843,6 +909,22 @@ What the chassis does for you:
843
909
  What is yours: the contrast of your palette, and the accessible names of whatever you put inside
844
910
  the panels.
845
911
 
912
+ ## Drawing the buttons yourself
913
+
914
+ The chassis draws two buttons you never reach: the rail's, and a panel header's close. Replace
915
+ them rather than configure them — a tooltip, a shortcut hint, a badge belong to your design
916
+ system, not to a layout library.
917
+
918
+ ```tsx
919
+ <Panels components={{ IconButton: MyIconButton }}>
920
+ ```
921
+
922
+ `MyIconButton` receives `IconButtonProps`: `icon`, `label`, `active`, `accented`, `acts`,
923
+ `onClick`, plus anything else you spread. `acts` tells the close button from a rail toggle.
924
+
925
+ Read **once**, like `storage`: an object written inline changes identity on every render, and
926
+ every panel would re-render with it.
927
+
846
928
  ==============================================================================
847
929
  Document tabs
848
930
  ==============================================================================
@@ -1008,6 +1090,10 @@ Give them different storage keys, or they overwrite each other's layout:
1008
1090
 
1009
1091
  Each `<Panels>` makes its own store; nothing is shared between them.
1010
1092
 
1093
+ Two chassis is not the same question as two **views**. Two chassis are two frames on screen at
1094
+ once; two views are one frame that two parts of the application arrange differently — see
1095
+ `view` in [PANELS](PANELS.md#views), which keeps the lengths shared.
1096
+
1011
1097
  ## Testing
1012
1098
 
1013
1099
  The chassis is ordinary React. Give it a storage that forgets, so one test cannot leak into the
@@ -1097,17 +1183,25 @@ the same constraint `<Route>` has, and it is understood.
1097
1183
 
1098
1184
  ## What is stored, and what is resolved
1099
1185
 
1100
- Stored: which panel each half holds, and the sizes. Nothing else.
1186
+ Stored: which panel each half was **told** to hold, per view, and the sizes. Nothing else — and
1187
+ only a real choice: a half nobody has touched is stored open, naming nobody.
1101
1188
 
1102
1189
  Everything else is **resolved at render**:
1103
1190
 
1104
- - what a zone *draws* is not what it *holds* — a `solo` panel silences the other half
1191
+ - which panel a half *draws* is not what it *holds* — the one it names if that panel is still
1192
+ declared for that half, else the first one that is
1193
+ - what a zone *draws* is not what its halves hold either — a `solo` panel silences the other
1105
1194
  - the size a zone *takes* is bounded against the opposite zone and the measured room
1106
1195
  - whether a zone *takes room* is not whether it *draws* — the band's two halves share one height
1107
1196
 
1108
1197
  Each of those was a bug before it was a function. They are `shownIn`, `sharedSizes` and
1109
1198
  `zoneTakesRoom`, and each has a test that starts from the arrangement that broke it.
1110
1199
 
1200
+ Resolving the first one at render rather than writing it down is what makes conditional panels
1201
+ free. A panel withdrawn — behind a right, a route, a connection — leaves a half that falls back;
1202
+ declared again, it takes its half back. Written down, the choice would have been lost the first
1203
+ time the panel was hidden, and nothing would ever have restored it.
1204
+
1111
1205
  ## Why the selectors are scalar
1112
1206
 
1113
1207
  `resize`, `resplit` and `fit` replace `lengths` and `available` wholesale. A component subscribed
@@ -1136,11 +1230,16 @@ not something a repaint should reach.
1136
1230
 
1137
1231
  ## What was deliberately left out
1138
1232
 
1139
- - **surfaces / sections** — the chassis has no notion of "which screen am I on". In React the
1140
- router changes the children; there is no problem to solve.
1233
+ - **surfaces / sections** — the chassis still has no notion of what a screen *is*, or of your
1234
+ domain. What it does carry, since views, is that one application can have two screens worth
1235
+ arranging apart: `view` names an arrangement and nothing more. The router changes the children;
1236
+ the view says which arrangement they are drawn in.
1141
1237
  - **capabilities** — a panel that should not be offered is a panel you do not declare.
1142
1238
  - **i18n** — four strings, passed in already translated.
1143
1239
  - **an icon set** — `icon` takes any node.
1240
+ - **tooltips** — and everything else a design system puts on a button. The two buttons the
1241
+ chassis draws itself are replaceable (`components`), which is what keeps `tooltip`, `shortcut`
1242
+ and `badge` from becoming props here one after the other.
1144
1243
  - **floating and dragging panels between zones** — planned, and the state model already allows an
1145
1244
  override of the declared zone. Not in v1.
1146
1245
 
package/llms.txt CHANGED
@@ -20,14 +20,23 @@
20
20
  provider its own file sits under.
21
21
  - **One store per chassis**, created not imported (`createPanelsStore`). No module singleton —
22
22
  two chassis can live in one application, and a test cannot leak into the next.
23
- - **What is stored is not what is shown.** Stored: which panel each half holds, and the sizes.
24
- Resolved at render: what a zone draws (a `solo` panel silences the other half), the size it
25
- takes (bounded against the opposite zone), whether it takes room at all (the band's two halves
26
- share one height).
27
- - **There are no "surfaces" or sections.** An earlier design carried them; they were removed. In
28
- React the router changes the children there is nothing to solve.
23
+ - **What is stored is not what is shown, and only a CHOICE is stored.** Stored: which panel each
24
+ half was told to hold, per view, and the sizes. A half nobody touched is stored open, naming
25
+ nobody, and a view nobody has opened is simply absent which is what tells the chassis to
26
+ settle it. Resolved at render: which panel a half draws (the one named if it is still declared for
27
+ that half, else the first one that is), whether the zone draws at all, the size it takes
28
+ (bounded against the opposite zone), whether it takes room (the band's two halves share one
29
+ height).
30
+ - **No tooltips, no icon set, no i18n.** The two buttons the chassis draws — the rail's and the
31
+ close — are REPLACED, not configured: `<Panels components={{ IconButton: Mine }}>`. That is
32
+ what keeps `tooltip`, `shortcut` and `badge` from landing here one after the other.
29
33
  - **A panel that should not be offered is a panel you do not declare.** That replaces what a
30
- capability system would be for.
34
+ capability system would be for — and withdrawing one costs nothing: the half falls back, the
35
+ choice is remembered, and declaring the panel again gives it its place back.
36
+ - **Views are named arrangements, and they are opt-in.** `<Panels view="review">`: each view
37
+ keeps the panels it had open to itself, the lengths stay shared. Left out, everything lands in
38
+ one view and nothing about this is visible. Not "surfaces": the library still knows nothing
39
+ about your domain, only that one application can have two screens worth arranging apart.
31
40
 
32
41
  ## Install and shape
33
42
 
@@ -75,8 +84,8 @@ usual missing piece.
75
84
  `PanelHeader` `IconButton` `Separator` `ResizeHandle`
76
85
 
77
86
  ### `<Panels>` props
78
- `store` `storageKey` `storage` `defaultOpen` `header` `footer` `railHeader` `labels` `theme`
79
- `className`
87
+ `store` `storageKey` `storage` `defaultOpen` `view` `header` `footer` `railHeader` `labels`
88
+ `components` `theme` `className`
80
89
 
81
90
  ### `<Panel>` props
82
91
  `id` `zone` `slot?` `title` `icon?` `actions?` `opens?` `solo?`
@@ -84,14 +93,17 @@ usual missing piece.
84
93
  ### Hooks
85
94
  - `usePanels<Id>()` → `{ panels, reveal, close, toggle, isShown, focusedZone, reset }`
86
95
  - `useZone<Id>(zone)` → `{ primary, secondary, draws, size, split, focused }`
87
- - `useArrangement` `useShownIn` `useZoneDraws` `useZoneTakesRoom` `useBandHalves`
96
+ - `useArrangement` `useShownIn` `useShownSpecsIn` `useZoneDraws` `useZoneTakesRoom`
97
+ `useBandHalves`
88
98
  - `useContainerFit(ref)` `usePointerDrag()` `usePanelsState(selector)` `usePanelsActions()`
89
99
 
90
100
  ### Core
91
- `createPanelsStore` `shownIn` `specOf` `zoneDraws` `zoneTakesRoom` `undraggedSizeOf`
101
+ `createPanelsStore` `shownIn` `shownSpecsIn` `openOf` `specOf` `zoneDraws` `zoneTakesRoom`
102
+ `undraggedSizeOf`
92
103
  `fitZoneSize` `fitSplit` `sharedSizes` `sizeKeyOf` `MIN_SIZE` `MIN_CENTER` `MIN_SPLIT`
93
104
  `DEFAULT_SIZES` · `browserStorage` `memoryStorage` `readLayout` `writeLayout` `LAYOUT_VERSION`
94
- · `ZONES` `SLOTS` `BOTTOM_ZONES` `ZONES_BY_SIDE` `isBottom` `isHorizontal` `isLeading` `cx`
105
+ · `ZONES` `SLOTS` `BOTTOM_ZONES` `ZONES_BY_SIDE` `DEFAULT_VIEW` `isBottom` `isHorizontal`
106
+ `isLeading` `cx`
95
107
 
96
108
  ### Optional entry point
97
109
  `@pasquelin/panels/dockview` → `DockviewCenter`, for document tabs. Separate so its weight only
@@ -130,6 +142,14 @@ Gauges: `--pnl-rail` `--pnl-rail-button` `--pnl-rail-inset` `--pnl-gutter` `--pn
130
142
  write, so subscribing to either as an object re-renders on every frame of a drag.
131
143
  - `close(id)` closes THAT panel or nothing. `close(zone, slot)` on the store empties the half
132
144
  whatever stands in it — two panels share a half.
145
+ - `show(id)` on a panel already on screen only FOCUSES it. A half drawing it by declaration
146
+ order has named nobody, and writing the name down would settle for every other view a question
147
+ the click never asked.
148
+ - The `view` prop is CONTROLLED: `setView` on the store is overruled by the next render. It is
149
+ reconciled on every render, not on a dependency change, so the contract does not depend on
150
+ which other props you happened to memoise.
151
+ - Views are never evicted. `view={location.pathname}` grows one entry per URL, for ever. Name
152
+ the handful of screens that own an arrangement, not every route.
133
153
  - `useZoneTakesRoom` ≠ `useZoneDraws`: the band's halves share a height, so either drawing means
134
154
  the strip takes it.
135
155
  - Two chassis in one app need two `storageKey`s, or they overwrite each other.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasquelin/panels",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Châssis à panneaux React : rails d'icônes, zones redimensionnables, centre libre. Headless + composants par défaut, entièrement surchargeable.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.16.0",
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "dist",
26
26
  "LICENSE",
27
+ "CHANGELOG.md",
27
28
  "llms.txt",
28
29
  "llms-full.txt"
29
30
  ],
@@ -45,22 +46,24 @@
45
46
  "./package.json": "./package.json"
46
47
  },
47
48
  "scripts": {
48
- "types": "tsc -p tsconfig.build.json",
49
49
  "build": "tsc --noEmit && vite build && pnpm types",
50
+ "types": "tsc -p tsconfig.build.json",
50
51
  "prepack": "pnpm build",
51
52
  "typecheck": "tsc --noEmit",
53
+ "lint": "eslint src examples",
54
+ "format": "prettier --write \"{src,examples,site}/**/*.{ts,tsx,css}\"",
55
+ "format:check": "prettier --check \"{src,examples,site}/**/*.{ts,tsx,css}\"",
52
56
  "test": "vitest run",
53
57
  "test:watch": "vitest",
54
- "lint": "eslint src examples",
55
- "format": "prettier --write \"{src,examples}/**/*.{ts,tsx,css}\"",
56
- "format:check": "prettier --check \"{src,examples}/**/*.{ts,tsx,css}\"",
57
- "dev": "vite --config examples/vite.config.ts",
58
- "build:site": "vite build --config examples/vite.config.ts",
59
- "preview:site": "vite preview --config examples/vite.config.ts",
60
- "llms": "node scripts/make-llms-full.mjs",
61
- "llms:check": "node scripts/make-llms-full.mjs --check",
58
+ "llms": "node scripts/make-llms.mjs",
59
+ "site:check": "node scripts/check-site.mjs",
60
+ "llms:check": "node scripts/make-llms.mjs --check",
62
61
  "licences:check": "node scripts/check-licences.mjs",
63
- "validate": "pnpm llms:check && pnpm licences:check && pnpm typecheck && pnpm lint && pnpm format:check && pnpm test"
62
+ "validate": "pnpm site:check && pnpm llms:check && pnpm licences:check && pnpm typecheck && pnpm lint && pnpm format:check && pnpm test",
63
+ "dev:example": "vite --config examples/vite.config.ts",
64
+ "build:example": "vite build --config examples/vite.config.ts",
65
+ "build:site": "pnpm build:example && node site/build.mjs _site",
66
+ "preview:site": "node scripts/serve.mjs _site --base=/panels/"
64
67
  },
65
68
  "peerDependencies": {
66
69
  "dockview-react": ">=8.0.0",