@labpane/os 0.0.0-stage → 1.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/README.md CHANGED
@@ -1,3 +1,173 @@
1
- # Temporary Holding Version
1
+ # @labpane/os
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 { desktopSlots, useWindowFrame, type WindowHost } from "@labpane/os";
112
+
113
+ const host: WindowHost = {
114
+ platform: "windows", // or "macos" or "linux"
115
+ minimize: () => shell.minimize(),
116
+ toggleMaximize: () => shell.toggleMaximize(),
117
+ close: () => shell.close(),
118
+ drag: () => shell.startDrag(), // optional
119
+ resize: (edge) => shell.startResize(edge), // optional, for windows without a native frame
120
+ fullscreen: (on) => shell.setFullscreen(on), // optional
121
+ trafficLights: (center) => shell.setTrafficLights(center), // optional, macOS
122
+ };
123
+
124
+ function App() {
125
+ useWindowFrame(host);
126
+ return (
127
+ <Workspace
128
+ titleBar
129
+ {...desktopSlots(host, (align) => <AppMenu align={align} />)}
130
+ // ...
131
+ />
132
+ );
133
+ }
134
+ ```
135
+
136
+ - `desktopSlots(host, menu)` fills the workspace's `leading` and `trailing` slots for the platform: on macOS, room for the traffic lights first and the menu at the end; elsewhere, the menu first and the window buttons at the end. A function `menu` gets the side it lands on, `"start"` or `"end"`, so a dropdown can open inward. Without a host, the menu just leads.
137
+ - `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.
138
+ - `TrafficLights` keeps room for macOS's traffic lights at the start of the toolbar, and hides on other platforms and in full screen. If the host has `trafficLights`, it calls it with the centre of the title bar, in pixels from the top of the window, whenever that moves, so the shell can centre the buttons there.
139
+ - `titleBar` centres the top row's toolbar controls in the title bar, which runs from the top of the window to the top pane, the same as the window buttons. Without it they centre in the toolbar, below any space the page leaves above the workspace.
140
+ - `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.
141
+ - `useFullscreen(host)` returns `[fullscreen, setFullscreen]`. Without a host it uses the browser's Fullscreen API.
142
+
143
+ ## Keyboard
144
+
145
+ - Left and Right move between tabs, Home and End jump to the first and last, and Space selects
146
+ - Alt+Left and Alt+Right move the focused tab
147
+ - Alt+Shift+Left, Right, or Down split the focused tab into a new pane on that side
148
+ - Delete closes the focused tab
149
+ - F6 moves between panes
150
+ - Arrow keys on a divider resize the panes beside it
151
+
152
+ ## Theming
153
+
154
+ Set these on `:root`, or on any ancestor of the workspace. Add a `dark` class to an ancestor for the dark defaults.
155
+
156
+ ```css
157
+ :root {
158
+ --lab-font-sans: inherit;
159
+ --lab-background: #f4f1ea;
160
+ --lab-foreground: #242424;
161
+ --lab-card: #ffffff;
162
+ --lab-muted-foreground: #626262;
163
+ --lab-accent: #e6e6e6;
164
+ --lab-border: #d6d6d6;
165
+ --lab-ring: #1d9db4;
166
+ }
167
+ ```
168
+
169
+ All classes start with `lab-` and the CSS has no global rules, so it doesn't affect the rest of your page.
170
+
171
+ ## License
172
+
173
+ 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.