@labpane/os 0.0.0-stage → 1.0.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/README.md +177 -2
- package/dist/LICENSE +21 -0
- package/dist/index.js +1968 -0
- package/dist/index.js.map +7 -0
- package/dist/os/context.d.ts +67 -0
- package/dist/os/drag-content.d.ts +11 -0
- package/dist/os/frame.d.ts +25 -0
- package/dist/os/icons.d.ts +4 -0
- package/dist/os/index.d.ts +6 -0
- package/dist/os/layout.d.ts +58 -0
- package/dist/os/surface.d.ts +24 -0
- package/dist/os/tabs.d.ts +23 -0
- package/dist/os/toolbar.d.ts +9 -0
- package/dist/os/unsaved-changes.d.ts +3 -0
- package/dist/os/window.d.ts +15 -0
- package/dist/os/workspace-frame.d.ts +6 -0
- package/dist/os/workspace.d.ts +38 -0
- package/dist/styles.css +1 -0
- package/dist/styles.d.ts +1 -0
- package/package.json +59 -4
package/README.md
CHANGED
|
@@ -1,3 +1,178 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @labpane/os
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A browser-tab workspace for React. Tabs can be reordered, dragged into new panes, split left, right, or below, and resized, with full keyboard support. It depends only on React and ships its own plain CSS.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @labpane/os
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Workspace, type Layout, type WorkspaceTab } from "@labpane/os";
|
|
14
|
+
import "@labpane/os/styles.css";
|
|
15
|
+
|
|
16
|
+
const initial: WorkspaceTab[] = [
|
|
17
|
+
{ key: "inbox", label: "Inbox" },
|
|
18
|
+
{ key: "reports", label: "Reports" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function App() {
|
|
22
|
+
const [tabs, setTabs] = useState(initial);
|
|
23
|
+
const [active, setActive] = useState<string | undefined>("inbox");
|
|
24
|
+
const [layout, setLayout] = useState<Layout>({
|
|
25
|
+
id: "main",
|
|
26
|
+
tabs: initial.map((tab) => tab.key),
|
|
27
|
+
active: "inbox",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ height: "100vh" }}>
|
|
32
|
+
<Workspace
|
|
33
|
+
tabs={tabs}
|
|
34
|
+
active={active}
|
|
35
|
+
onSelect={setActive}
|
|
36
|
+
onClose={(key, next) => {
|
|
37
|
+
setTabs((tabs) => tabs.filter((tab) => tab.key !== key));
|
|
38
|
+
setActive((current) => (current === key ? next : current));
|
|
39
|
+
}}
|
|
40
|
+
layout={layout}
|
|
41
|
+
onLayoutChange={setLayout}
|
|
42
|
+
empty={() => <p>No tabs open</p>}
|
|
43
|
+
>
|
|
44
|
+
{(tab) => <h1>{tab.label}</h1>}
|
|
45
|
+
</Workspace>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The workspace owns no state: you keep the tabs, the active tab, and the layout, so they can live in your router, store, or URL.
|
|
52
|
+
|
|
53
|
+
## Tabs
|
|
54
|
+
|
|
55
|
+
Each tab is a `WorkspaceTab`:
|
|
56
|
+
|
|
57
|
+
| Field | |
|
|
58
|
+
| ----------- | ---------------------------------------------------------------------- |
|
|
59
|
+
| `key` | Unique id |
|
|
60
|
+
| `label` | Text on the tab |
|
|
61
|
+
| `icon` | Optional icon before the label |
|
|
62
|
+
| `actions` | Buttons shown at the end of the pane's toolbar while the tab is active |
|
|
63
|
+
| `href` | Makes the tab a real link, so middle-click and copy link work |
|
|
64
|
+
| `scrollKey` | The panel scrolls to the top when this changes |
|
|
65
|
+
| `opener` | Key of the tab that opened it; closing returns there |
|
|
66
|
+
| `fill` | Removes the panel padding, for full-bleed content such as a terminal |
|
|
67
|
+
|
|
68
|
+
Tabs whose content has unsaved changes call `useUnsavedChanges(true)`. Closing one asks first: through `confirmClose` if you pass it, and the browser's `confirm` otherwise. The page also warns before unloading.
|
|
69
|
+
|
|
70
|
+
## Slots
|
|
71
|
+
|
|
72
|
+
The workspace renders no menus or buttons of its own. You fill these slots with your own components, or with `@labpane/ui`:
|
|
73
|
+
|
|
74
|
+
| Prop | |
|
|
75
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
76
|
+
| `toolbar(pane)` | Content at the end of each pane's toolbar, such as an "open a tab" button |
|
|
77
|
+
| `menu(target, actions, restoreFocus, tab)` | Wraps a tab, or a divider, in your context menu. `actions` are the workspace's own (move, split, close); add your own for that `tab` |
|
|
78
|
+
| `confirmClose(tab)` | Asks before closing a tab with unsaved changes; return or resolve `true` to close |
|
|
79
|
+
| `leading` | Content at the top left of the workspace, such as an app menu |
|
|
80
|
+
| `trailing` | Content pinned to the top right corner, such as window buttons; the top-right pane keeps room for it |
|
|
81
|
+
|
|
82
|
+
## Layout
|
|
83
|
+
|
|
84
|
+
`layout` is a tree of panes and splits:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
type Pane = { id: string; tabs: string[]; active?: string };
|
|
88
|
+
type Split = {
|
|
89
|
+
id: string;
|
|
90
|
+
axis: "x" | "y";
|
|
91
|
+
ratio: number;
|
|
92
|
+
first: Layout;
|
|
93
|
+
second: Layout;
|
|
94
|
+
};
|
|
95
|
+
type Layout = Pane | Split;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`restoreLayout`, `reconcileLayout`, `dockTab`, and `panes` help you save, repair, and inspect layouts, for example after restoring one from storage.
|
|
99
|
+
|
|
100
|
+
## Size
|
|
101
|
+
|
|
102
|
+
`density` scales the workspace from 0 (dense) to 100 (comfortable). `densitySize(density)` returns the matching tab height, logo size, and gaps. The workspace also sets `--lab-tab-height` and `--lab-logo-size` on its element for content placed in its slots.
|
|
103
|
+
|
|
104
|
+
`WorkspaceFrame` wraps a workspace in a centred frame that users can resize or expand to full width.
|
|
105
|
+
|
|
106
|
+
## Desktop windows
|
|
107
|
+
|
|
108
|
+
To run the workspace in a frameless desktop window (Wails, Tauri, Electron, or your own shell), describe the window as a `WindowHost` and the workspace becomes its title bar:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import {
|
|
112
|
+
WindowControls,
|
|
113
|
+
useFullscreen,
|
|
114
|
+
useWindowFrame,
|
|
115
|
+
type WindowHost,
|
|
116
|
+
} from "@labpane/os";
|
|
117
|
+
|
|
118
|
+
const host: WindowHost = {
|
|
119
|
+
platform: "windows", // or "macos" or "linux"
|
|
120
|
+
minimize: () => shell.minimize(),
|
|
121
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
122
|
+
close: () => shell.close(),
|
|
123
|
+
drag: () => shell.startDrag(), // optional
|
|
124
|
+
resize: (edge) => shell.startResize(edge), // optional, for windows without a native frame
|
|
125
|
+
fullscreen: (on) => shell.setFullscreen(on), // optional
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function App() {
|
|
129
|
+
const [fullscreen] = useFullscreen(host);
|
|
130
|
+
useWindowFrame(host);
|
|
131
|
+
return (
|
|
132
|
+
<Workspace
|
|
133
|
+
leading={
|
|
134
|
+
host.platform === "macos" &&
|
|
135
|
+
!fullscreen && <div style={{ width: 72 }} />
|
|
136
|
+
}
|
|
137
|
+
trailing={<WindowControls host={host} />}
|
|
138
|
+
// ...
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- `WindowControls` are Windows-style minimise, maximise, and close buttons, flush in the window's top-right corner and reaching down to the top pane. They hide on macOS, which keeps its own traffic lights, and in full screen.
|
|
145
|
+
- `useWindowFrame` moves the window when an empty part of a toolbar is dragged, maximises it on a double click, resizes it from its edges, and toggles full screen with F11, or Ctrl+Cmd+F on macOS. Add `data-window-drag` to other elements that should move the window.
|
|
146
|
+
- `useFullscreen(host)` returns `[fullscreen, setFullscreen]`. Without a host it uses the browser's Fullscreen API.
|
|
147
|
+
|
|
148
|
+
## Keyboard
|
|
149
|
+
|
|
150
|
+
- Left and Right move between tabs, Home and End jump to the first and last, and Space selects
|
|
151
|
+
- Alt+Left and Alt+Right move the focused tab
|
|
152
|
+
- Alt+Shift+Left, Right, or Down split the focused tab into a new pane on that side
|
|
153
|
+
- Delete closes the focused tab
|
|
154
|
+
- F6 moves between panes
|
|
155
|
+
- Arrow keys on a divider resize the panes beside it
|
|
156
|
+
|
|
157
|
+
## Theming
|
|
158
|
+
|
|
159
|
+
Set these on `:root`, or on any ancestor of the workspace. Add a `dark` class to an ancestor for the dark defaults.
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
:root {
|
|
163
|
+
--lab-font-sans: inherit;
|
|
164
|
+
--lab-background: #f4f1ea;
|
|
165
|
+
--lab-foreground: #242424;
|
|
166
|
+
--lab-card: #ffffff;
|
|
167
|
+
--lab-muted-foreground: #626262;
|
|
168
|
+
--lab-accent: #e6e6e6;
|
|
169
|
+
--lab-border: #d6d6d6;
|
|
170
|
+
--lab-ring: #1d9db4;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
All classes start with `lab-` and the CSS has no global rules, so it doesn't affect the rest of your page.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
package/dist/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Authors of Labpane
|
|
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.
|