@pasquelin/panels 0.1.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/components/Band.d.ts +13 -0
- package/dist/components/Center.d.ts +18 -0
- package/dist/components/IconButton.d.ts +20 -0
- package/dist/components/Panel.d.ts +31 -0
- package/dist/components/PanelFrame.d.ts +23 -0
- package/dist/components/PanelHeader.d.ts +15 -0
- package/dist/components/Panels.d.ts +30 -0
- package/dist/components/Rail.d.ts +23 -0
- package/dist/components/ResizeHandle.d.ts +37 -0
- package/dist/components/Separator.d.ts +6 -0
- package/dist/components/Surface.d.ts +9 -0
- package/dist/components/ZoneEdge.d.ts +17 -0
- package/dist/components/content.d.ts +9 -0
- package/dist/components/labels.d.ts +12 -0
- package/dist/core/clamps.d.ts +55 -0
- package/dist/core/context.d.ts +39 -0
- package/dist/core/cx.d.ts +5 -0
- package/dist/core/hooks/useArrangement.d.ts +36 -0
- package/dist/core/hooks/useContainerFit.d.ts +9 -0
- package/dist/core/hooks/usePanels.d.ts +26 -0
- package/dist/core/hooks/usePointerDrag.d.ts +21 -0
- package/dist/core/hooks/useZone.d.ts +28 -0
- package/dist/core/layoutEffect.d.ts +10 -0
- package/dist/core/persistence.d.ts +30 -0
- package/dist/core/store.d.ts +86 -0
- package/dist/core/types.d.ts +88 -0
- package/dist/cx-CcykAxZN.js +6 -0
- package/dist/cx-YyuC5RtB.cjs +1 -0
- package/dist/dockview/DockviewCenter.d.ts +37 -0
- package/dist/dockview/index.d.ts +3 -0
- package/dist/dockview.cjs +1 -0
- package/dist/dockview.js +39 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +935 -0
- package/dist/styles.css +1 -0
- package/llms-full.txt +1242 -0
- package/llms.txt +150 -0
- package/package.json +99 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 alban pasquelin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @pasquelin/panels
|
|
2
|
+
|
|
3
|
+
**[Live demo โ](https://pasquelin.github.io/panel/)** ยท **[Documentation ๐ซ๐ท](docs/fr/README.md)** ยท **[Documentation ๐ฌ๐ง](docs/en/README.md)** ยท MIT
|
|
4
|
+
|
|
5
|
+
A React panel chassis: icon rails on the edges, resizable zones around a free centre, and a
|
|
6
|
+
layout that survives a reload.
|
|
7
|
+
|
|
8
|
+
It is a **layout, not a framework**. What goes in the panels is your project's business โ the
|
|
9
|
+
library draws the frame, remembers the sizes, and gets out of the way.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @pasquelin/panels
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Panels, Panel } from '@pasquelin/panels'
|
|
17
|
+
import '@pasquelin/panels/styles.css'
|
|
18
|
+
|
|
19
|
+
type PanelId = 'files' | 'chat'
|
|
20
|
+
|
|
21
|
+
export function App() {
|
|
22
|
+
return (
|
|
23
|
+
<Panels<PanelId>>
|
|
24
|
+
<Panel<PanelId> id="files" zone="left" title="Files" icon={<FilesIcon />}>
|
|
25
|
+
<FileTree />
|
|
26
|
+
</Panel>
|
|
27
|
+
|
|
28
|
+
<Panel<PanelId> id="chat" zone="right" title="Assistant" icon={<ChatIcon />}>
|
|
29
|
+
<Conversation />
|
|
30
|
+
</Panel>
|
|
31
|
+
|
|
32
|
+
<Panels.Center>
|
|
33
|
+
<Outlet />
|
|
34
|
+
</Panels.Center>
|
|
35
|
+
</Panels>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That is the whole of it. The rails, the resize handles, the persistence and the keyboard are
|
|
41
|
+
already there.
|
|
42
|
+
|
|
43
|
+
## What you get
|
|
44
|
+
|
|
45
|
+
- **Five zones** โ `left`, `right`, `top`, `bottomLeft`, `bottomRight`. A zone nobody fills takes
|
|
46
|
+
no room at all.
|
|
47
|
+
- **Two halves per zone** โ `primary` and `secondary`, parted by a handle. Panels sharing a half
|
|
48
|
+
take turns; the rail switches between them.
|
|
49
|
+
- **Icon rails** that stay put when a zone closes, so a closed panel is always one click away.
|
|
50
|
+
- **Resize** by pointer or keyboard, clamped so the centre never disappears.
|
|
51
|
+
- **Persistence** to `localStorage` out of the box, or wherever you say.
|
|
52
|
+
- **Typed ids** โ `reveal('chatt')` does not compile.
|
|
53
|
+
- **No dependencies** beyond React. No icon set, no CSS framework, no i18n.
|
|
54
|
+
|
|
55
|
+
## Panel props
|
|
56
|
+
|
|
57
|
+
| Prop | Meaning |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `id` | Unique, and the type parameter of `<Panels>` |
|
|
60
|
+
| `zone` | Which edge it hangs from |
|
|
61
|
+
| `slot` | `primary` (nearest the edge) or `secondary`. Defaults to `primary` |
|
|
62
|
+
| `title` | Accessible name and header title, **already translated** |
|
|
63
|
+
| `icon` | Anything React renders โ the library imposes no icon set |
|
|
64
|
+
| `actions` | Rendered on the panel's own title row |
|
|
65
|
+
| `opens` | The width this panel wants when it leads its zone |
|
|
66
|
+
| `solo` | Takes the zone whole; the other half is put away, not closed, and comes back |
|
|
67
|
+
|
|
68
|
+
## Driving the panels from anywhere
|
|
69
|
+
|
|
70
|
+
`usePanels()` is all a header, a shortcut or a menu needs:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
const { panels, reveal, close, toggle, isShown } = usePanels<PanelId>()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
From **outside React** โ a socket message, an Electron menu โ build the store yourself and keep
|
|
77
|
+
the reference:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
const store = createPanelsStore<PanelId>()
|
|
81
|
+
|
|
82
|
+
socket.on('alert', () => store.getState().show('alerts'))
|
|
83
|
+
|
|
84
|
+
<Panels<PanelId> store={store}>โฆ</Panels>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Making it yours
|
|
88
|
+
|
|
89
|
+
Three levels, and you can stop at any of them.
|
|
90
|
+
|
|
91
|
+
**1. Repaint it.** Every value is a custom property on `.pnl-root`:
|
|
92
|
+
|
|
93
|
+
```css
|
|
94
|
+
.pnl-root {
|
|
95
|
+
--pnl-panel: #101418;
|
|
96
|
+
--pnl-chassis: #1b1f24;
|
|
97
|
+
--pnl-accent: #47965c; /* the rails follow your brand */
|
|
98
|
+
--pnl-radius: 10px;
|
|
99
|
+
--pnl-rail: 56px;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**2. Replace a piece.** `Rail`, `PanelHeader`, `ResizeHandle`, `Surface`, `IconButton` and the
|
|
104
|
+
rest are exported. Build your own frame out of the ones you keep.
|
|
105
|
+
|
|
106
|
+
**3. Take the logic only.** Every component is built on hooks that render nothing:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const zone = useZone('left') // what it draws, its size, its split
|
|
110
|
+
const { reveal, isShown } = usePanels()
|
|
111
|
+
useContainerFit(ref) // re-clamps when the container resizes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Draw whatever you like on top. The chassis has no opinion about it.
|
|
115
|
+
|
|
116
|
+
## It fits inside your page
|
|
117
|
+
|
|
118
|
+
The chassis measures **its own container**, never the window. Put it in a route, under your
|
|
119
|
+
navigation, beside your sidebar โ the clamps follow the box it is actually in.
|
|
120
|
+
|
|
121
|
+
## Header and footer are yours
|
|
122
|
+
|
|
123
|
+
`<Panels>` draws no chrome. `header` and `footer` are slots; pass your own, or nothing. Where a
|
|
124
|
+
project's panels come from โ a router, a state machine, a config โ is that project's business.
|
|
125
|
+
|
|
126
|
+
## Examples
|
|
127
|
+
|
|
128
|
+
All four run at **[pasquelin.github.io/panel](https://pasquelin.github.io/panel/)**, or locally:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pnpm dev
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `examples/minimal` โ the smallest working chassis
|
|
135
|
+
- `examples/router` โ the centre as a React Router outlet
|
|
136
|
+
- `examples/dockview` โ document tabs, on the optional Dockview entry point
|
|
137
|
+
- `examples/theme` โ the same chassis under four palettes
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
| | ๐ซ๐ท Franรงais | ๐ฌ๐ง English |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| **Guide + index** | [docs/fr/](docs/fr/README.md) | [docs/en/](docs/en/README.md) |
|
|
144
|
+
| Panels | [PANELS.md](docs/fr/PANELS.md) | [PANELS.md](docs/en/PANELS.md) |
|
|
145
|
+
| Layout | [LAYOUT.md](docs/fr/LAYOUT.md) | [LAYOUT.md](docs/en/LAYOUT.md) |
|
|
146
|
+
| Hooks | [HOOKS.md](docs/fr/HOOKS.md) | [HOOKS.md](docs/en/HOOKS.md) |
|
|
147
|
+
| Theming | [THEMING.md](docs/fr/THEMING.md) | [THEMING.md](docs/en/THEMING.md) |
|
|
148
|
+
| Components | [COMPONENTS.md](docs/fr/COMPONENTS.md) | [COMPONENTS.md](docs/en/COMPONENTS.md) |
|
|
149
|
+
| Document tabs | [DOCKVIEW.md](docs/fr/DOCKVIEW.md) | [DOCKVIEW.md](docs/en/DOCKVIEW.md) |
|
|
150
|
+
| Recipes | [RECIPES.md](docs/fr/RECIPES.md) | [RECIPES.md](docs/en/RECIPES.md) |
|
|
151
|
+
| Architecture | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | โ |
|
|
152
|
+
|
|
153
|
+
For coding agents: [`llms.txt`](llms.txt) is the mental model, the API and the traps in one
|
|
154
|
+
page; [`llms-full.txt`](llms-full.txt) is every English chapter concatenated. Both ship with the
|
|
155
|
+
package, and `pnpm llms` regenerates the second from the first plus `docs/`.
|
|
156
|
+
|
|
157
|
+
## Licence
|
|
158
|
+
|
|
159
|
+
**MIT** โ see [LICENSE](LICENSE).
|
|
160
|
+
|
|
161
|
+
Every dependency is MIT too, so there is no copyleft obligation anywhere in the tree.
|
|
162
|
+
`zustand` is bundled into the published package and its notice travels with it; everything else
|
|
163
|
+
is a peer dependency the consuming project installs itself. The full accounting is in
|
|
164
|
+
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), and `pnpm licences:check` fails the build if it
|
|
165
|
+
stops being true.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PanelsLabels } from './labels';
|
|
2
|
+
export type BandProps = {
|
|
3
|
+
left: boolean;
|
|
4
|
+
right: boolean;
|
|
5
|
+
labels: PanelsLabels;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The bottom band: one strip, one height, and up to two zones side by side.
|
|
9
|
+
*
|
|
10
|
+
* Alone, a half takes the whole strip โ and the frame has already run it under the opposite
|
|
11
|
+
* column. Together they share the width, parted by a handle that starts at the middle.
|
|
12
|
+
*/
|
|
13
|
+
export declare function Band<Id extends string = string>({ left, right, labels }: BandProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type CenterProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Declares what the middle holds โ a router outlet, a canvas, a map, document tabs.
|
|
8
|
+
*
|
|
9
|
+
* A descriptor like `<Panel>`, and read by `<Panels>` for the same reason: the centre sits
|
|
10
|
+
* between the zones in the DOM, not where it is written.
|
|
11
|
+
*
|
|
12
|
+
* It stays at ONE place in the tree through every arrangement of the zones around it. Moved,
|
|
13
|
+
* React would unmount it โ tearing down whatever engine, canvas or editor it holds.
|
|
14
|
+
*/
|
|
15
|
+
export declare function Center(_props: CenterProps): null;
|
|
16
|
+
export declare namespace Center {
|
|
17
|
+
var displayName: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode, Ref } from 'react';
|
|
2
|
+
export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label' | 'aria-pressed' | 'children'> & {
|
|
3
|
+
/** Free-form. The library imposes no icon set. */
|
|
4
|
+
icon?: ReactNode;
|
|
5
|
+
/** Accessible name. Already translated. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Panel currently shown: neutral background, and `aria-pressed`. */
|
|
8
|
+
active?: boolean;
|
|
9
|
+
/** Shown AND in the focused zone: accented background. */
|
|
10
|
+
accented?: boolean;
|
|
11
|
+
/** Acts rather than toggles: no `aria-pressed`. */
|
|
12
|
+
acts?: boolean;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
ref?: Ref<HTMLButtonElement>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The rail's button, and the panel header's. One place for the active and accented states, the
|
|
18
|
+
* accessible name and the geometry โ copied at each site, a missing `aria-label` goes unnoticed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function IconButton({ icon, label, active, accented, acts, className, children, ref, ...rest }: IconButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PanelSpec, Slot } from '../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* Everything the chassis knows about a panel, plus what only JSX can carry.
|
|
5
|
+
*
|
|
6
|
+
* Derived from `PanelSpec` rather than restated: the two had been written out field by field,
|
|
7
|
+
* with the documentation copied word for word, so adding one meant remembering to add it twice
|
|
8
|
+
* โ and nothing would have failed if you had not.
|
|
9
|
+
*
|
|
10
|
+
* `slot` is the one field that changes shape: it is required in the registry and optional here,
|
|
11
|
+
* because a panel that does not say lands in the half nearest the edge.
|
|
12
|
+
*/
|
|
13
|
+
export type PanelProps<Id extends string = string> = Omit<PanelSpec<Id>, 'slot'> & {
|
|
14
|
+
slot?: Slot;
|
|
15
|
+
/** Actions on the panel's own title row, beside its name. */
|
|
16
|
+
actions?: ReactNode;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Declares one panel. It is a DESCRIPTOR, never rendered where it is written: `<Panels>` reads
|
|
21
|
+
* its props to build the rail and hands its children to whichever zone it named.
|
|
22
|
+
*
|
|
23
|
+
* The same shape as `<Route>` in React Router โ the element describes, the parent arranges. The
|
|
24
|
+
* alternative was a portal per panel, which puts the panel's content in a different React tree
|
|
25
|
+
* from the one it was declared in: context, error boundaries and suspense would all stop at the
|
|
26
|
+
* boundary, and a panel could not read a provider its own file sits under.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Panel<Id extends string = string>(_props: PanelProps<Id>): null;
|
|
29
|
+
export declare namespace Panel {
|
|
30
|
+
var displayName: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type PanelSpec } from '../core/types';
|
|
2
|
+
export type PanelFrameProps<Id extends string> = {
|
|
3
|
+
panel: PanelSpec<Id>;
|
|
4
|
+
/** Length of its own along the zone's inner axis. Absent takes whatever the other half left. */
|
|
5
|
+
length?: number;
|
|
6
|
+
/** Text of the close button, already translated. */
|
|
7
|
+
closeLabel: string;
|
|
8
|
+
onFocus: () => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* One panel on screen: its surface, its title row, its content.
|
|
12
|
+
*
|
|
13
|
+
* Closing is the only way out, on purpose. A collapsed panel is a third state between open and
|
|
14
|
+
* closed that looks like neither, and the rail already reopens a panel in one click.
|
|
15
|
+
*/
|
|
16
|
+
declare function PanelFrameInner<Id extends string>({ panel, length, closeLabel, onFocus, }: PanelFrameProps<Id>): import("react").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Memoised: a zone drag writes a new size on every `pointermove`, and without this each frame
|
|
19
|
+
* re-renders both halves and everything the project put in them. `onFocus` must stay stable for
|
|
20
|
+
* that to bite โ see `ZoneEdge`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PanelFrame: typeof PanelFrameInner;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type PanelHeaderProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
/** The panel's own actions, on the same line as its name. */
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Lets the actions take the free width rather than hug the trailing edge โ for a panel whose
|
|
8
|
+
* row is wide and mostly empty, and which carries a whole bar there rather than a button.
|
|
9
|
+
*/
|
|
10
|
+
fillActions?: boolean;
|
|
11
|
+
/** Pinned past the actions: whatever crowds the row, the way out of the panel stays reachable. */
|
|
12
|
+
trailing?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function PanelHeader({ title, children, fillActions, trailing, className, }: PanelHeaderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type PanelsProviderProps } from '../core/context';
|
|
3
|
+
import { type PanelsLabels } from './labels';
|
|
4
|
+
export type PanelsProps<Id extends string = string> = Omit<PanelsProviderProps<Id>, 'children'> & {
|
|
5
|
+
/** Above the rails, full width. The project's own โ the library imposes no chrome. */
|
|
6
|
+
header?: ReactNode;
|
|
7
|
+
/** Below the rails, full width: a status line, counters, a breadcrumb. */
|
|
8
|
+
footer?: ReactNode;
|
|
9
|
+
/** Pinned at the top of the left rail, above the icons. */
|
|
10
|
+
railHeader?: ReactNode;
|
|
11
|
+
/** Words the chassis says. English by default; pass your own already translated. */
|
|
12
|
+
labels?: Partial<PanelsLabels>;
|
|
13
|
+
/**
|
|
14
|
+
* Forces the palette. Left out, the chassis follows the reader's system setting โ which is
|
|
15
|
+
* what a project wants until it offers a switch of its own.
|
|
16
|
+
*/
|
|
17
|
+
theme?: 'dark' | 'light';
|
|
18
|
+
className?: string;
|
|
19
|
+
/** `<Panel>` and `<Center>` descriptors. Anything else is rendered where it stands. */
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The chassis: icon rails stuck to the edges, rounded panels laid over the gutter, a free centre,
|
|
24
|
+
* and the project's own header and footer around them.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Panels<Id extends string = string>({ header, footer, railHeader, labels, theme, className, children, ...provider }: PanelsProps<Id>): import("react").JSX.Element;
|
|
27
|
+
export declare namespace Panels {
|
|
28
|
+
var Panel: typeof import("./Panel").Panel;
|
|
29
|
+
var Center: typeof import("./Center").Center;
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Side, type Zone } from '../core/types';
|
|
2
|
+
export type RailProps = {
|
|
3
|
+
/** Edge the rail sticks to. Each rail also carries the band's half on its own side. */
|
|
4
|
+
side: Side;
|
|
5
|
+
/** Rendered above the panel icons โ a "new" button, a logo, anything the project pins there. */
|
|
6
|
+
header?: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* An edge's icon rail, IDE-style: it stays in place when the zone is closed, so a closed panel
|
|
11
|
+
* is always one click away.
|
|
12
|
+
*
|
|
13
|
+
* Each rail is split into groups โ its column's panels at the top, its half of the bottom band
|
|
14
|
+
* at the foot โ so that an icon's position tells where the panel will open.
|
|
15
|
+
*/
|
|
16
|
+
export declare function Rail({ side, header, className }: RailProps): import("react").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* One zone's icons, cut the way the zone itself is cut: the icons above the separator open in
|
|
19
|
+
* its first half, the ones below in its second. The rail is the legend of the column.
|
|
20
|
+
*/
|
|
21
|
+
export declare function RailZone<Id extends string = string>({ zone }: {
|
|
22
|
+
zone: Zone;
|
|
23
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type ResizeHandleProps = {
|
|
2
|
+
/** `vertical` moves up and down and sets a height; `horizontal` sets a width. */
|
|
3
|
+
axis: 'vertical' | 'horizontal';
|
|
4
|
+
/** The panel grows as the pointer moves backwards โ true for a right, bottom or lower half. */
|
|
5
|
+
invert?: boolean;
|
|
6
|
+
/** Where the cut stands. Absent means CSS is dividing the box, and the handle measures it. */
|
|
7
|
+
size?: number;
|
|
8
|
+
onSize: (size: number, available: number) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Measures the panel this handle moves, for the case CSS is sizing it and `size` is unknown.
|
|
11
|
+
* The PARENT supplies it because the parent holds the element: reading
|
|
12
|
+
* `previousElementSibling` here would break the moment a consumer wraps a zone.
|
|
13
|
+
*/
|
|
14
|
+
measure?: () => number;
|
|
15
|
+
/** Names the handle for assistive tech โ "Resize the left column". Already translated. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Bounds announced to assistive tech, and the step the keyboard moves by. */
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
step?: number;
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resize handle. Captures the pointer so the gesture survives a cursor leaving the handle โ
|
|
25
|
+
* without capture, a fast drag detaches.
|
|
26
|
+
*
|
|
27
|
+
* It measures the container when the gesture starts and passes that dimension along: it is the
|
|
28
|
+
* one that knows what is available, the store knows nothing about the DOM.
|
|
29
|
+
*
|
|
30
|
+
* It measures the panel it moves through a ref its parent gives it, never through
|
|
31
|
+
* `previousElementSibling`. Reading the sibling works in a tree one owns and breaks silently in
|
|
32
|
+
* a library: a consumer wrapping a zone in a `<div>` would move the wrapper's neighbour instead.
|
|
33
|
+
*
|
|
34
|
+
* Focusable and driven by the arrow keys: a separator that only answers a pointer is a control
|
|
35
|
+
* a keyboard user cannot operate at all (WCAG 2.1.1).
|
|
36
|
+
*/
|
|
37
|
+
export declare function ResizeHandle({ axis, invert, size, onSize, measure, label, min, max, step, className, }: ResizeHandleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type SeparatorProps = {
|
|
2
|
+
orientation?: 'vertical' | 'horizontal';
|
|
3
|
+
className?: string;
|
|
4
|
+
};
|
|
5
|
+
/** Hairline between groups of controls. Decorative, hence hidden from assistive tech. */
|
|
6
|
+
export declare function Separator({ orientation, className }: SeparatorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The rounded box a panel is drawn on, laid over the chassis gutter.
|
|
4
|
+
*
|
|
5
|
+
* Darker than the chassis it sits on in the default theme โ that inversion is what reads as
|
|
6
|
+
* "panels on a frame" rather than as a web page. A project that inverts it back only has to
|
|
7
|
+
* repaint two tokens.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Surface({ children, className, ...rest }: HTMLAttributes<HTMLElement>): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Zone } from '../core/types';
|
|
2
|
+
import type { PanelsLabels } from './labels';
|
|
3
|
+
export type ZoneEdgeProps = {
|
|
4
|
+
zone: Zone;
|
|
5
|
+
labels: PanelsLabels;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A zone's two halves and its resize handle, ordered by the zone. `left` and `top` put the
|
|
9
|
+
* panels first; the opposite zones put the handle first, because they grow backwards.
|
|
10
|
+
*/
|
|
11
|
+
declare function ZoneEdgeInner<Id extends string = string>({ zone, labels }: ZoneEdgeProps): import("react").JSX.Element | null;
|
|
12
|
+
/**
|
|
13
|
+
* Memoised for the reason `PanelFrame` is: with five zones on screen, one frame of a drag used
|
|
14
|
+
* to re-render all five and every panel under them, where two are owed.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ZoneEdge: typeof ZoneEdgeInner;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** What a declared panel renders, kept beside its spec rather than inside the store: React
|
|
3
|
+
* nodes in a store make every subscriber re-render whenever a child changes. */
|
|
4
|
+
export type PanelContent = {
|
|
5
|
+
content: ReactNode;
|
|
6
|
+
actions?: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export declare const ContentProvider: import("react").Provider<ReadonlyMap<string, PanelContent>>;
|
|
9
|
+
export declare function usePanelContent(id: string): PanelContent | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every word the chassis puts on screen or announces. Already translated: the library carries
|
|
3
|
+
* no i18n, and a key would impose a namespace on every project that installs it.
|
|
4
|
+
*/
|
|
5
|
+
export type PanelsLabels = {
|
|
6
|
+
closePanel: string;
|
|
7
|
+
resizeZone: string;
|
|
8
|
+
resizeSplit: string;
|
|
9
|
+
resizeBand: string;
|
|
10
|
+
};
|
|
11
|
+
/** English, so the library works before a project has said anything about words. */
|
|
12
|
+
export declare const DEFAULT_LABELS: PanelsLabels;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type Lengths, type OpenByZone, type Zone } from './types';
|
|
2
|
+
/** Smallest a zone may be dragged to before it is worth closing instead. */
|
|
3
|
+
export declare const MIN_SIZE = 140;
|
|
4
|
+
/** Room the centre must keep, whatever the side zones ask for. */
|
|
5
|
+
export declare const MIN_CENTER = 240;
|
|
6
|
+
/** Room a split keeps for the half it is taken from. */
|
|
7
|
+
export declare const MIN_SPLIT = 100;
|
|
8
|
+
/** What a zone opens at when nothing has been dragged and no panel asks for more. */
|
|
9
|
+
export declare const DEFAULT_SIZES: Record<Zone, number>;
|
|
10
|
+
/**
|
|
11
|
+
* The half that carries the band's height, stored once: two halves lying at two heights would
|
|
12
|
+
* leave the frame above them in a step.
|
|
13
|
+
*/
|
|
14
|
+
export declare const BAND_MAIN: Zone;
|
|
15
|
+
/** Where a zone's length is read and written โ the band's halves share the one key. */
|
|
16
|
+
export declare function sizeKeyOf(zone: Zone): Zone;
|
|
17
|
+
/** The zone that takes room from the same axis, and therefore bounds this one. */
|
|
18
|
+
export declare const OPPOSITE: Record<Zone, Zone>;
|
|
19
|
+
/**
|
|
20
|
+
* Clamps a zone size against what the opposite zone already takes. Capping each side at half
|
|
21
|
+
* the container independently would let left and right add up to the full width, leaving the
|
|
22
|
+
* centre at zero โ and overflowing once the container shrinks.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fitZoneSize(size: number, available: number, opposite: number): number;
|
|
25
|
+
/** Same idea one level down: neither half of a zone may swallow the other. */
|
|
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
|
+
/**
|
|
32
|
+
* The room a zone currently takes, or zero when it draws nothing. Read while clamping the
|
|
33
|
+
* opposite zone: under-report it and the other side may be dragged over room this one is
|
|
34
|
+
* already drawing in, squeezing the centre past its floor.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sizeOf(lengths: Lengths, open: OpenByZone, zone: Zone, undragged: (zone: Zone) => number): number;
|
|
37
|
+
/**
|
|
38
|
+
* Every stored length, re-clamped to a container of this size. Sizes are persisted, so a layout
|
|
39
|
+
* set on a wide screen would otherwise overflow a narrow one โ pushing the panels under the
|
|
40
|
+
* rails and squeezing the centre to nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fitted(lengths: Lengths, open: OpenByZone, width: number, height: number, undragged: (zone: Zone) => number): Lengths;
|
|
43
|
+
/**
|
|
44
|
+
* The two side zones, bounded so the centre keeps its floor โ whether or not either was ever
|
|
45
|
+
* dragged.
|
|
46
|
+
*
|
|
47
|
+
* ๐ `fitted` cannot do this job: it walks the STORED lengths, and a layout nobody has touched
|
|
48
|
+
* has none. The zones then took the sizes they ask for and the centre took whatever was left,
|
|
49
|
+
* which on a narrow container was nothing โ measured at 104 px against a floor of 240, on a
|
|
50
|
+
* 900 px window with two untouched columns.
|
|
51
|
+
*
|
|
52
|
+
* When the two do not fit, they give ground in PROPORTION to what they asked for: taking it all
|
|
53
|
+
* from one would collapse the narrower of them while the wider kept its full width.
|
|
54
|
+
*/
|
|
55
|
+
export declare function sharedSizes(leading: number, trailing: number, available: number): [number, number];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type PanelsState, type PanelsStore } from './store';
|
|
3
|
+
import { type LayoutStorage } from './persistence';
|
|
4
|
+
import type { OpenByZone } from './types';
|
|
5
|
+
export type PanelsProviderProps<Id extends string> = {
|
|
6
|
+
/**
|
|
7
|
+
* A store built by the project. Pass one to drive the chassis from OUTSIDE React โ a socket
|
|
8
|
+
* message, a native menu, a keyboard shortcut all call `store.getState().show(id)`. Left out,
|
|
9
|
+
* the provider makes its own.
|
|
10
|
+
*/
|
|
11
|
+
store?: PanelsStore<Id>;
|
|
12
|
+
/** Key the layout is stored under. Two chassis in one application need two keys. */
|
|
13
|
+
storageKey?: string;
|
|
14
|
+
/** Where the layout is kept. `localStorage` by default; `null` disables persistence. */
|
|
15
|
+
storage?: LayoutStorage | null;
|
|
16
|
+
/** Which halves start open, overriding "the first panel declared for that half". */
|
|
17
|
+
defaultOpen?: OpenByZone<Id>;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
export declare function PanelsProvider<Id extends string = string>({ store, storageKey, storage, defaultOpen, children, }: PanelsProviderProps<Id>): import("react").JSX.Element;
|
|
21
|
+
/** The store this chassis runs on. Throws outside a provider rather than answering nothing. */
|
|
22
|
+
export declare function usePanelsStore<Id extends string = string>(): PanelsStore<Id>;
|
|
23
|
+
/** A slice of the chassis state, subscribed. */
|
|
24
|
+
export declare function usePanelsState<Id extends string, T>(selector: (state: PanelsState<Id>) => T): T;
|
|
25
|
+
/**
|
|
26
|
+
* The actions, which are stable for the store's lifetime โ subscribing to them would only add
|
|
27
|
+
* selectors re-run on every write.
|
|
28
|
+
*/
|
|
29
|
+
export declare function usePanelsActions<Id extends string = string>(): {
|
|
30
|
+
show: (id: Id) => void;
|
|
31
|
+
close: (zone: import("./types").Zone, slot: import("./types").Slot) => void;
|
|
32
|
+
toggle: (id: Id) => void;
|
|
33
|
+
focus: (zone: import("./types").Zone | null) => void;
|
|
34
|
+
resize: (zone: import("./types").Zone, size: number, available: number) => void;
|
|
35
|
+
resplit: (zone: import("./types").Zone, size: number, available: number) => void;
|
|
36
|
+
resplitBand: (size: number, available: number) => void;
|
|
37
|
+
fit: (width: number, height: number) => void;
|
|
38
|
+
reset: () => void;
|
|
39
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type ClassValue = string | false | null | undefined;
|
|
2
|
+
/** Joins class names, dropping the falsy ones. The library owns its class names, so there is
|
|
3
|
+
* nothing to merge or de-duplicate โ a consumer's `className` is appended and wins by order. */
|
|
4
|
+
export declare function cx(...classes: ClassValue[]): string;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type OpenByZone, type PanelSpec, type Zone } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* What every reader of `shownIn` and `zoneDraws` needs: the declared panels, and which half of
|
|
4
|
+
* each zone holds which.
|
|
5
|
+
*
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useArrangement<Id extends string = string>(): {
|
|
11
|
+
registry: PanelSpec<Id>[];
|
|
12
|
+
open: OpenByZone<Id>;
|
|
13
|
+
};
|
|
14
|
+
/** What a zone's two halves actually draw โ a `solo` panel silences the other. */
|
|
15
|
+
export declare function useShownIn<Id extends string = string>(zone: Zone): {
|
|
16
|
+
primary?: Id;
|
|
17
|
+
secondary?: Id;
|
|
18
|
+
};
|
|
19
|
+
/** Whether a zone draws at all. An empty one takes neither room nor handle. */
|
|
20
|
+
export declare function useZoneDraws<Id extends string = string>(zone: Zone): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a zone takes room off the axis it shares with its opposite โ the band counting as one
|
|
23
|
+
* strip. See `zoneTakesRoom`: this is not the same question as whether it draws.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useZoneTakesRoom<Id extends string = string>(zone: Zone): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether each half of the band draws anything โ what the frame asks before it arranges itself:
|
|
28
|
+
* a column runs to the FOOT unless the band's half on its side is drawing.
|
|
29
|
+
*
|
|
30
|
+
* Headless, so it lives here rather than beside `<Band>`: a project replacing the band must be
|
|
31
|
+
* able to ask this without importing from a component file.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useBandHalves<Id extends string = string>(): {
|
|
34
|
+
left: boolean;
|
|
35
|
+
right: boolean;
|
|
36
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Re-clamps the zones when the CONTAINER changes size โ not the window.
|
|
4
|
+
*
|
|
5
|
+
* The difference is what lets the chassis live inside an existing page: measured against
|
|
6
|
+
* `window`, a chassis sitting under a navigation bar or beside a sidebar believes it has room
|
|
7
|
+
* it does not have, and the zones can be dragged over the centre until it disappears.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useContainerFit(ref: RefObject<HTMLElement | null>): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PanelSpec, Zone } from '../types';
|
|
2
|
+
export type PanelsApi<Id extends string> = {
|
|
3
|
+
/** Every panel the project has declared, in declaration order. */
|
|
4
|
+
panels: PanelSpec<Id>[];
|
|
5
|
+
/** Brings a panel up in the half it declared, and focuses its zone. */
|
|
6
|
+
reveal: (id: Id) => void;
|
|
7
|
+
/** Closes THIS panel, or nothing โ never whatever happens to stand in its half. */
|
|
8
|
+
close: (id: Id) => void;
|
|
9
|
+
/** Reveals it if hidden, closes it if shown. What a rail icon does. */
|
|
10
|
+
toggle: (id: Id) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Whether that panel is on screen right now.
|
|
13
|
+
*
|
|
14
|
+
* ๐ Subscribed, and that is the whole point of reading `open` above: written as a plain
|
|
15
|
+
* `getState()` read it answered correctly on the render it was called in and never ran again,
|
|
16
|
+
* so a header built on it kept showing the arrangement as it stood when it last mounted.
|
|
17
|
+
*/
|
|
18
|
+
isShown: (id: Id) => boolean;
|
|
19
|
+
focusedZone: Zone | null;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The chassis, as a project drives it. Everything a header, a shortcut or a menu needs to act
|
|
24
|
+
* on the panels without knowing anything about how they are laid out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function usePanels<Id extends string = string>(): PanelsApi<Id>;
|