@pasquelin/panels 0.1.0 → 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 +153 -0
- package/README.md +15 -2
- package/dist/components/Panels.d.ts +7 -1
- package/dist/core/clamps.d.ts +9 -7
- package/dist/core/components.d.ts +18 -0
- package/dist/core/context.d.ts +13 -3
- package/dist/core/hooks/useArrangement.d.ts +12 -10
- package/dist/core/persistence.d.ts +16 -11
- package/dist/core/store.d.ts +95 -24
- package/dist/core/types.d.ts +19 -4
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +612 -569
- package/dist/styles.cjs +1 -0
- package/dist/styles.css +1 -1
- package/dist/styles.d.ts +11 -0
- package/dist/styles.js +1 -0
- package/llms-full.txt +127 -28
- package/llms.txt +33 -13
- package/package.json +17 -14
package/dist/styles.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/styles.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.pnl-dockview{height:100%;--dv-background-color: var(--pnl-panel);--dv-group-view-background-color: var(--pnl-panel);--dv-tabs-and-actions-container-background-color: var(--pnl-panel);--dv-tabs-container-scrollbar-color: var(--pnl-
|
|
1
|
+
.pnl-dockview{height:100%;--dv-background-color: var(--pnl-panel);--dv-group-view-background-color: var(--pnl-panel);--dv-tabs-and-actions-container-background-color: var(--pnl-panel);--dv-tabs-container-scrollbar-color: var(--pnl-border);--dv-activegroup-visiblepanel-tab-background-color: var(--pnl-elevated);--dv-activegroup-hiddenpanel-tab-background-color: transparent;--dv-inactivegroup-visiblepanel-tab-background-color: var(--pnl-surface);--dv-inactivegroup-hiddenpanel-tab-background-color: transparent;--dv-activegroup-visiblepanel-tab-color: var(--pnl-text);--dv-activegroup-hiddenpanel-tab-color: var(--pnl-muted);--dv-inactivegroup-visiblepanel-tab-color: var(--pnl-muted);--dv-inactivegroup-hiddenpanel-tab-color: var(--pnl-muted);--dv-separator-border: transparent;--dv-paneview-header-border-color: var(--pnl-border);--dv-tab-divider-color: transparent;--dv-drag-over-background-color: color-mix(in srgb, var(--pnl-accent) 18%, transparent);--dv-drag-over-border-color: var(--pnl-accent);--dv-tabs-and-actions-container-height: var(--pnl-tab);--dv-tabs-and-actions-container-font-size: 12px}.pnl-dockview .dv-tabs-container{padding-left:var(--pnl-gutter)}.pnl-dockview .dv-tab{display:flex;align-items:center;border-radius:var(--pnl-radius);margin:6px 2px;padding:0 4px;transition:background-color .12s ease}.pnl-dockview .dv-tab>.dv-react-part{display:flex;align-items:center;min-width:0}.pnl-dockview .dv-tab .dv-default-tab{min-width:0}.pnl-dockview .dv-tab:hover{background-color:var(--pnl-surface)}.pnl-dockview .dv-default-tab{height:100%;padding:0 4px}.pnl-dockview .dv-default-tab-action{display:flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border-radius:9999px;background-color:transparent;transition:background-color .12s ease,color .12s ease}.pnl-dockview .dv-default-tab-action:hover{background-color:var(--pnl-elevated);color:var(--pnl-text)}:where(.pnl-root){--pnl-rail: 48px;--pnl-rail-button: 36px;--pnl-rail-inset: 14px;--pnl-gutter: 6px;--pnl-header: 40px;--pnl-tab: 40px;--pnl-radius: 6px;--pnl-radius-sm: 4px;--pnl-font-size: 13px;--pnl-chassis: #2b2d30;--pnl-panel: #191a1c;--pnl-surface: #202124;--pnl-elevated: #3c3f44;--pnl-border: #34363a;--pnl-text: #dfe1e5;--pnl-muted: #91959b;--pnl-accent: #346ef2;--pnl-accent-text: #ffffff}.pnl-root{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--pnl-chassis);color:var(--pnl-text);font-size:var(--pnl-font-size)}.pnl-root *,.pnl-root *:before,.pnl-root *:after{box-sizing:border-box}@media(prefers-color-scheme:light){:where(.pnl-root:not([data-pnl-theme=dark])){--pnl-chassis: #dcdde1;--pnl-panel: #ffffff;--pnl-surface: #f2f3f6;--pnl-elevated: #e4e6ea;--pnl-border: #c9cbd1;--pnl-text: #1f2124;--pnl-muted: #5c6066}}:where(.pnl-root[data-pnl-theme=light]){--pnl-chassis: #dcdde1;--pnl-panel: #ffffff;--pnl-surface: #f2f3f6;--pnl-elevated: #e4e6ea;--pnl-border: #c9cbd1;--pnl-text: #1f2124;--pnl-muted: #5c6066}.pnl-middle{display:flex;flex:1;min-height:0}.pnl-columns{display:flex;flex:1;flex-direction:column;min-width:0;min-height:0;padding-block:var(--pnl-gutter)}.pnl-row{display:flex;flex:1;min-height:0}.pnl-stack{display:flex;flex:1;flex-direction:column;min-width:0;min-height:0}.pnl-centre{flex:1;min-width:0}.pnl-rail{display:flex;flex-direction:column;flex-shrink:0;align-items:center;justify-content:space-between;width:var(--pnl-rail);padding-block:var(--pnl-gutter)}.pnl-rail__group{display:flex;flex-direction:column;align-items:center;gap:8px}.pnl-rail__button{width:var(--pnl-rail-button);height:var(--pnl-rail-button);border-radius:var(--pnl-radius)}.pnl-zone{display:flex;min-width:0;min-height:0}.pnl-zone--col{flex-direction:column}.pnl-zone--row{flex-direction:row}.pnl-band{display:flex;min-height:0}.pnl-band__half{display:flex;flex-direction:column;min-width:0}.pnl-band__half--even,.pnl-band__half--rest{flex:1}.pnl-surface{display:flex;flex-direction:column;flex-shrink:0;min-height:0;overflow:hidden;border-radius:var(--pnl-radius);background:var(--pnl-panel)}.pnl-surface--fill{flex:1 1 0}.pnl-surface--give{flex-shrink:1}.pnl-body{display:flex;flex:1;flex-direction:column;min-height:0;overflow:auto}.pnl-header{display:flex;flex-shrink:0;align-items:center;gap:8px;height:var(--pnl-header);padding-inline:12px 6px}.pnl-header__title{overflow:hidden;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.pnl-header__title--fixed{flex-shrink:0}.pnl-header__actions{display:flex;align-items:center;gap:2px;min-width:0;overflow:hidden;margin-left:auto}.pnl-header__actions--fill{flex:1;margin-left:0}.pnl-header__trailing{display:flex;flex-shrink:0;align-items:center;gap:2px}.pnl-icon-button{display:inline-flex;flex-shrink:0;align-items:center;justify-content:center;gap:6px;width:28px;height:28px;padding:0;border:none;border-radius:var(--pnl-radius-sm);background:transparent;color:var(--pnl-muted);cursor:pointer;transition:background-color .12s ease,color .12s ease}.pnl-icon-button:hover:not(:disabled){background:var(--pnl-elevated);color:var(--pnl-text)}.pnl-icon-button:disabled{opacity:.4;cursor:not-allowed}.pnl-icon-button--active{background:var(--pnl-elevated);color:var(--pnl-text)}.pnl-icon-button--accented,.pnl-icon-button--accented:hover:not(:disabled){background:var(--pnl-accent);color:var(--pnl-accent-text)}.pnl-icon-button--header{width:24px;height:24px}.pnl-icon-button__glyph{display:inline-flex;align-items:center;justify-content:center}.pnl-separator{display:block;flex-shrink:0;background:var(--pnl-border)}.pnl-separator--vertical{width:1px;height:16px;margin-inline:4px}.pnl-separator--horizontal{width:80%;height:1px;margin-block:4px}.pnl-handle{flex-shrink:0;background:transparent;border:none}.pnl-handle--col{width:var(--pnl-gutter);height:100%;cursor:col-resize}.pnl-handle--row{width:100%;height:var(--pnl-gutter);cursor:row-resize}.pnl-handle:focus-visible{background:var(--pnl-accent);outline:none}
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stylesheet, as an entry point of its own.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Imported from `index.ts` instead, `tsc` carried `import './styles/panels.css'` into the
|
|
5
|
+
* published declaration — where that path does not exist, so a consumer typechecking without
|
|
6
|
+
* `skipLibCheck` got TS2882 on a package that had just installed cleanly.
|
|
7
|
+
*
|
|
8
|
+
* Here, the emitted declaration is empty and nothing leaks. Consumers import
|
|
9
|
+
* `@pasquelin/panels/styles.css`, which is what the documentation already told them to do.
|
|
10
|
+
*/
|
|
11
|
+
import './styles/panels.css';
|
package/dist/styles.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/llms-full.txt
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> types). Icon rails on the edges, five resizable zones around a free centre, a layout that
|
|
5
5
|
> survives a reload. **Headless underneath, repaintable on top** — the components are built on
|
|
6
6
|
> hooks that render nothing, and every visual value is a CSS custom property.
|
|
7
|
-
> MIT. Live: https://pasquelin.github.io/
|
|
7
|
+
> MIT. Live: https://pasquelin.github.io/panels/
|
|
8
8
|
|
|
9
9
|
## Mental model (read this first)
|
|
10
10
|
|
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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`
|
|
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` `
|
|
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`
|
|
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` `
|
|
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.
|
|
@@ -155,7 +175,7 @@ README
|
|
|
155
175
|
|
|
156
176
|
# @pasquelin/panels
|
|
157
177
|
|
|
158
|
-
**[Live demo ↗](https://pasquelin.github.io/
|
|
178
|
+
**[Live demo ↗](https://pasquelin.github.io/panels/)** · **[Documentation 🇫🇷](docs/fr/README.md)** · **[Documentation 🇬🇧](docs/en/README.md)** · MIT
|
|
159
179
|
|
|
160
180
|
A React panel chassis: icon rails on the edges, resizable zones around a free centre, and a
|
|
161
181
|
layout that survives a reload.
|
|
@@ -280,7 +300,7 @@ project's panels come from — a router, a state machine, a config — is that p
|
|
|
280
300
|
|
|
281
301
|
## Examples
|
|
282
302
|
|
|
283
|
-
All four run at **[pasquelin.github.io/
|
|
303
|
+
All four run at **[pasquelin.github.io/panels](https://pasquelin.github.io/panels/)**, or locally:
|
|
284
304
|
|
|
285
305
|
```bash
|
|
286
306
|
pnpm dev
|
|
@@ -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
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
|
1140
|
-
|
|
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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> types). Icon rails on the edges, five resizable zones around a free centre, a layout that
|
|
5
5
|
> survives a reload. **Headless underneath, repaintable on top** — the components are built on
|
|
6
6
|
> hooks that render nothing, and every visual value is a CSS custom property.
|
|
7
|
-
> MIT. Live: https://pasquelin.github.io/
|
|
7
|
+
> MIT. Live: https://pasquelin.github.io/panels/
|
|
8
8
|
|
|
9
9
|
## Mental model (read this first)
|
|
10
10
|
|
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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`
|
|
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` `
|
|
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`
|
|
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` `
|
|
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.
|
|
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",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"author": "Alban Pasquelin <alban.pasquelin@gmail.com>",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/pasquelin/
|
|
11
|
+
"url": "git+https://github.com/pasquelin/panels.git"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.com/pasquelin/
|
|
14
|
-
"bugs": "https://github.com/pasquelin/
|
|
13
|
+
"homepage": "https://github.com/pasquelin/panels#readme",
|
|
14
|
+
"bugs": "https://github.com/pasquelin/panels/issues",
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
@@ -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
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
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",
|