@dashfoo/react 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pedro Filho
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,407 @@
1
+ # @dashfoo/react
2
+
3
+ The React layer for dashfoo: a headless docking-layout component in the
4
+ FlexLayout / VS-Code mold — tiled, resizable, tabbed regions with drag-to-dock,
5
+ maximize, close, and inline rename.
6
+
7
+ "Headless" is the whole point. `@dashfoo/react` renders semantic markup tagged
8
+ with `data-dashfoo="..."` attributes and applies **zero** visual styling. It
9
+ sizes and positions nodes (flex, percentages, the resize handles) and wires up
10
+ roles, focus, and keyboard behavior. Everything you can see — color, borders,
11
+ radius, spacing, the look of a tab or a dock indicator — is yours to write
12
+ against the `data-dashfoo` selectors. The package owns the geometry; you own the
13
+ paint.
14
+
15
+ It builds on three engines:
16
+
17
+ - **[react-resizable-panels](https://github.com/bvaughn/react-resizable-panels)** for splitter resize (the resize adapter is the only file that imports it).
18
+ - **[@dnd-kit/dom](https://github.com/clauderic/dnd-kit) 0.4** — the framework-agnostic core (no React bindings) — for drag (the drag adapter is the only file that touches it; pointer-only).
19
+ - **[@dashfoo/core](https://www.npmjs.com/package/@dashfoo/core)** for the document: a zod schema, a pure reducer, and the XState machines that drive state and the drag lifecycle.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pnpm add @dashfoo/react @dashfoo/core react react-dom
25
+ ```
26
+
27
+ React 18.3+ or 19 is a peer dependency.
28
+
29
+ ## Quick start
30
+
31
+ A tab's `component` field is a string key. Map those keys to React components
32
+ through the `components` registry; each component receives the live `TabNode`.
33
+
34
+ ```tsx
35
+ import { DashfooLayout } from "@dashfoo/react";
36
+ import type { TabNode } from "@dashfoo/core";
37
+ import { model, row, tabset, tab } from "@dashfoo/core";
38
+
39
+ // Optional default skin — without it the chrome is unstyled (headless).
40
+ import "@dashfoo/theme/dashfoo.css";
41
+
42
+ const startingModel = model(
43
+ row([tabset([tab("editor", "Editor"), tab("preview", "Preview")], { id: "ts1" })]),
44
+ );
45
+
46
+ const Editor = ({ node }: { node: TabNode }) => <div>editing {node.name}</div>;
47
+ const Preview = ({ node }: { node: TabNode }) => <div>preview of {node.name}</div>;
48
+
49
+ export const App = () => (
50
+ <DashfooLayout defaultModel={startingModel} components={{ editor: Editor, preview: Preview }} />
51
+ );
52
+ ```
53
+
54
+ The `model` / `row` / `tabset` / `tab` builders come from `@dashfoo/core`; they
55
+ produce the same plain object you could write by hand. If you don't import
56
+ `@dashfoo/theme`, nothing renders until you style it: the container is
57
+ `[data-dashfoo="layout"]` with `display: flex; height: 100%; width: 100%` — give it
58
+ a sized parent and add your CSS (see the [attribute reference](#data-dashfoo-attribute-reference)).
59
+
60
+ ## `DashfooLayout` props
61
+
62
+ | Prop | Type | Default | Description |
63
+ | ------------------------- | -------------------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
64
+ | `model` | `Dashfoo` | — | Controlled document. When set, the prop is the source of truth. |
65
+ | `defaultModel` | `Dashfoo` | — | Uncontrolled initial document. The component owns it from there. |
66
+ | `onModelChange` | `(model: Dashfoo, action?: Action) => void` | — | Called after every change with the next model and the action that caused it. |
67
+ | `components` | `Record<string, ComponentType<{ node: TabNode }>>` | — | Registry mapping `tab.component` keys to components. |
68
+ | `factory` | `(tab: TabNode) => ReactNode` | — | Render override. When provided, it resolves every tab and `components` is ignored. |
69
+ | `persist` | `string \| { key; storage?; debounceMs? }` | — | Auto-save the model (uncontrolled mode only). A bare string is a localStorage key. |
70
+ | `onAction` | `(action: Action) => Action \| null` | — | Intercept each action before it commits — return it, a replacement, or `null` to veto. |
71
+ | `onActiveTabsetChange` | `(id: string \| undefined) => void` | — | Fires when the active tabset changes. |
72
+ | `onMaximizedTabsetChange` | `(id: string \| undefined) => void` | — | Fires when a tabset is maximized or restored. |
73
+ | `renderTabLabel` | `(tab: TabNode) => ReactNode` | — | Override the tab label (the accessible name stays `tab.name`). |
74
+ | `renderTabsetToolbar` | `(tabset: TabsetNode) => ReactNode` | — | Inject custom controls into a tabset's toolbar. |
75
+ | `closableTabs` | `boolean` | `true` | Show the per-tab close control. |
76
+ | `renamableTabs` | `boolean` | `true` | Allow double-click inline rename. |
77
+ | `maximizable` | `boolean` | `true` | Show the tabset maximize/restore control. |
78
+ | `draggableTabsets` | `boolean` | `true` | Show the grip that drags a whole tabset. |
79
+ | `keepMounted` | `boolean` | `false` | Keep inactive tab panels mounted (hidden) so their state survives a tab switch. |
80
+
81
+ You must pass either `model` or `defaultModel`. Passing neither throws.
82
+
83
+ A `ref` exposes a `DashfooHandle` for imperative control:
84
+ `addTab`, `closeTab`, `selectTab`, `renameTab`, `maximizeTabset`, `dispatch`,
85
+ `getModel`, `undo` / `redo` / `canUndo` / `canRedo`, and `resetLayout()` (reset to
86
+ the original `defaultModel`, clearing undo history and any persisted copy).
87
+
88
+ ```tsx
89
+ const ref = useRef<DashfooHandle>(null);
90
+ <DashfooLayout defaultModel={model} ref={ref} ... />;
91
+ ref.current?.undo();
92
+ ref.current?.resetLayout();
93
+ ```
94
+
95
+ ### Controlled vs. uncontrolled
96
+
97
+ **Uncontrolled** (`defaultModel`): the internal XState actor owns the document
98
+ and gives you undo/redo. `onModelChange` still fires on every change if you want
99
+ to observe or persist.
100
+
101
+ ```tsx
102
+ <DashfooLayout defaultModel={model} components={registry} />
103
+ ```
104
+
105
+ **Controlled** (`model`): your prop is the source of truth. Every interaction
106
+ routes through `onModelChange` with the reduced next model; you store it and pass
107
+ it back.
108
+
109
+ ```tsx
110
+ const [model, setModel] = useState(initialModel);
111
+
112
+ <DashfooLayout model={model} onModelChange={setModel} components={registry} />;
113
+ ```
114
+
115
+ The model is normalized at the boundary on the way in (selection clamped, no
116
+ empty tabsets, `maximizedTabsetId` pointed at a live node), so a hand-written
117
+ model gets the same invariants the reducer guarantees.
118
+
119
+ ### Resolving tab content: registry or factory
120
+
121
+ `factory` wins when both are set. With `components`, a tab whose `component` key
122
+ is not registered renders nothing and logs a one-time dev warning:
123
+ `[dashfoo] no component registered for "<key>"`.
124
+
125
+ ```tsx
126
+ // Registry: keyed lookup, the common case.
127
+ <DashfooLayout components={{ editor: Editor, terminal: Terminal }} ... />
128
+
129
+ // Factory: full control, e.g. switch on node fields or wrap in a boundary.
130
+ <DashfooLayout factory={(tab) => <Pane id={tab.id}>{tab.name}</Pane>} ... />
131
+ ```
132
+
133
+ ### `Panel` — common panel chrome
134
+
135
+ `Panel` is an optional compound helper for common panel chrome: compose a root,
136
+ header, title/icon/badge slots, and body around your content. It's headless — it
137
+ emits `data-dashfoo="panel*"` attributes and the theme styles them.
138
+
139
+ ```tsx
140
+ import { Panel } from "@dashfoo/react";
141
+
142
+ const ChartPanel = ({ node }: { node: TabNode }) => (
143
+ <Panel.Root>
144
+ <Panel.Header>
145
+ <Panel.Icon>
146
+ <MyIcon />
147
+ </Panel.Icon>
148
+ <Panel.Title>{node.name}</Panel.Title>
149
+ <Panel.Badge>Live</Panel.Badge>
150
+ </Panel.Header>
151
+ <Panel.Body>{/* your content */}</Panel.Body>
152
+ </Panel.Root>
153
+ );
154
+ ```
155
+
156
+ | Part | Element | Description |
157
+ | -------------- | -------- | -------------------------------------------------------------- |
158
+ | `Panel.Root` | `<div>` | Panel shell. |
159
+ | `Panel.Header` | `<div>` | Header row. |
160
+ | `Panel.Title` | `<span>` | Header title. |
161
+ | `Panel.Icon` | `<span>` | Optional leading icon slot; consumers choose the icon library. |
162
+ | `Panel.Badge` | `<span>` | Optional trailing badge; accepts arbitrary children. |
163
+ | `Panel.Body` | `<div>` | Scrollable body content. |
164
+
165
+ ## The chrome
166
+
167
+ The component renders interactive controls into the markup. Each is plain HTML
168
+ with the right role and ARIA wiring, ready for you to style.
169
+
170
+ - **Close** — a `[data-dashfoo="tab-close"]` button next to each tab label, shown when closing is enabled. Dispatches `deleteTab`.
171
+ - **Rename** — double-click a tab to swap its label for a `[data-dashfoo="tab-rename"]` input. Enter commits, Escape cancels, blur commits. A trimmed, changed value dispatches `renameTab`; focus returns to the tab afterward.
172
+ - **Maximize** — a `[data-dashfoo="tabset-maximize"]` toggle in the tabset toolbar. Dispatches `setMaximizedTabset`; one maximized tabset fills the frame and `aria-pressed` reflects state.
173
+ - **Tabs** — roving-tabindex keyboard model (WAI-ARIA APG): Arrow keys move and select, Home/End jump to the ends, focus follows selection.
174
+ - **Drag-to-dock** — drag a tab to restack it or split a tabset (when split-dock is on). A `[data-dashfoo="dock-indicator"]` previews where it lands.
175
+
176
+ ### Per-node enable flags
177
+
178
+ The three top-level chrome props are global gates. Individual nodes can opt out
179
+ through optional boolean fields in the model (a flag defaults to enabled unless
180
+ explicitly `false`). A control shows only when both the global prop and the
181
+ node's flag allow it.
182
+
183
+ | Field | On node | Disables |
184
+ | ----------------- | ------- | ------------------------------------------------ |
185
+ | `enableClose` | tab | the close control for that tab |
186
+ | `enableClose` | tabset | closing for every tab in the tabset |
187
+ | `enableRename` | tab | double-click rename for that tab |
188
+ | `enableDrag` | tab | dragging that tab |
189
+ | `enableMaximize` | tabset | the maximize control for that tabset |
190
+ | `enableSplitDock` | global | splitting a tabset on drop (drops stack instead) |
191
+
192
+ ```ts
193
+ { id: "logs", type: "tab", name: "Logs", component: "logs", enableClose: false }
194
+ ```
195
+
196
+ ### Panel sizing
197
+
198
+ Rows and tabsets can carry `min` and `max` dimensions from `@dashfoo/core`.
199
+ `DashfooLayout` passes those constraints to `react-resizable-panels`. When a
200
+ tabset has no node-level `min`, it uses `global.tabSetMinSize`; when that global
201
+ is omitted, the React adapter falls back to `320px`.
202
+
203
+ ## Persistence
204
+
205
+ The `persist` prop saves an uncontrolled layout and restores it on load. It loads
206
+ once (validated through `@dashfoo/core`'s serialize, falling back to
207
+ `defaultModel` on a miss or corrupt value) and debounce-saves every change. A
208
+ `ref` exposes `resetLayout()` to clear the saved copy and return to the default.
209
+
210
+ ```tsx
211
+ import type { DashfooHandle } from "@dashfoo/react";
212
+ import { DashfooLayout } from "@dashfoo/react";
213
+ import { useRef } from "react";
214
+
215
+ const Layout = () => {
216
+ const ref = useRef<DashfooHandle>(null);
217
+
218
+ return (
219
+ <>
220
+ <DashfooLayout defaultModel={model} persist="my-app:layout" ref={ref} components={registry} />
221
+ <button type="button" onClick={() => ref.current?.resetLayout()}>
222
+ Reset layout
223
+ </button>
224
+ </>
225
+ );
226
+ };
227
+ ```
228
+
229
+ `persist` accepts a bare localStorage key or `{ key, storage?, debounceMs? }` for
230
+ a custom store (sessionStorage, in-memory, your own). A pending save flushes on
231
+ unmount, so the last change is never lost. It applies to uncontrolled mode only;
232
+ in controlled mode, save the model yourself in `onModelChange`.
233
+
234
+ The lower-level `usePersistence` load/save primitive (that `persist` is built on)
235
+ is also exported, for hosts that drive the store directly. See the
236
+ [persistence guide](https://docs.dashfoo.dev/persistence) for the storage seam,
237
+ validation pipeline, and SSR notes.
238
+
239
+ ### Options
240
+
241
+ The full form of `persist` (`{ key, storage?, debounceMs? }`):
242
+
243
+ | Option | Type | Default | Description |
244
+ | ------------ | ---------------- | --------------------- | ------------------------------------------------- |
245
+ | `key` | `string` | — (required) | Storage key. A bare `persist="key"` is shorthand. |
246
+ | `storage` | `StorageAdapter` | `localStorageAdapter` | Where to read and write. |
247
+ | `debounceMs` | `number` | `300` | Save debounce window. |
248
+
249
+ ### Storage adapters
250
+
251
+ `StorageAdapter` is a `localStorage`-shaped backend, so a layout can persist to
252
+ the browser, an in-memory map, or a custom store.
253
+
254
+ ```ts
255
+ type StorageAdapter = {
256
+ getItem: (key: string) => string | null;
257
+ removeItem: (key: string) => void;
258
+ setItem: (key: string, value: string) => void;
259
+ };
260
+ ```
261
+
262
+ Two adapters ship with the package:
263
+
264
+ - `localStorageAdapter` — SSR-safe `window.localStorage`. Reads return `null` and writes are swallowed (with a warning) when storage is unavailable or throws (no window, private mode, quota exceeded). This is the default.
265
+ - `memoryStorageAdapter()` — a fresh in-memory `Map`, returned by a factory call. Good for tests and SSR previews.
266
+
267
+ ```tsx
268
+ import { memoryStorageAdapter } from "@dashfoo/react";
269
+
270
+ const storage = useMemo(() => memoryStorageAdapter(), []);
271
+
272
+ <DashfooLayout defaultModel={model} persist={{ key: "preview", storage }} />;
273
+ ```
274
+
275
+ ## `data-dashfoo` attribute reference
276
+
277
+ Every styleable element carries a `data-dashfoo` attribute. Selectors are stable;
278
+ target them in your stylesheet. The package sets only the positioning styles it
279
+ needs inline (sizes, flex, the dock indicator's position) and leaves the rest to
280
+ you.
281
+
282
+ | `data-dashfoo` value | Element | Notes |
283
+ | -------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
284
+ | `layout` | root `div` | The outer container. `display: flex` over the full parent. |
285
+ | `row` | rrp `Group` | A resizable row/column. `orientation` comes from the node. |
286
+ | `splitter` | rrp `Separator` | Resize handle between siblings (also matches `[data-separator]`, see below). |
287
+ | `tabset` | `div` | A tabbed region. Carries `data-dragging-source` while the whole tabset is being dragged, and `data-tab-location` (`top`/`bottom`). |
288
+ | `tabstrip` | `div` | The strip row: tablist plus a trailing toolbar slot. |
289
+ | `tablist` | `div` | `role="tablist"`, the tabs themselves. |
290
+ | `tab-item` | `span` | Wraps one tab's button and its close button. `data-dragging` while dragged. |
291
+ | `tab` | `button` | `role="tab"`. Carries `aria-selected` and `data-tab-id`. |
292
+ | `tab-close` | `button` | Per-tab close. `aria-label="Close <name>"`. |
293
+ | `tab-rename` | `input` | Inline rename editor, shown during a rename. |
294
+ | `tabset-toolbar` | `div` | Trailing controls in the strip: overflow menu, grip, custom toolbar slot, maximize. |
295
+ | `tab-overflow-root` | `div` | Wraps the overflow trigger and its menu; rendered when tabs don't fit the strip. |
296
+ | `tab-overflow` | `button` | Overflow menu trigger. `aria-label="More tabs"`. |
297
+ | `tab-overflow-menu` | `div` | `role="menu"`, lists the hidden tabs while open. |
298
+ | `tab-overflow-item` | `button` | `role="menuitem"`, one hidden tab; selecting it activates the tab. |
299
+ | `tabset-grip` | `button` | Drags the whole tabset. `aria-label="Move tabset"`; shown when `draggableTabsets` is on and the tabset is not maximized. |
300
+ | `tabset-maximize` | `button` | Maximize/restore toggle. `aria-pressed` reflects state. |
301
+ | `tabcontent` | `div` | `role="tabpanel"`, the active tab's content (or empty when none). |
302
+ | `dock-indicator` | `div` | The drag preview overlay (insertion line or zone). `pointer-events: none`. |
303
+ | `drag-preview` | `div` | The chip that follows the pointer during a drag, showing the dragged label. |
304
+ | `separator` | rrp `Separator` | rrp emits `data-separator` with `aria-orientation`; style splitters here. |
305
+
306
+ The `splitter` handle is dashfoo's name; react-resizable-panels also stamps the
307
+ same element with `data-separator` and an `aria-orientation` of `vertical` or
308
+ `horizontal`. Either selector works. Orientation lives on the separator, so size
309
+ the handle and pick the cursor off `[data-separator][aria-orientation="..."]`.
310
+
311
+ ### Dock indicator CSS variables
312
+
313
+ The `[data-dashfoo="dock-indicator"]` overlay positions itself inline, but every
314
+ visual property reads from a CSS variable with a neutral fallback. Override them
315
+ to theme the drag preview without touching layout.
316
+
317
+ | Variable | Used for | Fallback |
318
+ | ----------------------------- | ---------------------------- | ---------------------------------------------- |
319
+ | `--dashfoo-dock-fill` | zone fill (split preview) | `oklch(0.556 0 0 / 0.18)` |
320
+ | `--dashfoo-dock-border` | zone border color | `oklch(0.708 0 0 / 0.75)` |
321
+ | `--dashfoo-dock-border-width` | zone border width | `1px` |
322
+ | `--dashfoo-dock-radius` | zone corner radius | `6px` |
323
+ | `--dashfoo-dock-line` | tab insertion line color | `oklch(0.556 0 0)` |
324
+ | `--dashfoo-dock-line-radius` | insertion line corner radius | `2px` |
325
+ | `--dashfoo-dock-transition` | indicator move animation | `left 60ms, top 60ms, width 60ms, height 60ms` |
326
+
327
+ Set `--dashfoo-dock-transition: none` to disable the indicator animation.
328
+
329
+ ```css
330
+ :root {
331
+ --dashfoo-dock-fill: rgba(255, 255, 255, 0.1);
332
+ --dashfoo-dock-border: rgba(255, 255, 255, 0.4);
333
+ --dashfoo-dock-line: rgba(255, 255, 255, 0.85);
334
+ }
335
+ ```
336
+
337
+ ## Hooks and the store
338
+
339
+ `useDashfooStore` is what `DashfooLayout` uses internally. Reach for it when you
340
+ need the store's controls (undo/redo, raw dispatch) outside the default chrome.
341
+
342
+ ```ts
343
+ const store = useDashfooStore({ defaultModel });
344
+ // store: { model, dispatch, undo, redo, canUndo, canRedo, setModel }
345
+ ```
346
+
347
+ It binds an XState actor to React, normalizes the incoming model, and exposes
348
+ `dispatch` plus undo/redo. Uncontrolled mode gives full history; controlled mode
349
+ routes changes through `onModelChange` and keeps the actor in sync.
350
+
351
+ `useDashfooContext` returns the live context (`dispatch`, `renderTab`, the chrome
352
+ flags) and throws outside a `<DashfooLayout>`. Use it from a custom view.
353
+
354
+ ## Exports
355
+
356
+ From the package root:
357
+
358
+ ```ts
359
+ // Component + types
360
+ DashfooLayout;
361
+ type DashfooLayoutProps;
362
+ type DashfooHandle; // the imperative ref handle (undo/redo, addTab, resetLayout, …)
363
+ type TabComponent;
364
+
365
+ // Panel helper
366
+ Panel;
367
+ type PanelBadgeProps;
368
+ type PanelBodyProps;
369
+ type PanelHeaderProps;
370
+ type PanelIconProps;
371
+ type PanelRootProps;
372
+ type PanelTitleProps;
373
+
374
+ // Store
375
+ useDashfooStore;
376
+ type DashfooStore;
377
+ type UseDashfooStoreOptions;
378
+
379
+ // Context
380
+ DashfooContext;
381
+ useDashfooContext;
382
+ type DashfooContextValue;
383
+
384
+ // Persistence
385
+ usePersistence; // load/save primitive (the `persist` prop builds on this)
386
+ localStorageAdapter;
387
+ memoryStorageAdapter;
388
+ type StorageAdapter;
389
+ type Persistence;
390
+ type PersistConfig;
391
+
392
+ // Responsive
393
+ useResponsiveModel;
394
+ matchBreakpoint; // does a breakpoint apply at a given width
395
+ activeBreakpoint; // first matching breakpoint, else the last (the catch-all)
396
+ type Breakpoint;
397
+ type ResponsiveModel; // what useResponsiveModel returns
398
+ type UseResponsiveModelOptions;
399
+ ```
400
+
401
+ The document type `Dashfoo`, node types (`TabNode`, `TabsetNode`, `RowNode`),
402
+ the `Action` union, the reducer, and the serialize helpers come
403
+ from `@dashfoo/core`.
404
+
405
+ ## License
406
+
407
+ MIT
@@ -0,0 +1,152 @@
1
+ import { ComponentProps, ComponentType, ReactNode } from "react";
2
+ import { Action, Dashfoo, DockLocation, TabNode, TabsetNode } from "@dashfoo/core";
3
+
4
+ //#region src/hooks/context.d.ts
5
+ type DashfooContextValue = {
6
+ closableTabs: boolean;
7
+ dispatch: (action: Action) => void;
8
+ draggableTabsets: boolean;
9
+ keepMounted: boolean;
10
+ maximizable: boolean;
11
+ maximizedTabsetId?: string;
12
+ renamableTabs: boolean;
13
+ renderTab: (tab: TabNode) => ReactNode;
14
+ renderTabLabel?: (tab: TabNode) => ReactNode;
15
+ renderTabsetToolbar?: (tabset: TabsetNode) => ReactNode;
16
+ tabLocation: "bottom" | "top";
17
+ tabsetMinSize: number;
18
+ tabStripEnabled: boolean;
19
+ };
20
+ declare const DashfooContext: import("react").Context<DashfooContextValue | null>;
21
+ declare const useDashfooContext: () => DashfooContextValue;
22
+ //#endregion
23
+ //#region src/hooks/persistence.d.ts
24
+ type StorageAdapter = {
25
+ getItem: (key: string) => string | null;
26
+ removeItem: (key: string) => void;
27
+ setItem: (key: string, value: string) => void;
28
+ };
29
+ declare const memoryStorageAdapter: () => StorageAdapter;
30
+ declare const localStorageAdapter: StorageAdapter;
31
+ type PersistConfig = {
32
+ debounceMs: number;
33
+ key: string;
34
+ storage: StorageAdapter;
35
+ };
36
+ type Persistence = {
37
+ clear: () => void;
38
+ initialModel: Dashfoo | undefined;
39
+ save: (model: Dashfoo) => void;
40
+ };
41
+ declare const usePersistence: (config: PersistConfig | null, defaultModel: Dashfoo | undefined) => Persistence;
42
+ //#endregion
43
+ //#region src/components/dashfoo-layout.d.ts
44
+ type PersistInput = string | {
45
+ debounceMs?: number;
46
+ key: string;
47
+ storage?: StorageAdapter;
48
+ };
49
+ type TabComponent = ComponentType<{
50
+ node: TabNode;
51
+ }>;
52
+ type DashfooHandle = {
53
+ addTab: (tab: TabNode, target: {
54
+ index?: number;
55
+ location?: DockLocation;
56
+ targetId: string;
57
+ }) => void;
58
+ canRedo: () => boolean;
59
+ canUndo: () => boolean;
60
+ closeTab: (tabId: string) => void;
61
+ dispatch: (action: Action) => void;
62
+ getModel: () => Dashfoo;
63
+ maximizeTabset: (tabsetId: string | null) => void;
64
+ redo: () => void;
65
+ renameTab: (tabId: string, name: string) => void;
66
+ resetLayout: () => void;
67
+ selectTab: (tabsetId: string, index: number) => void;
68
+ undo: () => void;
69
+ };
70
+ type DashfooLayoutProps = {
71
+ closableTabs?: boolean;
72
+ components?: Record<string, TabComponent>;
73
+ defaultModel?: Dashfoo;
74
+ draggableTabsets?: boolean;
75
+ factory?: (tab: TabNode) => ReactNode;
76
+ keepMounted?: boolean;
77
+ maximizable?: boolean;
78
+ model?: Dashfoo;
79
+ onAction?: (action: Action) => Action | null;
80
+ onActiveTabsetChange?: (tabsetId: string | undefined) => void;
81
+ onMaximizedTabsetChange?: (tabsetId: string | undefined) => void;
82
+ onModelChange?: (model: Dashfoo, action?: Action) => void;
83
+ persist?: PersistInput;
84
+ renamableTabs?: boolean;
85
+ renderTabLabel?: (tab: TabNode) => ReactNode;
86
+ renderTabsetToolbar?: (tabset: TabsetNode) => ReactNode;
87
+ };
88
+ declare const DashfooLayout: import("react").ForwardRefExoticComponent<DashfooLayoutProps & import("react").RefAttributes<DashfooHandle>>;
89
+ //#endregion
90
+ //#region src/components/panel.d.ts
91
+ type PanelRootProps = ComponentProps<"div">;
92
+ type PanelHeaderProps = ComponentProps<"div">;
93
+ type PanelBodyProps = ComponentProps<"div">;
94
+ type PanelTitleProps = ComponentProps<"span">;
95
+ type PanelIconProps = ComponentProps<"span">;
96
+ type PanelBadgeProps = ComponentProps<"span">;
97
+ declare const Panel: {
98
+ readonly Badge: (props: PanelBadgeProps) => ReactNode;
99
+ readonly Body: (props: PanelBodyProps) => ReactNode;
100
+ readonly Header: (props: PanelHeaderProps) => ReactNode;
101
+ readonly Icon: (props: PanelIconProps) => ReactNode;
102
+ readonly Root: (props: PanelRootProps) => ReactNode;
103
+ readonly Title: (props: PanelTitleProps) => ReactNode;
104
+ };
105
+ //#endregion
106
+ //#region src/hooks/responsive.d.ts
107
+ type Breakpoint = {
108
+ id: string;
109
+ model: Dashfoo;
110
+ query?: {
111
+ maxWidth: number;
112
+ } | {
113
+ media: string;
114
+ };
115
+ };
116
+ type UseResponsiveModelOptions = {
117
+ breakpoints: Array<Breakpoint>;
118
+ };
119
+ type ResponsiveModel = {
120
+ breakpoint: string;
121
+ containerRef: (element: HTMLElement | null) => void;
122
+ defaultModel: Dashfoo;
123
+ key: string;
124
+ };
125
+ declare const matchBreakpoint: (breakpoint: Breakpoint, width: number) => boolean;
126
+ declare const activeBreakpoint: (breakpoints: Array<Breakpoint>, width: number) => Breakpoint;
127
+ declare const useResponsiveModel: ({
128
+ breakpoints
129
+ }: UseResponsiveModelOptions) => ResponsiveModel;
130
+ //#endregion
131
+ //#region src/hooks/store.d.ts
132
+ type DashfooStore = {
133
+ canRedo: () => boolean;
134
+ canUndo: () => boolean;
135
+ dispatch: (action: Action) => void;
136
+ model: Dashfoo;
137
+ redo: () => void;
138
+ setModel: (model: Dashfoo) => void;
139
+ undo: () => void;
140
+ };
141
+ type UseDashfooStoreOptions = {
142
+ defaultModel?: Dashfoo;
143
+ model?: Dashfoo;
144
+ onAction?: (action: Action) => Action | null;
145
+ onActiveTabsetChange?: (tabsetId: string | undefined) => void;
146
+ onMaximizedTabsetChange?: (tabsetId: string | undefined) => void;
147
+ onModelChange?: (model: Dashfoo, action?: Action) => void;
148
+ };
149
+ declare const useDashfooStore: (options: UseDashfooStoreOptions) => DashfooStore;
150
+ //#endregion
151
+ export { type Breakpoint, DashfooContext, type DashfooContextValue, type DashfooHandle, DashfooLayout, type DashfooLayoutProps, type DashfooStore, Panel, type PanelBadgeProps, type PanelBodyProps, type PanelHeaderProps, type PanelIconProps, type PanelRootProps, type PanelTitleProps, type PersistConfig, type Persistence, type ResponsiveModel, type StorageAdapter, type TabComponent, type UseDashfooStoreOptions, type UseResponsiveModelOptions, activeBreakpoint, localStorageAdapter, matchBreakpoint, memoryStorageAdapter, useDashfooContext, useDashfooStore, usePersistence, useResponsiveModel };
152
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/hooks/context.ts","../src/hooks/persistence.ts","../src/components/dashfoo-layout.tsx","../src/components/panel.tsx","../src/hooks/responsive.ts","../src/hooks/store.ts"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use client";import{Fragment as e,createContext as t,forwardRef as n,useCallback as r,useContext as i,useEffect as a,useImperativeHandle as o,useInsertionEffect as s,useLayoutEffect as c,useMemo as l,useRef as u,useState as d}from"react";import{canRedo as f,canUndo as p,dashfooMachine as m,dragDockMachine as h,findTabset as g,fromJSON as _,normalize as v,reducer as y,resolveDockTarget as b,toJSON as x,zoneRect as S}from"@dashfoo/core";import{useActorRef as C,useSelector as w}from"@xstate/react";import{Accessibility as T,DragDropManager as E,Draggable as D,Feedback as O,KeyboardSensor as k}from"@dnd-kit/dom";import{Fragment as A,jsx as j,jsxs as M}from"react/jsx-runtime";import{Group as N,Panel as P,Separator as F}from"react-resizable-panels";const I=t(null),L=()=>{let e=i(I);if(e===null)throw Error(`dashfoo components must be rendered inside <DashfooLayout>.`);return e},R=()=>{let e=new Map;return{getItem:t=>e.get(t)??null,removeItem:t=>{e.delete(t)},setItem:(t,n)=>{e.set(t,n)}}},z={getItem:e=>{if(typeof window>`u`)return null;try{return window.localStorage.getItem(e)}catch{return null}},removeItem:e=>{if(!(typeof window>`u`))try{window.localStorage.removeItem(e)}catch{}},setItem:(e,t)=>{if(!(typeof window>`u`))try{window.localStorage.setItem(e,t)}catch(e){console.warn(`[dashfoo] failed to persist layout`,e)}}},B=(e,t)=>{let n=u(e);a(()=>{n.current=e});let[i]=d(()=>{if(e===null||t===void 0)return t;let n=e.storage.getItem(e.key);if(n===null)return t;try{return _(n)}catch{return t}});a(()=>{let e=n.current;if(e===null)return;let t=e.storage.getItem(e.key);if(t!==null)try{_(t)}catch(t){console.warn(`[dashfoo] discarding unreadable persisted layout`,t),e.storage.removeItem(e.key)}},[]);let o=u(e?.key);a(()=>{e!==null&&e.key!==o.current&&(console.warn(`[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key — remount (key={persistKey}) to load the new key`),o.current=e.key)},[e]);let s=u(null),c=u(null),l=r(()=>{s.current!==null&&(clearTimeout(s.current),s.current=null);let e=n.current;c.current!==null&&e!==null&&(e.storage.setItem(c.current.key,c.current.value),c.current=null)},[]),f=r(e=>{let t=n.current;t!==null&&(c.current={key:t.key,value:x(e)},s.current!==null&&clearTimeout(s.current),s.current=setTimeout(l,t.debounceMs))},[l]);return a(()=>()=>{l()},[l]),{clear:r(()=>{s.current!==null&&(clearTimeout(s.current),s.current=null),c.current=null;let e=n.current;e!==null&&e.storage.removeItem(e.key)},[]),initialModel:i,save:f}},V=e=>{let{defaultModel:t,model:n,onAction:i,onActiveTabsetChange:o,onMaximizedTabsetChange:s,onModelChange:c}=e,l=n??t;if(l===void 0)throw Error("useDashfooStore requires either a `model` or a `defaultModel`.");let u=C(m,{input:{model:v(l)}}),d=w(u,e=>e.context.history);a(()=>{n!==void 0&&u.send({model:v(n),type:`SET_MODEL`})},[u,n]);let h=n??d.present,g=r((e,t,n)=>{Object.is(e,t)||(c?.(t,n),t.activeTabsetId!==e.activeTabsetId&&o?.(t.activeTabsetId),t.maximizedTabsetId!==e.maximizedTabsetId&&s?.(t.maximizedTabsetId))},[c,o,s]),_=r(e=>{let t=i?i(e):e;if(!t)return;let r=(()=>{if(n===void 0){let e=u.getSnapshot().context.history.present;return u.send({action:t,type:`DISPATCH`}),{after:u.getSnapshot().context.history.present,before:e}}return{after:y(n,t),before:v(n)}})();g(r.before,r.after,t)},[u,n,g,i]),b=r(()=>{if(n!==void 0)return;let e=u.getSnapshot().context.history.present;u.send({type:`UNDO`});let t=u.getSnapshot().context.history.present;g(e,t)},[u,n,g]);return{canRedo:()=>f(u.getSnapshot().context.history),canUndo:()=>p(u.getSnapshot().context.history),dispatch:_,model:h,redo:r(()=>{if(n!==void 0)return;let e=u.getSnapshot().context.history.present;u.send({type:`REDO`});let t=u.getSnapshot().context.history.present;g(e,t)},[u,n,g]),setModel:r(e=>{u.send({model:v(e),type:`SET_MODEL`})},[u]),undo:b}},H=t(null),ee=t(null),te=()=>{let e=u(null),t=u(null);return{draggableRef:t,elementRef:e,ref:r(n=>{e.current=n,t.current&&(t.current.element=n??void 0)},[])}},ne=(e,t,n)=>{let r=i(H),{draggableRef:o,elementRef:s,ref:c}=te(),{data:l,id:u}=e;return a(()=>{let e=r?.manager;if(!e||t)return;let n=new D({data:{...l,label:``},id:u},e);return n.element=s.current??void 0,o.current=n,()=>{n.destroy(),o.current=null}},[r,l,t,o,s,u]),a(()=>{let e=o.current;e&&(e.data={...l,label:n})},[l,o,n]),{ref:c}},re=(e,t=!1,n=``)=>ne({data:l(()=>({type:`tab`}),[]),id:e},t,n),ie=(e,t=!1,n=``)=>ne({data:l(()=>({tabsetId:e,type:`tabset`}),[e]),id:`grip-${e}`},t,n),ae=e=>{let t=i(H);return{ref:r(n=>{t?.registerTabset(e,n)},[t,e])}},U=()=>i(ee),W=(e,t)=>e.x>=t.x&&e.x<=t.x+t.width&&e.y>=t.y&&e.y<=t.y+t.height,oe=(e,t)=>{let n=e.findIndex(e=>t<e.x+e.width/2);return n===-1?e.length:n},se=(e,t,n)=>{let r=t[n],i=t.at(-1),a=r?.x??(i?i.x+i.width:e.x);return{height:e.height,width:2,x:a-1,y:e.y}},ce=(e,t,n)=>!(e===`grip-${t}`||n.length===1&&n[0]===e),G=(e,t)=>[...e.querySelectorAll(`[data-dashfoo="tab"]`)].flatMap(e=>e.dataset.tabId===t?[]:[e.getBoundingClientRect()]),le=(e,t)=>[...e.querySelectorAll(`[data-dashfoo="tab-item"]`)].flatMap(e=>e.querySelector(`[data-dashfoo="tab"]`)?.dataset.tabId===t?[]:[e.getBoundingClientRect()]),ue=(e,t,n,r)=>{let i=t.querySelector(`[data-dashfoo="tabstrip"]`);if(i&&W(n,i.getBoundingClientRect()))return{index:oe(G(i,r),n.x),location:`center`,targetId:e};let a=b(n,t.getBoundingClientRect()),o=a.kind===`tab`?`center`:`split-${a.edge}`;return o===`center`&&i?{index:G(i,r).length,location:o,targetId:e}:{location:o,targetId:e}},K={boxSizing:`border-box`,pointerEvents:`none`,position:`fixed`,transition:`var(--dashfoo-dock-transition, left 60ms, top 60ms, width 60ms, height 60ms)`,zIndex:9999},de=e=>({...K,background:`var(--dashfoo-dock-fill, oklch(0.556 0 0 / 0.18))`,border:`var(--dashfoo-dock-border-width, 1px) solid var(--dashfoo-dock-border, oklch(0.708 0 0 / 0.75))`,borderRadius:`var(--dashfoo-dock-radius, 6px)`,height:e.height,left:e.x,top:e.y,width:e.width}),fe=e=>({...K,background:`var(--dashfoo-dock-line, oklch(0.556 0 0))`,borderRadius:`var(--dashfoo-dock-line-radius, 2px)`,height:e.height,left:e.x,top:e.y,width:e.width}),pe=({actorRef:e,getTabsetElement:t})=>{let n=w(e,e=>e.context.intent),r=w(e,e=>e.context.subject?.id);if(!n)return null;let i=t(n.targetId);if(!i)return null;if(n.location===`center`){let e=i.querySelector(`[data-dashfoo="tabstrip"]`);if(e)return j(`div`,{"data-dashfoo":`dock-indicator`,style:fe(se(e.getBoundingClientRect(),le(e,r),n.index??0))},`line`)}return j(`div`,{"data-dashfoo":`dock-indicator`,style:de(S(i.getBoundingClientRect(),n.location))},`pane`)},q={x:12,y:8},me={left:0,position:`fixed`,top:0,zIndex:9999},he=({overlayRef:e,preview:t})=>t?j(`div`,{"data-dashfoo":`drag-preview`,ref:e,style:{...me,transform:`translate(${t.x}px, ${t.y}px)`},children:t.label}):null,ge=e=>{let t=e?.data?.label;return typeof t==`string`?t:``},_e=({children:e,onCommit:t,splitDock:n=!0})=>{let i=C(h),o=w(i,e=>e.context.subject),c=u(new Map),f=u(null),[p,m]=d(null),[g]=d(()=>new E({plugins:e=>e.filter(e=>e!==T&&e!==O),sensors:e=>e.filter(e=>e!==k)}));s(()=>()=>g.destroy(),[g]),a(()=>{let e=i.on(`COMMIT`,e=>{t(e.action)});return()=>{e.unsubscribe()}},[i,t]);let _=r((e,t)=>{t?c.current.set(e,t):c.current.delete(e)},[]),v=r(e=>c.current.get(e),[]),y=r(e=>{for(let[t,n]of c.current)if(W(e,n.getBoundingClientRect()))return{element:n,id:t}},[]),b=r((e,t,r,i)=>{if(!ce(i,e,[...t.querySelectorAll(`[data-dashfoo="tab"]`)].map(e=>e.dataset.tabId??``)))return null;let a=ue(e,t,r,i);return!n&&a.location.startsWith(`split-`)?{location:`center`,targetId:e}:a},[n]),x=r(e=>{let t=f.current;t&&(t.style.transform=`translate(${e.x+q.x}px, ${e.y+q.y}px)`)},[]),S=r(e=>{f.current=e},[]);a(()=>{let e=e=>{let t=e.operation.source;if(!t)return;let n=t.data?.type===`tabset`,r=e.operation.position.current;m({label:ge(t),x:r.x+q.x,y:r.y+q.y}),i.send({subject:{id:String(n?t.data.tabsetId:t.id),kind:n?`tabset`:`tab`},type:`START`})},t=e=>{let t=e.operation,n=t.position.current;x(n);let r=t.source?String(t.source.id):void 0,a=y(n);i.send({intent:a?b(a.id,a.element,n,r):null,type:`OVER`})},n=e=>{if(m(null),e.canceled){i.send({type:`CANCEL`});return}let t=e.operation,n=t.position.current,r=t.source?String(t.source.id):void 0,a=y(n);a&&i.send({intent:b(a.id,a.element,n,r),type:`OVER`}),i.send({type:`DROP`})},r=g.monitor.addEventListener(`dragstart`,e),a=g.monitor.addEventListener(`dragmove`,t),o=g.monitor.addEventListener(`dragend`,n);return()=>{r(),a(),o()}},[i,g,x,b,y]);let D=l(()=>({manager:g,registerTabset:_}),[g,_]);return j(H.Provider,{value:D,children:M(ee.Provider,{value:o,children:[e,j(pe,{actorRef:i,getTabsetElement:v}),j(he,{overlayRef:S,preview:p})]})})},ve=e=>{let t=e.getBoundingClientRect();return[...e.querySelectorAll(`[data-dashfoo="tab"]`)].flatMap(e=>{let n=e.getBoundingClientRect();return(n.right>t.right+1||n.left<t.left-1)&&e.dataset.tabId?[e.dataset.tabId]:[]})},ye=(e,t)=>{let[n,r]=d([]);return a(()=>{let t=e.current;if(!t)return;let n=()=>{r(ve(t))};n();let i=new ResizeObserver(n);return i.observe(t),t.addEventListener(`scroll`,n,{passive:!0}),()=>{i.disconnect(),t.removeEventListener(`scroll`,n)}},[e,t]),n},J=(e,t)=>`dashfoo-tab-${e}-${t}`,Y=e=>`dashfoo-panel-${e}`,be=(e,t)=>t+1<e?t+1:t-1,xe={pointerEvents:`none`},X=({children:e})=>j(`svg`,{"aria-hidden":`true`,fill:`none`,height:`12`,stroke:`currentColor`,strokeLinecap:`round`,strokeLinejoin:`round`,strokeWidth:`2`,style:xe,viewBox:`0 0 24 24`,width:`12`,children:e}),Se=()=>M(X,{children:[j(`path`,{d:`M18 6 6 18`}),j(`path`,{d:`m6 6 12 12`})]}),Ce=()=>M(X,{children:[j(`circle`,{cx:`12`,cy:`5`,r:`1`}),j(`circle`,{cx:`19`,cy:`5`,r:`1`}),j(`circle`,{cx:`5`,cy:`5`,r:`1`}),j(`circle`,{cx:`12`,cy:`12`,r:`1`}),j(`circle`,{cx:`19`,cy:`12`,r:`1`}),j(`circle`,{cx:`5`,cy:`12`,r:`1`}),j(`circle`,{cx:`12`,cy:`19`,r:`1`}),j(`circle`,{cx:`19`,cy:`19`,r:`1`}),j(`circle`,{cx:`5`,cy:`19`,r:`1`})]}),we=({maximized:e})=>e?M(X,{children:[j(`path`,{d:`m14 10 7-7`}),j(`path`,{d:`M20 10h-6V4`}),j(`path`,{d:`m3 21 7-7`}),j(`path`,{d:`M4 14h6v6`})]}):M(X,{children:[j(`path`,{d:`M15 3h6v6`}),j(`path`,{d:`m21 3-7 7`}),j(`path`,{d:`m3 21 7-7`}),j(`path`,{d:`M9 21H3v-6`})]}),Te=({name:e,onCancel:t,onCommit:n})=>{let r=u(null),i=u(!1);return a(()=>{r.current?.focus(),r.current?.select()},[]),j(`input`,{"aria-label":`Rename ${e}`,"data-dashfoo":`tab-rename`,defaultValue:e,onBlur:e=>{i.current||(i.current=!0,n(e.currentTarget.value))},onKeyDown:e=>{e.key===`Enter`?(i.current=!0,n(e.currentTarget.value)):e.key===`Escape`&&(i.current=!0,t())},ref:r,type:`text`})},Ee=({closable:e,index:t,onClose:n,renamable:r,selected:i,tab:o,tabsetId:s})=>{let{dispatch:c,renderTabLabel:l}=L(),{ref:f}=re(o.id,o.enableDrag===!1,o.name),p=U(),m=p?.kind===`tab`&&p.id===o.id,[h,g]=d(!1),_=u(null),v=u(!1);return a(()=>{v.current&&!h&&_.current?.querySelector(`[data-dashfoo="tab"]`)?.focus(),v.current=h},[h]),M(`span`,{"data-dashfoo":`tab-item`,"data-dragging":m||void 0,ref:_,children:[h?j(Te,{name:o.name,onCancel:()=>{g(!1)},onCommit:e=>{g(!1);let t=e.trim();t&&t!==o.name&&c({name:t,tabId:o.id,type:`renameTab`})}}):j(`button`,{"aria-controls":Y(s),"aria-label":l?o.name:void 0,"aria-selected":t===i,"data-dashfoo":`tab`,"data-tab-id":o.id,id:J(s,o.id),onClick:()=>{c({index:t,tabsetId:s,type:`selectTab`})},onDoubleClick:()=>{r&&g(!0)},ref:f,role:`tab`,tabIndex:t===i?0:-1,type:`button`,children:l?l(o):o.name}),e&&!h?j(`button`,{"aria-label":`Close ${o.name}`,"data-dashfoo":`tab-close`,onClick:e=>{e.stopPropagation(),n(o.id)},tabIndex:t===i?0:-1,type:`button`,children:j(Se,{})}):null]})},De=()=>M(`svg`,{"aria-hidden":`true`,fill:`currentColor`,height:`10`,viewBox:`0 0 10 10`,width:`10`,children:[j(`circle`,{cx:`2`,cy:`5`,r:`1`}),j(`circle`,{cx:`5`,cy:`5`,r:`1`}),j(`circle`,{cx:`8`,cy:`5`,r:`1`})]}),Oe={position:`absolute`,right:0,top:`100%`,zIndex:10},ke=({items:e,onSelect:t})=>{let[n,r]=d(!1),[i,o]=d(0),s=u(null),c=u(null),l=u([]);a(()=>{if(!n)return;let e=e=>{e.target instanceof Node&&!s.current?.contains(e.target)&&r(!1)};return document.addEventListener(`pointerdown`,e),()=>{document.removeEventListener(`pointerdown`,e)}},[n]),a(()=>{n&&l.current[0]?.focus()},[n]);let f=()=>{r(!1),c.current?.focus()},p=e=>{o(e),l.current[e]?.focus()},m=()=>{o(0),r(e=>!e)},h=e=>{t(e),f()};return M(`div`,{"data-dashfoo":`tab-overflow-root`,ref:s,style:{position:`relative`},children:[j(`button`,{"aria-expanded":n,"aria-haspopup":`menu`,"aria-label":`More tabs`,"data-dashfoo":`tab-overflow`,onClick:m,ref:c,type:`button`,children:j(De,{})}),n?j(`div`,{"data-dashfoo":`tab-overflow-menu`,onBlur:e=>{e.relatedTarget instanceof Node&&s.current?.contains(e.relatedTarget)||r(!1)},onKeyDown:t=>{let n=e.length-1;switch(t.key){case`ArrowDown`:t.preventDefault(),p(i>=n?0:i+1);break;case`ArrowUp`:t.preventDefault(),p(i<=0?n:i-1);break;case`Home`:t.preventDefault(),p(0);break;case`End`:t.preventDefault(),p(n);break;case`Escape`:t.preventDefault(),f();break;default:break}},role:`menu`,style:Oe,tabIndex:-1,children:e.map((e,t)=>j(`button`,{"data-dashfoo":`tab-overflow-item`,onClick:()=>h(e.id),ref:e=>{l.current[t]=e},role:`menuitem`,tabIndex:t===i?0:-1,type:`button`,children:e.name},e.id))}):null]})},Ae={display:`flex`,flexDirection:`column`,height:`100%`,minHeight:0,minWidth:0,width:`100%`},je={display:`flex`,flexShrink:0},Me={display:`flex`,minWidth:0,overflowX:`auto`},Z={flex:1,minHeight:0,overflow:`auto`},Ne={alignItems:`center`,display:`flex`,flexShrink:0,marginInlineStart:`auto`},Pe=({tab:e})=>{let{renderTab:t}=L();return t(e)},Fe=({node:e})=>{let{closableTabs:t,dispatch:n,draggableTabsets:r,keepMounted:i,maximizable:o,maximizedTabsetId:s,renamableTabs:c,renderTabsetToolbar:d,tabLocation:f,tabStripEnabled:p}=L(),{ref:m}=ae(e.id),h=U(),g=s===e.id,_=u(null),v=u(null),y=u(!1),b=t&&e.enableClose!==!1,x=o&&e.enableMaximize!==!1,S=h?.kind===`tab`?e.children.findIndex(e=>e.id===h.id):-1,C=S!==-1&&S===e.selected?be(e.children.length,S):e.selected,w=e.children[C],T=h?.kind===`tabset`&&h.id===e.id,{ref:E}=ie(e.id,!r||g,w?.name??``),D=()=>{n({tabsetId:g?null:e.id,type:`setMaximizedTabset`})},O=ye(_,e.children.map(e=>`${e.id}:${e.name}`).join(`|`)),k=l(()=>{let t=new Map(e.children.map(e=>[e.id,e.name]));return O.map(e=>({id:e,name:t.get(e)??e}))},[O,e.children]),N=t=>{let r=e.children.findIndex(e=>e.id===t);r!==-1&&(n({index:r,tabsetId:e.id,type:`selectTab`}),_.current?.querySelector(`#${CSS.escape(J(e.id,t))}`)?.scrollIntoView({block:`nearest`,inline:`nearest`}))},P=e=>{y.current=!0,n({tabId:e,type:`deleteTab`})};a(()=>{if(!y.current)return;y.current=!1;let t=e.children[e.selected]?.id;t?_.current?.querySelector(`#${CSS.escape(J(e.id,t))}`)?.focus():v.current?.focus()},[e.children,e.selected,e.id]);let F=p?M(`div`,{"data-dashfoo":`tabstrip`,style:je,children:[j(`div`,{"aria-label":e.name??`Tabs`,"aria-orientation":`horizontal`,"data-dashfoo":`tablist`,onKeyDown:t=>{if(!(t.target instanceof HTMLElement)||!t.target.closest(`[role="tab"]`))return;let r=e.children.length;if(r===0)return;let i={ArrowLeft:(e.selected-1+r)%r,ArrowRight:(e.selected+1)%r,End:r-1,Home:0}[t.key];if(i===void 0)return;t.preventDefault(),n({index:i,tabsetId:e.id,type:`selectTab`});let a=e.children[i]?.id;a&&_.current?.querySelector(`#${CSS.escape(J(e.id,a))}`)?.focus()},ref:_,role:`tablist`,style:Me,tabIndex:-1,children:e.children.map((t,n)=>j(Ee,{closable:b&&t.enableClose!==!1,index:n,onClose:P,renamable:c&&t.enableRename!==!1,selected:C,tab:t,tabsetId:e.id},t.id))}),x||d||k.length>0||r&&!g?M(`div`,{"data-dashfoo":`tabset-toolbar`,style:Ne,children:[k.length>0?j(ke,{items:k,onSelect:N}):null,r&&!g?j(`button`,{"aria-label":`Move tabset`,"data-dashfoo":`tabset-grip`,ref:E,type:`button`,children:j(Ce,{})}):null,d?.(e),x?j(`button`,{"aria-label":g?`Restore`:`Maximize`,"aria-pressed":g,"data-dashfoo":`tabset-maximize`,onClick:D,type:`button`,children:j(we,{maximized:g})}):null]}):null]}):null,I=i&&e.children.length>0?e.children.map((t,n)=>j(`div`,{"aria-labelledby":J(e.id,t.id),"data-dashfoo":`tabcontent`,hidden:n!==C||void 0,id:n===C?Y(e.id):void 0,role:n===C?`tabpanel`:void 0,style:Z,tabIndex:n===C?0:void 0,children:j(Pe,{tab:t})},t.id)):w?j(`div`,{"aria-labelledby":J(e.id,w.id),"data-dashfoo":`tabcontent`,id:Y(e.id),role:`tabpanel`,style:Z,tabIndex:0,children:j(Pe,{tab:w})}):j(`div`,{"data-dashfoo":`tabcontent`,style:Z});return j(`div`,{"data-dashfoo":`tabset`,"data-dragging-source":T||void 0,"data-tab-location":f,ref:e=>{m(e),v.current=e},style:Ae,tabIndex:-1,children:f===`bottom`?M(A,{children:[I,F]}):M(A,{children:[F,I]})})},Ie=e=>`${e.value}${e.unit}`,Le=e=>e?.unit===`px`?e.value:void 0,Re={display:`flex`,flex:1,minHeight:0,minWidth:0},ze=.01,Be=(e,t)=>e.reduce((e,n)=>(e[n.id]=(n.weight??1)/t*100,e),{}),Ve=(e,t)=>{let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?r.every(n=>Math.abs((e[n]??NaN)-t[n])<=ze):!1},He=(e,t,n)=>{let r=Le(e.min);if(r!==void 0)return r;if(e.min!==void 0)return;if(e.type===`tabset`)return n;let i=[];for(let r of e.children){let e=He(r,t,n);if(e===void 0)return;i.push(e)}if(i.length!==0)return e.orientation===t?i.reduce((e,t)=>e+t,0):Math.max(...i)},Q=({node:t})=>{let{dispatch:n,tabsetMinSize:r}=L(),i=t.orientation===`row`?`horizontal`:`vertical`,a=t.children.reduce((e,t)=>e+(t.weight??1),0),o=l(()=>Be(t.children,a),[t.children,a]),s=u(!1),d=u(null),f=u(!1);return c(()=>{let e=d.current;if(!e||Ve(e.getLayout(),o))return;f.current=!0,e.setLayout(o);let t=requestAnimationFrame(()=>{f.current=!1});return()=>{cancelAnimationFrame(t),f.current=!1}},[o]),j(N,{"data-dashfoo":`row`,groupRef:d,onLayoutChanged:e=>{if(!s.current){s.current=!0;return}if(f.current)return;let r=t.children.map(t=>e[t.id]??t.weight??1);r.some((e,n)=>Math.abs(e-(t.children[n]?.weight??1))>ze)&&n({rowId:t.id,type:`adjustSplit`,weights:r})},orientation:i,style:Re,children:t.children.map((n,i)=>{let o=(n.weight??1)/a*100,s=n.min?Ie(n.min):void 0;if(s===void 0){let e=He(n,t.orientation,r);s=e===void 0?void 0:`${e}px`}let c=n.max?Ie(n.max):void 0;return M(e,{children:[i>0?j(F,{"data-dashfoo":`splitter`}):null,j(P,{defaultSize:`${o}%`,id:n.id,maxSize:c,minSize:s,children:n.type===`row`?j(Q,{node:n}):j(Fe,{node:n})})]},n.id)})},t.children.map(e=>e.id).join(`|`))},Ue=(e,t)=>e===void 0||!t?null:typeof e==`string`?{debounceMs:300,key:e,storage:z}:{debounceMs:e.debounceMs??300,key:e.key,storage:e.storage??z},We=new Set,Ge={display:`flex`,height:`100%`,width:`100%`},Ke=n((e,t)=>{let{closableTabs:n=!0,components:i,defaultModel:a,draggableTabsets:s=!0,factory:c,keepMounted:u=!1,maximizable:d=!0,model:f,onAction:p,onActiveTabsetChange:m,onMaximizedTabsetChange:h,onModelChange:_,persist:v,renamableTabs:y=!0,renderTabLabel:b,renderTabsetToolbar:x}=e,S=B(l(()=>Ue(v,a!==void 0),[v,a]),a),C=r((e,t)=>{S.save(e),_?.(e,t)},[_,S]),w=V({defaultModel:S.initialModel??a,model:f,onAction:p,onActiveTabsetChange:m,onMaximizedTabsetChange:h,onModelChange:C});o(t,()=>({addTab:(e,t)=>w.dispatch({index:t.index,location:t.location??`center`,tab:e,targetId:t.targetId,type:`addNode`}),canRedo:w.canRedo,canUndo:w.canUndo,closeTab:e=>w.dispatch({tabId:e,type:`deleteTab`}),dispatch:w.dispatch,getModel:()=>w.model,maximizeTabset:e=>w.dispatch({tabsetId:e,type:`setMaximizedTabset`}),redo:w.redo,renameTab:(e,t)=>w.dispatch({name:t,tabId:e,type:`renameTab`}),resetLayout:()=>{S.clear(),a!==void 0&&w.setModel(a)},selectTab:(e,t)=>w.dispatch({index:t,tabsetId:e,type:`selectTab`}),undo:w.undo}),[a,S,w]);let T=r(e=>{if(c)return c(e);let t=i?.[e.component];return t?j(t,{node:e}):(i&&!We.has(e.component)&&(We.add(e.component),console.warn(`[dashfoo] no component registered for "${e.component}"`)),null)},[i,c]),E=w.model.global,D=n&&E.tabEnableClose!==!1,O=y&&E.tabEnableRename!==!1,k=d&&E.tabSetEnableMaximize!==!1,A=E.tabLocation??`top`,M=E.tabSetMinSize??320,N=E.tabSetEnableTabStrip!==!1,P=l(()=>({closableTabs:D,dispatch:w.dispatch,draggableTabsets:s,keepMounted:u,maximizable:k,maximizedTabsetId:w.model.maximizedTabsetId,renamableTabs:O,renderTab:T,renderTabLabel:b,renderTabsetToolbar:x,tabLocation:A,tabsetMinSize:M,tabStripEnabled:N}),[s,D,k,O,u,T,b,x,w.dispatch,w.model.maximizedTabsetId,A,M,N]),F=w.dispatch,L=E.enableSplitDock!==!1,R=E.splitterSize===void 0?Ge:{...Ge,"--dashfoo-splitter-size":`${E.splitterSize}px`},z=w.model.maximizedTabsetId?g(w.model,w.model.maximizedTabsetId):void 0;return j(I.Provider,{value:P,children:j(_e,{onCommit:F,splitDock:L,children:j(`div`,{"data-dashfoo":`layout`,style:R,children:z?j(Fe,{node:z}):j(Q,{node:w.model.layout})})})})});Ke.displayName=`DashfooLayout`;const qe={Badge:e=>j(`span`,{...e,"data-dashfoo":`panel-badge`}),Body:e=>j(`div`,{...e,"data-dashfoo":`panel-body`}),Header:e=>j(`div`,{...e,"data-dashfoo":`panel-header`}),Icon:e=>j(`span`,{...e,"data-dashfoo":`panel-icon`}),Root:e=>j(`div`,{...e,"data-dashfoo":`panel`}),Title:e=>j(`span`,{...e,"data-dashfoo":`panel-title`})},Je=()=>typeof window<`u`&&typeof window.matchMedia==`function`,Ye=(e,t)=>{let{query:n}=e;return n?`maxWidth`in n?t<=n.maxWidth:Je()&&window.matchMedia(n.media).matches:!0},$=(e,t)=>{let n=e.at(-1);if(!n)throw Error(`useResponsiveModel requires at least one breakpoint.`);return e.find(e=>Ye(e,t))??n},Xe=({breakpoints:e})=>{let[t,n]=d(1/0),[,i]=d(0),o=u(null),s=r(e=>{if(o.current?.disconnect(),!e)return;let t=new ResizeObserver(e=>{let t=e[0];t&&n(t.contentRect.width)});t.observe(e),o.current=t},[]);a(()=>()=>{o.current?.disconnect()},[]),a(()=>{if(!Je())return;let t=e.flatMap(e=>e.query&&`media`in e.query?[window.matchMedia(e.query.media)]:[]);if(t.length===0)return;let n=()=>{i(e=>e+1)};for(let e of t)e.addEventListener(`change`,n);return()=>{for(let e of t)e.removeEventListener(`change`,n)}},[e]);let c=$(e,t);return{breakpoint:c.id,containerRef:s,defaultModel:c.model,key:c.id}};export{I as DashfooContext,Ke as DashfooLayout,qe as Panel,$ as activeBreakpoint,z as localStorageAdapter,Ye as matchBreakpoint,R as memoryStorageAdapter,L as useDashfooContext,V as useDashfooStore,B as usePersistence,Xe as useResponsiveModel};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["Panel"],"sources":["../src/hooks/context.ts","../src/hooks/persistence.ts","../src/hooks/store.ts","../src/hooks/drag-hooks.tsx","../src/lib/tab-insertion.ts","../src/components/drag-adapter.tsx","../src/hooks/use-tab-overflow.ts","../src/lib/tab-ids.ts","../src/lib/tab-selection.ts","../src/components/tabset-icons.tsx","../src/components/tab-button.tsx","../src/components/tab-overflow.tsx","../src/components/tabset-view.tsx","../src/components/row-view.tsx","../src/components/dashfoo-layout.tsx","../src/components/panel.tsx","../src/hooks/responsive.ts"],"sourcesContent":["\"use client\";\n\nimport type { Action, TabNode, TabsetNode } from \"@dashfoo/core\";\nimport type { ReactNode } from \"react\";\nimport { createContext, useContext } from \"react\";\n\ntype DashfooContextValue = {\n closableTabs: boolean;\n dispatch: (action: Action) => void;\n draggableTabsets: boolean;\n keepMounted: boolean;\n maximizable: boolean;\n maximizedTabsetId?: string;\n renamableTabs: boolean;\n renderTab: (tab: TabNode) => ReactNode;\n renderTabLabel?: (tab: TabNode) => ReactNode;\n renderTabsetToolbar?: (tabset: TabsetNode) => ReactNode;\n tabLocation: \"bottom\" | \"top\";\n tabsetMinSize: number;\n tabStripEnabled: boolean;\n};\n\nconst DashfooContext = createContext<DashfooContextValue | null>(null);\n\nconst useDashfooContext = (): DashfooContextValue => {\n const value = useContext(DashfooContext);\n if (value === null) {\n throw new Error(\"dashfoo components must be rendered inside <DashfooLayout>.\");\n }\n return value;\n};\n\nexport { DashfooContext, useDashfooContext };\nexport type { DashfooContextValue };\n","\"use client\";\n\nimport type { Dashfoo } from \"@dashfoo/core\";\nimport { fromJSON, toJSON } from \"@dashfoo/core\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n// A minimal localStorage-shaped backend so layouts can persist to anything: the\n// browser, sessionStorage, an in-memory map, or a custom store.\ntype StorageAdapter = {\n getItem: (key: string) => string | null;\n removeItem: (key: string) => void;\n setItem: (key: string, value: string) => void;\n};\n\nconst memoryStorageAdapter = (): StorageAdapter => {\n const store = new Map<string, string>();\n return {\n getItem: (key) => store.get(key) ?? null,\n removeItem: (key) => {\n store.delete(key);\n },\n setItem: (key, value) => {\n store.set(key, value);\n },\n };\n};\n\n// SSR-safe browser adapter: reads return null and writes are swallowed (with a\n// warning) when storage is unavailable or throws (no window, private mode, quota).\nconst localStorageAdapter: StorageAdapter = {\n getItem: (key) => {\n if (typeof window === \"undefined\") {\n return null;\n }\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n removeItem: (key) => {\n if (typeof window === \"undefined\") {\n return;\n }\n try {\n window.localStorage.removeItem(key);\n } catch {\n // storage unavailable — nothing to remove.\n }\n },\n setItem: (key, value) => {\n if (typeof window === \"undefined\") {\n return;\n }\n try {\n window.localStorage.setItem(key, value);\n } catch (error) {\n // A persistence failure (quota, private mode) must not be silent.\n // oxlint-disable-next-line no-console\n console.warn(\"[dashfoo] failed to persist layout\", error);\n }\n },\n};\n\n// A resolved persistence target: where to save and how often. DashfooLayout's\n// `persist` prop normalizes its input to this.\ntype PersistConfig = {\n debounceMs: number;\n key: string;\n storage: StorageAdapter;\n};\n\ntype Persistence = {\n clear: () => void;\n // undefined only in controlled mode, where no defaultModel seeds the load.\n initialModel: Dashfoo | undefined;\n save: (model: Dashfoo) => void;\n};\n\n// The load/save half of persistence, behind the DashfooLayout `persist` prop.\n// Loads the saved model once (validated via serialize.ts, falling back to\n// `defaultModel` on miss or corruption) and\n// debounce-saves every change. Resetting the live model is the caller's job —\n// this primitive only owns the storage side. A null config makes it a no-op, so\n// callers can run the hook unconditionally.\nconst usePersistence = (\n config: PersistConfig | null,\n defaultModel: Dashfoo | undefined,\n): Persistence => {\n // Refs keep the callbacks stable while always seeing the latest config; synced\n // in an effect (never written during render).\n const configRef = useRef(config);\n useEffect(() => {\n configRef.current = config;\n });\n\n // Load the saved model once. The initializer stays pure (no writes during\n // render); a corrupt stored value is pruned in the mount effect below.\n const [initialModel] = useState<Dashfoo | undefined>(() => {\n if (config === null || defaultModel === undefined) {\n return defaultModel;\n }\n const raw = config.storage.getItem(config.key);\n if (raw === null) {\n return defaultModel;\n }\n try {\n return fromJSON(raw);\n } catch {\n return defaultModel;\n }\n });\n\n useEffect(() => {\n const current = configRef.current;\n if (current === null) {\n return;\n }\n const raw = current.storage.getItem(current.key);\n if (raw === null) {\n return;\n }\n try {\n fromJSON(raw);\n } catch (error) {\n // Mirror the setItem warn: a discarded corrupt layout must not be silent.\n // oxlint-disable-next-line no-console\n console.warn(\"[dashfoo] discarding unreadable persisted layout\", error);\n current.storage.removeItem(current.key);\n }\n }, []);\n\n // We load + prune once, but save/flush/clear target the *current* key. Changing\n // config.key for a mounted layout would save the stale model under the new key\n // and clobber it; warn instead of doing that silently. Remounting is the fix.\n const loadedKey = useRef(config?.key);\n useEffect(() => {\n if (config !== null && config.key !== loadedKey.current) {\n // oxlint-disable-next-line no-console\n console.warn(\n \"[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key — remount (key={persistKey}) to load the new key\",\n );\n loadedKey.current = config.key;\n }\n }, [config]);\n\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n // The pending write carries the key it was produced for, so a key change while\n // a save is queued cannot write the old value under the new key.\n const pending = useRef<{ key: string; value: string } | null>(null);\n\n const flush = useCallback((): void => {\n if (timer.current !== null) {\n clearTimeout(timer.current);\n timer.current = null;\n }\n const current = configRef.current;\n if (pending.current !== null && current !== null) {\n current.storage.setItem(pending.current.key, pending.current.value);\n pending.current = null;\n }\n }, []);\n\n const save = useCallback(\n (next: Dashfoo): void => {\n const current = configRef.current;\n if (current === null) {\n return;\n }\n pending.current = { key: current.key, value: toJSON(next) };\n if (timer.current !== null) {\n clearTimeout(timer.current);\n }\n timer.current = setTimeout(flush, current.debounceMs);\n },\n [flush],\n );\n\n // Flush a pending save on unmount so the last change is never lost.\n useEffect(\n () => () => {\n flush();\n },\n [flush],\n );\n\n const clear = useCallback((): void => {\n if (timer.current !== null) {\n clearTimeout(timer.current);\n timer.current = null;\n }\n pending.current = null;\n const current = configRef.current;\n if (current !== null) {\n current.storage.removeItem(current.key);\n }\n }, []);\n\n return { clear, initialModel, save };\n};\n\nexport { localStorageAdapter, memoryStorageAdapter, usePersistence };\nexport type { Persistence, PersistConfig, StorageAdapter };\n","\"use client\";\n\nimport type { Action, Dashfoo } from \"@dashfoo/core\";\nimport { canRedo, canUndo, dashfooMachine, normalize, reducer } from \"@dashfoo/core\";\nimport { useActorRef, useSelector } from \"@xstate/react\";\nimport { useCallback, useEffect } from \"react\";\n\ntype DashfooStore = {\n canRedo: () => boolean;\n canUndo: () => boolean;\n dispatch: (action: Action) => void;\n model: Dashfoo;\n redo: () => void;\n setModel: (model: Dashfoo) => void;\n undo: () => void;\n};\n\ntype UseDashfooStoreOptions = {\n defaultModel?: Dashfoo;\n model?: Dashfoo;\n onAction?: (action: Action) => Action | null;\n onActiveTabsetChange?: (tabsetId: string | undefined) => void;\n onMaximizedTabsetChange?: (tabsetId: string | undefined) => void;\n onModelChange?: (model: Dashfoo, action?: Action) => void;\n};\n\n// Binds a dashfooMachine actor to React. Uncontrolled (defaultModel) lets the\n// actor own the document with full undo/redo; controlled (model) makes the prop\n// the source of truth and routes every change through onModelChange. The\n// actor/inspector stays in sync once the host round-trips `after` back into the\n// `model` prop (it is not synced synchronously within dispatch).\nconst useDashfooStore = (options: UseDashfooStoreOptions): DashfooStore => {\n const {\n defaultModel,\n model: controlledModel,\n onAction,\n onActiveTabsetChange,\n onMaximizedTabsetChange,\n onModelChange,\n } = options;\n const initialModel = controlledModel ?? defaultModel;\n if (initialModel === undefined) {\n throw new Error(\"useDashfooStore requires either a `model` or a `defaultModel`.\");\n }\n\n // Normalize at the boundary so a host-supplied model satisfies the same\n // invariants (clamped selection, no empty tabsets, live maximizedTabsetId) the\n // reducer guarantees — every entry point holds a canonical model.\n const actorRef = useActorRef(dashfooMachine, { input: { model: normalize(initialModel) } });\n const history = useSelector(actorRef, (snapshot) => snapshot.context.history);\n\n useEffect(() => {\n if (controlledModel !== undefined) {\n actorRef.send({ model: normalize(controlledModel), type: \"SET_MODEL\" });\n }\n }, [actorRef, controlledModel]);\n\n const model = controlledModel ?? history.present;\n\n // Single notification path so dispatch and undo/redo stay in lockstep. The\n // Object.is guard makes a no-op transition (e.g. undo at the bottom of the\n // stack, where the history helper returns the same object) emit nothing, so we\n // never write spurious persistence on a change that did not happen.\n const notify = useCallback(\n (before: Dashfoo, after: Dashfoo, action?: Action) => {\n if (Object.is(before, after)) {\n return;\n }\n onModelChange?.(after, action);\n if (after.activeTabsetId !== before.activeTabsetId) {\n onActiveTabsetChange?.(after.activeTabsetId);\n }\n if (after.maximizedTabsetId !== before.maximizedTabsetId) {\n onMaximizedTabsetChange?.(after.maximizedTabsetId);\n }\n },\n [onModelChange, onActiveTabsetChange, onMaximizedTabsetChange],\n );\n\n const dispatch = useCallback(\n (action: Action) => {\n // onAction may veto (null) or replace the action before it mutates anything.\n const resolved = onAction ? onAction(action) : action;\n if (!resolved) {\n return;\n }\n // Uncontrolled: the actor owns history. Controlled: derive the next model by\n // hand (the host round-trips it through the model prop). normalize the\n // controlled `before` too, else a normalization-only id change fires spuriously.\n const transition = ((): { after: Dashfoo; before: Dashfoo } => {\n if (controlledModel === undefined) {\n const before = actorRef.getSnapshot().context.history.present;\n actorRef.send({ action: resolved, type: \"DISPATCH\" });\n return { after: actorRef.getSnapshot().context.history.present, before };\n }\n return { after: reducer(controlledModel, resolved), before: normalize(controlledModel) };\n })();\n notify(transition.before, transition.after, resolved);\n },\n [actorRef, controlledModel, notify, onAction],\n );\n\n const undo = useCallback(() => {\n // actor history is always empty in controlled mode, so undo would be a dead\n // send that still emitted a spurious onModelChange.\n if (controlledModel !== undefined) {\n return;\n }\n const before = actorRef.getSnapshot().context.history.present;\n actorRef.send({ type: \"UNDO\" });\n const after = actorRef.getSnapshot().context.history.present;\n notify(before, after);\n }, [actorRef, controlledModel, notify]);\n\n const redo = useCallback(() => {\n if (controlledModel !== undefined) {\n return;\n }\n const before = actorRef.getSnapshot().context.history.present;\n actorRef.send({ type: \"REDO\" });\n const after = actorRef.getSnapshot().context.history.present;\n notify(before, after);\n }, [actorRef, controlledModel, notify]);\n\n // Replace the whole document, resetting undo history. Drives an uncontrolled\n // reset (e.g. clearing a persisted layout back to its default) without a\n // remount; normalized at the boundary like every other entry point.\n const setModel = useCallback(\n (next: Dashfoo) => {\n actorRef.send({ model: normalize(next), type: \"SET_MODEL\" });\n },\n [actorRef],\n );\n\n // Functions, not booleans: they read the live actor snapshot so a caller\n // reading them right after undo/redo (e.g. inside onModelChange) sees the fresh\n // value, not the one-render-stale useSelector result.\n return {\n canRedo: () => canRedo(actorRef.getSnapshot().context.history),\n canUndo: () => canUndo(actorRef.getSnapshot().context.history),\n dispatch,\n model,\n redo,\n setModel,\n undo,\n };\n};\n\nexport { useDashfooStore };\nexport type { DashfooStore, UseDashfooStoreOptions };\n","\"use client\";\n\nimport type { DragSubject } from \"@dashfoo/core\";\nimport { Draggable } from \"@dnd-kit/dom\";\nimport type { DragDropManager } from \"@dnd-kit/dom\";\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef } from \"react\";\n\n// Shared drag contexts + the draggable/droppable hooks. Lives apart from\n// drag-adapter.tsx so the contexts can be owned here without a circular import:\n// drag-adapter imports these for DragProvider and re-exports the hooks, and this\n// module never reaches back into drag-adapter.\n\ntype DragContextValue = {\n manager: DragDropManager;\n registerTabset: (id: string, element: HTMLElement | null) => void;\n};\n\nconst DragContext = createContext<DragContextValue | null>(null);\n\nconst DragSubjectContext = createContext<DragSubject | null>(null);\n\n// Tracks a Draggable's element across renders without rebuilding it. Returned by\n// the draggable hooks; their effects own the Draggable's lifecycle.\ntype DraggableHandle = {\n draggableRef: { current: Draggable | null };\n elementRef: { current: Element | null };\n ref: (element: Element | null) => void;\n};\n\nconst useDraggableHandle = (): DraggableHandle => {\n const elementRef = useRef<Element | null>(null);\n const draggableRef = useRef<Draggable | null>(null);\n const ref = useCallback((element: Element | null): void => {\n elementRef.current = element;\n if (draggableRef.current) {\n draggableRef.current.element = element ?? undefined;\n }\n }, []);\n return { draggableRef, elementRef, ref };\n};\n\n// What a draggable carries: the dnd-kit id and the stable data (drag kind +\n// tabset id), both derived from identity. The chip label is passed separately so\n// it never sits in the construction effect's deps. data must be referentially\n// stable (the wrappers memoize it) so the construction effect tracks it without\n// rebuilding on every render.\ntype DraggableDescriptor = { data: Record<string, unknown>; id: string };\n\n// Owns the full Draggable lifecycle shared by the tab and tabset hooks. The\n// construction effect is keyed on identity only (manager, id, disabled, the\n// stable data) — never on the chip label — so a tab click or rename can't tear\n// down and rebuild the live instance. The label is folded into the live data by\n// a separate effect, since data is only read at drag start; both effects fire in\n// the same mount commit, so the label is current well before any pointer drag.\n// `Draggable.data` is a public accessor in @dnd-kit/abstract, so the in-place\n// update is type-safe.\nconst useDraggableEntity = (\n descriptor: DraggableDescriptor,\n disabled: boolean,\n label: string,\n): { ref: (element: Element | null) => void } => {\n const context = useContext(DragContext);\n const { draggableRef, elementRef, ref } = useDraggableHandle();\n const { data, id } = descriptor;\n\n useEffect(() => {\n const manager = context?.manager;\n if (!manager || disabled) {\n return undefined;\n }\n const draggable = new Draggable({ data: { ...data, label: \"\" }, id }, manager);\n draggable.element = elementRef.current ?? undefined;\n draggableRef.current = draggable;\n return () => {\n draggable.destroy();\n draggableRef.current = null;\n };\n }, [context, data, disabled, draggableRef, elementRef, id]);\n\n useEffect(() => {\n const draggable = draggableRef.current;\n if (draggable) {\n draggable.data = { ...data, label };\n }\n }, [data, draggableRef, label]);\n\n return { ref };\n};\n\nconst useTabDraggable = (\n tabId: string,\n disabled = false,\n label = \"\",\n): { ref: (element: Element | null) => void } => {\n const data = useMemo(() => ({ type: \"tab\" }), []);\n return useDraggableEntity({ data, id: tabId }, disabled, label);\n};\n\n// The whole tabset is draggable from its grip. A distinct dnd-kit id (grip-*)\n// avoids colliding with the tabset's own registered id; the real tabset id rides\n// in `data` and becomes the moveTabset subject. The label feeds the overlay chip.\nconst useTabsetDraggable = (\n tabsetId: string,\n disabled = false,\n label = \"\",\n): { ref: (element: Element | null) => void } => {\n const data = useMemo(() => ({ tabsetId, type: \"tabset\" }), [tabsetId]);\n return useDraggableEntity({ data, id: `grip-${tabsetId}` }, disabled, label);\n};\n\n// Registers the tabset element so the adapter can hit-test the pointer against it.\nconst useTabsetDroppable = (tabsetId: string): { ref: (element: HTMLElement | null) => void } => {\n const context = useContext(DragContext);\n const ref = useCallback(\n (element: HTMLElement | null): void => {\n context?.registerTabset(tabsetId, element);\n },\n [context, tabsetId],\n );\n return { ref };\n};\n\nconst useDragSubject = (): DragSubject | null => useContext(DragSubjectContext);\n\nexport type { DragContextValue };\nexport {\n DragContext,\n DragSubjectContext,\n useDragSubject,\n useTabDraggable,\n useTabsetDraggable,\n useTabsetDroppable,\n};\n","import type { Point, Rect } from \"@dashfoo/core\";\n\n// Pure drag math for the tab strip — rects in, geometry out. The drag adapter\n// gathers the rects from the DOM and feeds them here, so this stays unit-testable\n// without a live drag.\n\ntype Zone = { height: number; width: number; x: number; y: number };\n\n// Is the point inside the rect? (Rect is DOMRect-compatible: x/y/width/height.)\nconst pointInRect = (point: Point, rect: Rect): boolean =>\n point.x >= rect.x &&\n point.x <= rect.x + rect.width &&\n point.y >= rect.y &&\n point.y <= rect.y + rect.height;\n\n// Which slot in the strip the pointer is over: the first tab whose midpoint is\n// right of the pointer, else the end.\nconst insertionIndex = (rects: ReadonlyArray<Rect>, pointerX: number): number => {\n const found = rects.findIndex((rect) => pointerX < rect.x + rect.width / 2);\n return found === -1 ? rects.length : found;\n};\n\n// The thin vertical line marking where a tab will be inserted. Measured against\n// whole tab-item rects (label + close), so the \"after the last tab\" position sits\n// past the last close button rather than between the label and the close.\nconst insertionLineRect = (\n stripRect: Rect,\n itemRects: ReadonlyArray<Rect>,\n index: number,\n): Zone => {\n const at = itemRects[index];\n const last = itemRects.at(-1);\n const x = at?.x ?? (last ? last.x + last.width : stripRect.x);\n return { height: stripRect.height, width: 2, x: x - 1, y: stripRect.y };\n};\n\n// Whether a drop should commit, or is a no-op to be suppressed: dragging a tabset\n// onto itself (grip id is `grip-${tabsetId}`), or the sole tab of a tabset back\n// onto that same tabset — both change nothing once empties collapse.\nconst shouldAllowDrop = (\n draggedId: string | undefined,\n targetId: string,\n tabIds: ReadonlyArray<string>,\n): boolean => {\n if (draggedId === `grip-${targetId}`) {\n return false;\n }\n if (tabIds.length === 1 && tabIds[0] === draggedId) {\n return false;\n }\n return true;\n};\n\nexport { insertionIndex, insertionLineRect, pointInRect, shouldAllowDrop };\nexport type { Zone };\n","\"use client\";\n\nimport type { Action, DockLocation, DropIntent, Point } from \"@dashfoo/core\";\nimport { dragDockMachine, resolveDockTarget, zoneRect } from \"@dashfoo/core\";\nimport { Accessibility, DragDropManager, Feedback, KeyboardSensor } from \"@dnd-kit/dom\";\nimport type { DragEndEvent, DragMoveEvent, DragStartEvent } from \"@dnd-kit/dom\";\nimport { useActorRef, useSelector } from \"@xstate/react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from \"react\";\nimport type { ActorRefFrom } from \"xstate\";\n\nimport type { DragContextValue } from \"../hooks/drag-hooks\";\nimport {\n DragContext,\n DragSubjectContext,\n useDragSubject,\n useTabDraggable,\n useTabsetDraggable,\n useTabsetDroppable,\n} from \"../hooks/drag-hooks\";\nimport type { Zone } from \"../lib/tab-insertion\";\nimport {\n insertionIndex,\n insertionLineRect,\n pointInRect,\n shouldAllowDrop,\n} from \"../lib/tab-insertion\";\n\n// This module is the drag adapter: it (with ./drag-hooks) is where @dnd-kit is\n// touched. It wires the framework-agnostic @dnd-kit/dom core (no React bindings)\n// to the already unit-tested dragDockMachine — the PointerSensor supplies\n// activation + a live pointer, the adapter hit-tests that pointer against the\n// registered tabsets, and the machine owns the lifecycle and emits a moveNode\n// COMMIT forwarded via onCommit. The drag preview is our own overlay, so there is\n// no Feedback plugin, no placeholder clone, and none of the CSS workarounds those\n// required.\n\ntype DragActor = ActorRefFrom<typeof dragDockMachine>;\n\n// The dragged tab is excluded so its own slot never counts toward the order —\n// the insertion index and line are measured against the tabs it will land among.\nconst tabRects = (strip: Element, excludeId?: string): Array<DOMRect> =>\n [...strip.querySelectorAll<HTMLElement>('[data-dashfoo=\"tab\"]')].flatMap((tab) =>\n tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()],\n );\n\n// Whole-tab-item rects (label + close button), excluding the dragged tab. The\n// insertion line lands on these boundaries so the \"after the last tab\" position\n// sits past the close button, not between the label and the close.\nconst tabItemRects = (strip: Element, excludeId?: string): Array<DOMRect> =>\n [...strip.querySelectorAll<HTMLElement>('[data-dashfoo=\"tab-item\"]')].flatMap((item) =>\n item.querySelector<HTMLElement>('[data-dashfoo=\"tab\"]')?.dataset.tabId === excludeId\n ? []\n : [item.getBoundingClientRect()],\n );\n\n// The tab strip is always a \"stack as a tab\" target, with an insertion index that\n// places the tab at a specific slot. Only the content area below it resolves to\n// the center/split zones via resolveDockTarget (content center appends). The\n// adapter gathers rects from the DOM; the pure math lives in ./tab-insertion.\nconst intentForTabset = (\n id: string,\n element: HTMLElement,\n point: Point,\n draggedId?: string,\n): DropIntent => {\n const strip = element.querySelector('[data-dashfoo=\"tabstrip\"]');\n if (strip && pointInRect(point, strip.getBoundingClientRect())) {\n return {\n index: insertionIndex(tabRects(strip, draggedId), point.x),\n location: \"center\",\n targetId: id,\n };\n }\n const target = resolveDockTarget(point, element.getBoundingClientRect());\n const location: DockLocation = target.kind === \"tab\" ? \"center\" : `split-${target.edge}`;\n if (location === \"center\" && strip) {\n return { index: tabRects(strip, draggedId).length, location, targetId: id };\n }\n return { location, targetId: id };\n};\n\nconst overlayBase: CSSProperties = {\n boxSizing: \"border-box\",\n pointerEvents: \"none\",\n position: \"fixed\",\n // Overridable so a theme can drop it under prefers-reduced-motion.\n transition: \"var(--dashfoo-dock-transition, left 60ms, top 60ms, width 60ms, height 60ms)\",\n zIndex: 9999,\n};\n\n// Only position/size are fixed inline; every visual property is an overridable\n// CSS var with a neutral fallback, so a consumer's data-dashfoo=\"dock-indicator\"\n// theme fully owns the look.\nconst paneStyle = (zone: Zone): CSSProperties => ({\n ...overlayBase,\n background: \"var(--dashfoo-dock-fill, oklch(0.556 0 0 / 0.18))\",\n border:\n \"var(--dashfoo-dock-border-width, 1px) solid var(--dashfoo-dock-border, oklch(0.708 0 0 / 0.75))\",\n borderRadius: \"var(--dashfoo-dock-radius, 6px)\",\n height: zone.height,\n left: zone.x,\n top: zone.y,\n width: zone.width,\n});\n\nconst lineStyle = (zone: Zone): CSSProperties => ({\n ...overlayBase,\n background: \"var(--dashfoo-dock-line, oklch(0.556 0 0))\",\n borderRadius: \"var(--dashfoo-dock-line-radius, 2px)\",\n height: zone.height,\n left: zone.x,\n top: zone.y,\n width: zone.width,\n});\n\n// A \"where it will land\" indicator driven off the machine's live intent: an\n// insertion line in the tab bar for a stack, the matching content half for a split.\nconst DockIndicator = ({\n actorRef,\n getTabsetElement,\n}: {\n actorRef: DragActor;\n getTabsetElement: (id: string) => HTMLElement | undefined;\n}): ReactNode => {\n const intent = useSelector(actorRef, (snapshot) => snapshot.context.intent);\n const draggedId = useSelector(actorRef, (snapshot) => snapshot.context.subject?.id);\n if (!intent) {\n return null;\n }\n const element = getTabsetElement(intent.targetId);\n if (!element) {\n return null;\n }\n if (intent.location === \"center\") {\n const strip = element.querySelector('[data-dashfoo=\"tabstrip\"]');\n if (strip) {\n const line = insertionLineRect(\n strip.getBoundingClientRect(),\n tabItemRects(strip, draggedId),\n intent.index ?? 0,\n );\n return <div data-dashfoo=\"dock-indicator\" key=\"line\" style={lineStyle(line)} />;\n }\n }\n const zone = zoneRect(element.getBoundingClientRect(), intent.location);\n return <div data-dashfoo=\"dock-indicator\" key=\"pane\" style={paneStyle(zone)} />;\n};\n\n// The pointer-anchored chip that follows the cursor while dragging.\nconst PREVIEW_OFFSET: Point = { x: 12, y: 8 };\n\nconst previewStyle: CSSProperties = { left: 0, position: \"fixed\", top: 0, zIndex: 9999 };\n\ntype DragPreviewState = { label: string; x: number; y: number };\n\nconst DragPreview = ({\n overlayRef,\n preview,\n}: {\n overlayRef: (element: HTMLDivElement | null) => void;\n preview: DragPreviewState | null;\n}): ReactNode => {\n if (!preview) {\n return null;\n }\n return (\n <div\n data-dashfoo=\"drag-preview\"\n ref={overlayRef}\n style={{ ...previewStyle, transform: `translate(${preview.x}px, ${preview.y}px)` }}\n >\n {preview.label}\n </div>\n );\n};\n\nconst labelOf = (source: { data?: Record<string, unknown> } | null): string => {\n const raw = source?.data?.label;\n return typeof raw === \"string\" ? raw : \"\";\n};\n\ntype DragProviderProps = {\n children: ReactNode;\n onCommit: (action: Action) => void;\n splitDock?: boolean;\n};\n\nconst DragProvider = ({ children, onCommit, splitDock = true }: DragProviderProps): ReactNode => {\n const actorRef = useActorRef(dragDockMachine);\n const dragSubject = useSelector(actorRef, (snapshot) => snapshot.context.subject);\n const tabsets = useRef(new Map<string, HTMLElement>());\n const overlayRef = useRef<HTMLDivElement | null>(null);\n const [preview, setPreview] = useState<DragPreviewState | null>(null);\n\n // One manager for the whole layout. Plugins drop the screen-reader announcer\n // and the visual Feedback plugin (we render our own preview). Sensors drop the\n // KeyboardSensor: its nudge model double-binds the arrow keys the tab strip\n // already uses for roving-tabindex navigation, so keyboard docking needs its\n // own interaction design rather than this sensor. Pointer drag only.\n // useState lazily constructs it once; the destroy rides a useInsertionEffect\n // cleanup (not useEffect) so StrictMode's simulated unmount doesn't tear down\n // the live instance — the same pattern @dnd-kit/react uses internally.\n const [manager] = useState(\n () =>\n new DragDropManager({\n plugins: (defaults) =>\n defaults.filter((plugin) => plugin !== Accessibility && plugin !== Feedback),\n sensors: (defaults) => defaults.filter((sensor) => sensor !== KeyboardSensor),\n }),\n );\n useInsertionEffect(() => () => manager.destroy(), [manager]);\n\n useEffect(() => {\n const subscription = actorRef.on(\"COMMIT\", (emitted) => {\n onCommit(emitted.action);\n });\n return () => {\n subscription.unsubscribe();\n };\n }, [actorRef, onCommit]);\n\n const registerTabset = useCallback((id: string, element: HTMLElement | null): void => {\n if (element) {\n tabsets.current.set(id, element);\n } else {\n tabsets.current.delete(id);\n }\n }, []);\n\n const getTabsetElement = useCallback((id: string) => tabsets.current.get(id), []);\n\n // Which registered tabset sits under the pointer. Tabsets tile (never overlap),\n // so the first rect that contains the point is the unambiguous target — no\n // dnd-kit collision detection needed.\n const tabsetAt = useCallback((point: Point): { element: HTMLElement; id: string } | undefined => {\n for (const [id, element] of tabsets.current) {\n if (pointInRect(point, element.getBoundingClientRect())) {\n return { element, id };\n }\n }\n return undefined;\n }, []);\n\n // The dock intent for a pointer over a tabset, or null when the drop would be a\n // no-op: a tabset dragged onto itself, or the sole tab of a tabset dropped back\n // onto that same tabset. Otherwise the tabset resolves to center/split.\n const resolveIntent = useCallback(\n (\n targetId: string,\n element: HTMLElement,\n point: Point,\n draggedId?: string,\n ): DropIntent | null => {\n const tabIds = [...element.querySelectorAll<HTMLElement>('[data-dashfoo=\"tab\"]')].map(\n (tab) => tab.dataset.tabId ?? \"\",\n );\n if (!shouldAllowDrop(draggedId, targetId, tabIds)) {\n return null;\n }\n const intent = intentForTabset(targetId, element, point, draggedId);\n // When splitting is disabled, a drop over the body stacks instead of splits.\n if (!splitDock && intent.location.startsWith(\"split-\")) {\n return { location: \"center\", targetId };\n }\n return intent;\n },\n [splitDock],\n );\n\n // Position the preview imperatively (transform only) so following the pointer\n // never triggers a React re-render.\n const positionOverlay = useCallback((point: Point): void => {\n const element = overlayRef.current;\n if (element) {\n element.style.transform = `translate(${point.x + PREVIEW_OFFSET.x}px, ${point.y + PREVIEW_OFFSET.y}px)`;\n }\n }, []);\n\n const attachOverlay = useCallback((element: HTMLDivElement | null): void => {\n overlayRef.current = element;\n }, []);\n\n useEffect(() => {\n const handleStart = (event: DragStartEvent): void => {\n const source = event.operation.source;\n if (!source) {\n return;\n }\n // A tabset grip carries { tabsetId, type: \"tabset\" }; a tab carries its id.\n const isTabset = source.data?.type === \"tabset\";\n const point = event.operation.position.current;\n setPreview({\n label: labelOf(source),\n x: point.x + PREVIEW_OFFSET.x,\n y: point.y + PREVIEW_OFFSET.y,\n });\n actorRef.send({\n subject: {\n id: isTabset ? String(source.data.tabsetId) : String(source.id),\n kind: isTabset ? \"tabset\" : \"tab\",\n },\n type: \"START\",\n });\n };\n\n // dnd-kit's dragmove gives the live pointer on every move, which drives the\n // preview position and the indicator's intent.\n const handleMove = (event: DragMoveEvent): void => {\n const op = event.operation;\n const point = op.position.current;\n positionOverlay(point);\n const draggedId = op.source ? String(op.source.id) : undefined;\n const hit = tabsetAt(point);\n actorRef.send({\n intent: hit ? resolveIntent(hit.id, hit.element, point, draggedId) : null,\n type: \"OVER\",\n });\n };\n\n // Recompute the dock zone from the final pointer, then set the intent and\n // commit in one synchronous pair.\n const handleEnd = (event: DragEndEvent): void => {\n setPreview(null);\n if (event.canceled) {\n actorRef.send({ type: \"CANCEL\" });\n return;\n }\n const op = event.operation;\n const point = op.position.current;\n const draggedId = op.source ? String(op.source.id) : undefined;\n const hit = tabsetAt(point);\n if (hit) {\n actorRef.send({\n intent: resolveIntent(hit.id, hit.element, point, draggedId),\n type: \"OVER\",\n });\n }\n actorRef.send({ type: \"DROP\" });\n };\n const offStart = manager.monitor.addEventListener(\"dragstart\", handleStart);\n const offMove = manager.monitor.addEventListener(\"dragmove\", handleMove);\n const offEnd = manager.monitor.addEventListener(\"dragend\", handleEnd);\n return () => {\n offStart();\n offMove();\n offEnd();\n };\n }, [actorRef, manager, positionOverlay, resolveIntent, tabsetAt]);\n\n const contextValue = useMemo<DragContextValue>(\n () => ({ manager, registerTabset }),\n [manager, registerTabset],\n );\n\n return (\n <DragContext.Provider value={contextValue}>\n <DragSubjectContext.Provider value={dragSubject}>\n {children}\n <DockIndicator actorRef={actorRef} getTabsetElement={getTabsetElement} />\n <DragPreview overlayRef={attachOverlay} preview={preview} />\n </DragSubjectContext.Provider>\n </DragContext.Provider>\n );\n};\n\n// The draggable/droppable hooks and the drag contexts live in ./drag-hooks to\n// keep this module under the size cap and avoid a circular import; re-exported\n// here so consumers still import the whole drag surface from \"./drag-adapter\".\nexport { DragProvider, useDragSubject, useTabDraggable, useTabsetDraggable, useTabsetDroppable };\n","\"use client\";\n\nimport type { RefObject } from \"react\";\nimport { useEffect, useState } from \"react\";\n\n// The ids of the tabs clipped by the tablist's visible box — past the right edge\n// OR scrolled off before the left edge. +/-1 absorbs sub-pixel rounding. One\n// pass: keep the id only when the tab is both overflowing and identifiable.\nconst overflowingIds = (tablist: Element): Array<string> => {\n const box = tablist.getBoundingClientRect();\n return [...tablist.querySelectorAll<HTMLElement>('[data-dashfoo=\"tab\"]')].flatMap((tab) => {\n const rect = tab.getBoundingClientRect();\n const clipped = rect.right > box.right + 1 || rect.left < box.left - 1;\n return clipped && tab.dataset.tabId ? [tab.dataset.tabId] : [];\n });\n};\n\n// Recomputes the overflowing ids when the tablist resizes, scrolls, or its tab\n// labels/widths change. `signature` folds tab id+name so renames recompute even\n// when the count is unchanged. Returns [] until measured (and in jsdom, where\n// rects are 0).\nconst useTabOverflow = (\n tablistRef: RefObject<HTMLElement | null>,\n signature: string,\n): Array<string> => {\n const [overflow, setOverflow] = useState<Array<string>>([]);\n\n useEffect(() => {\n const element = tablistRef.current;\n if (!element) {\n return;\n }\n const recompute = (): void => {\n setOverflow(overflowingIds(element));\n };\n recompute();\n const observer = new ResizeObserver(recompute);\n observer.observe(element);\n // Scrolling the strip changes which tabs are clipped on the left/right without\n // resizing it, so listen for scroll too.\n element.addEventListener(\"scroll\", recompute, { passive: true });\n return () => {\n observer.disconnect();\n element.removeEventListener(\"scroll\", recompute);\n };\n }, [tablistRef, signature]);\n\n return overflow;\n};\n\nexport { overflowingIds, useTabOverflow };\n","// Stable ids wiring each tab to its panel (aria-controls / aria-labelledby).\n// Shared by tabset-view and tab-button so the wiring stays in one place with no\n// circular import between those two modules.\nconst tabDomId = (tabsetId: string, tabId: string): string => `dashfoo-tab-${tabsetId}-${tabId}`;\nconst panelDomId = (tabsetId: string): string => `dashfoo-panel-${tabsetId}`;\n\nexport { panelDomId, tabDomId };\n","// The neighbour to show as active when the selected tab is dragged out: the next\n// tab if there is one, else the previous, else -1 (the tabset is emptying).\nconst fallbackSelectedIndex = (count: number, removedIndex: number): number =>\n removedIndex + 1 < count ? removedIndex + 1 : removedIndex - 1;\n\nexport { fallbackSelectedIndex };\n","import type { CSSProperties, ReactNode } from \"react\";\n\nconst iconStyle: CSSProperties = { pointerEvents: \"none\" };\n\n// Shared lucide-style icon frame (24-unit grid, currentColor stroke). iconStyle\n// sets pointer-events:none so a pointerdown on a grip lands on the button (the\n// draggable), not the svg path — otherwise the drag sensor never activates.\nconst Icon = ({ children }: { children: ReactNode }): ReactNode => (\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height=\"12\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2\"\n style={iconStyle}\n viewBox=\"0 0 24 24\"\n width=\"12\"\n >\n {children}\n </svg>\n);\n\nconst CloseIcon = (): ReactNode => (\n <Icon>\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </Icon>\n);\n\nconst GripIcon = (): ReactNode => (\n <Icon>\n <circle cx=\"12\" cy=\"5\" r=\"1\" />\n <circle cx=\"19\" cy=\"5\" r=\"1\" />\n <circle cx=\"5\" cy=\"5\" r=\"1\" />\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n <circle cx=\"12\" cy=\"19\" r=\"1\" />\n <circle cx=\"19\" cy=\"19\" r=\"1\" />\n <circle cx=\"5\" cy=\"19\" r=\"1\" />\n </Icon>\n);\n\nconst MaximizeIcon = ({ maximized }: { maximized: boolean }): ReactNode =>\n maximized ? (\n <Icon>\n <path d=\"m14 10 7-7\" />\n <path d=\"M20 10h-6V4\" />\n <path d=\"m3 21 7-7\" />\n <path d=\"M4 14h6v6\" />\n </Icon>\n ) : (\n <Icon>\n <path d=\"M15 3h6v6\" />\n <path d=\"m21 3-7 7\" />\n <path d=\"m3 21 7-7\" />\n <path d=\"M9 21H3v-6\" />\n </Icon>\n );\n\nexport { CloseIcon, GripIcon, iconStyle, MaximizeIcon };\n","\"use client\";\n\nimport type { TabNode } from \"@dashfoo/core\";\nimport type { FocusEvent, KeyboardEvent, MouseEvent, ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { useDashfooContext } from \"../hooks/context\";\nimport { panelDomId, tabDomId } from \"../lib/tab-ids\";\n\nimport { useDragSubject, useTabDraggable } from \"./drag-adapter\";\nimport { CloseIcon } from \"./tabset-icons\";\n\n// Inline rename editor. Enter/Escape set `done` so the unmount blur does not\n// re-commit after a deliberate commit or cancel.\nconst TabRenameInput = ({\n name,\n onCancel,\n onCommit,\n}: {\n name: string;\n onCancel: () => void;\n onCommit: (value: string) => void;\n}): ReactNode => {\n const ref = useRef<HTMLInputElement>(null);\n const done = useRef(false);\n\n useEffect(() => {\n ref.current?.focus();\n ref.current?.select();\n }, []);\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"Enter\") {\n done.current = true;\n onCommit(event.currentTarget.value);\n } else if (event.key === \"Escape\") {\n done.current = true;\n onCancel();\n }\n };\n\n const handleCommitOnBlur = (event: FocusEvent<HTMLInputElement>): void => {\n if (done.current) {\n return;\n }\n done.current = true;\n onCommit(event.currentTarget.value);\n };\n\n return (\n <input\n aria-label={`Rename ${name}`}\n data-dashfoo=\"tab-rename\"\n defaultValue={name}\n onBlur={handleCommitOnBlur}\n onKeyDown={handleKeyDown}\n ref={ref}\n type=\"text\"\n />\n );\n};\n\n// A single tab — its own component so the draggable hook isn't called in a loop.\n// The close control is a sibling button (not nested in the tab button, which\n// would be invalid and would pollute the tab's accessible name).\nconst TabButton = ({\n closable,\n index,\n onClose,\n renamable,\n selected,\n tab,\n tabsetId,\n}: {\n closable: boolean;\n index: number;\n onClose: (tabId: string) => void;\n renamable: boolean;\n selected: number;\n tab: TabNode;\n tabsetId: string;\n}): ReactNode => {\n const { dispatch, renderTabLabel } = useDashfooContext();\n const { ref } = useTabDraggable(tab.id, tab.enableDrag === false, tab.name);\n // `data-dragging` is driven off the drag machine's subject, which is\n // authoritative for the whole drag — the source tab stays in the strip (dimmed)\n // while our overlay chip follows the pointer.\n const dragSubject = useDragSubject();\n const isDragging = dragSubject?.kind === \"tab\" && dragSubject.id === tab.id;\n const [editing, setEditing] = useState(false);\n const itemRef = useRef<HTMLSpanElement>(null);\n const wasEditing = useRef(false);\n\n // Return focus to the tab button once the inline editor closes.\n useEffect(() => {\n if (wasEditing.current && !editing) {\n itemRef.current?.querySelector<HTMLElement>('[data-dashfoo=\"tab\"]')?.focus();\n }\n wasEditing.current = editing;\n }, [editing]);\n\n const handleSelect = (): void => {\n dispatch({ index, tabsetId, type: \"selectTab\" });\n };\n\n const handleDoubleClick = (): void => {\n if (renamable) {\n setEditing(true);\n }\n };\n\n const handleRenameCommit = (value: string): void => {\n setEditing(false);\n const trimmed = value.trim();\n if (trimmed && trimmed !== tab.name) {\n dispatch({ name: trimmed, tabId: tab.id, type: \"renameTab\" });\n }\n };\n\n const handleRenameCancel = (): void => {\n setEditing(false);\n };\n\n // Closing is owned by TabsetView so it can restore focus to the new active tab;\n // we only relay the intent.\n const handleClose = (event: MouseEvent<HTMLButtonElement>): void => {\n event.stopPropagation();\n onClose(tab.id);\n };\n\n return (\n <span data-dashfoo=\"tab-item\" data-dragging={isDragging || undefined} ref={itemRef}>\n {editing ? (\n <TabRenameInput\n name={tab.name}\n onCancel={handleRenameCancel}\n onCommit={handleRenameCommit}\n />\n ) : (\n <button\n aria-controls={panelDomId(tabsetId)}\n aria-label={renderTabLabel ? tab.name : undefined}\n aria-selected={index === selected}\n data-dashfoo=\"tab\"\n data-tab-id={tab.id}\n id={tabDomId(tabsetId, tab.id)}\n onClick={handleSelect}\n onDoubleClick={handleDoubleClick}\n ref={ref}\n role=\"tab\"\n tabIndex={index === selected ? 0 : -1}\n type=\"button\"\n >\n {renderTabLabel ? renderTabLabel(tab) : tab.name}\n </button>\n )}\n {closable && !editing ? (\n <button\n aria-label={`Close ${tab.name}`}\n data-dashfoo=\"tab-close\"\n onClick={handleClose}\n // Roving tabindex: only the active tab's close button is tabbable, so\n // Tab does not stop on a close button inside every tab in the tablist.\n tabIndex={index === selected ? 0 : -1}\n type=\"button\"\n >\n <CloseIcon />\n </button>\n ) : null}\n </span>\n );\n};\n\nexport { TabButton };\n","\"use client\";\n\nimport type { CSSProperties, FocusEvent, KeyboardEvent, ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst OverflowIcon = (): ReactNode => (\n <svg aria-hidden=\"true\" fill=\"currentColor\" height=\"10\" viewBox=\"0 0 10 10\" width=\"10\">\n <circle cx=\"2\" cy=\"5\" r=\"1\" />\n <circle cx=\"5\" cy=\"5\" r=\"1\" />\n <circle cx=\"8\" cy=\"5\" r=\"1\" />\n </svg>\n);\n\nconst menuStyle: CSSProperties = {\n position: \"absolute\",\n right: 0,\n top: \"100%\",\n zIndex: 10,\n};\n\ntype OverflowItem = { id: string; name: string };\n\n// A dependency-free overflow control: a button that toggles a menu of the hidden\n// tabs. Implements the WAI-ARIA APG menu-button keyboard model — roving tabindex,\n// arrow/Home/End navigation, Escape-to-close with focus return. Closes on outside\n// pointerdown and focusout.\nconst TabOverflowMenu = ({\n items,\n onSelect,\n}: {\n items: Array<OverflowItem>;\n onSelect: (id: string) => void;\n}): ReactNode => {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(0);\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n useEffect(() => {\n if (!open) {\n return;\n }\n const handlePointerDown = (event: PointerEvent): void => {\n if (event.target instanceof Node && !rootRef.current?.contains(event.target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", handlePointerDown);\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown);\n };\n }, [open]);\n\n // moving DOM focus onto the first menuitem is a side effect of the menu becoming\n // visible, so it belongs in an effect rather than the click handler (the items don't\n // exist yet there). activeIndex is reset to 0 on open, so we always land on the first.\n useEffect(() => {\n if (open) {\n itemRefs.current[0]?.focus();\n }\n }, [open]);\n\n const closeAndReturnFocus = (): void => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const focusItem = (index: number): void => {\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n };\n\n const handleToggle = (): void => {\n setActiveIndex(0);\n setOpen((value) => !value);\n };\n\n const handleSelect = (id: string): void => {\n onSelect(id);\n closeAndReturnFocus();\n };\n\n const handleMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n const last = items.length - 1;\n switch (event.key) {\n case \"ArrowDown\": {\n event.preventDefault();\n focusItem(activeIndex >= last ? 0 : activeIndex + 1);\n break;\n }\n case \"ArrowUp\": {\n event.preventDefault();\n focusItem(activeIndex <= 0 ? last : activeIndex - 1);\n break;\n }\n case \"Home\": {\n event.preventDefault();\n focusItem(0);\n break;\n }\n case \"End\": {\n event.preventDefault();\n focusItem(last);\n break;\n }\n case \"Escape\": {\n event.preventDefault();\n closeAndReturnFocus();\n break;\n }\n default: {\n break;\n }\n }\n };\n\n // Tab out of the menu (or any other focus leaving the root) closes it without\n // stealing focus, complementing the outside-pointerdown handler for keyboard users.\n const handleFocusOut = (event: FocusEvent<HTMLDivElement>): void => {\n if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) {\n return;\n }\n setOpen(false);\n };\n\n return (\n <div data-dashfoo=\"tab-overflow-root\" ref={rootRef} style={{ position: \"relative\" }}>\n <button\n aria-expanded={open}\n aria-haspopup=\"menu\"\n aria-label=\"More tabs\"\n data-dashfoo=\"tab-overflow\"\n onClick={handleToggle}\n ref={triggerRef}\n type=\"button\"\n >\n <OverflowIcon />\n </button>\n {open ? (\n <div\n data-dashfoo=\"tab-overflow-menu\"\n onBlur={handleFocusOut}\n onKeyDown={handleMenuKeyDown}\n role=\"menu\"\n style={menuStyle}\n tabIndex={-1}\n >\n {items.map((item, index) => (\n <button\n data-dashfoo=\"tab-overflow-item\"\n key={item.id}\n onClick={() => handleSelect(item.id)}\n ref={(node) => {\n itemRefs.current[index] = node;\n }}\n role=\"menuitem\"\n tabIndex={index === activeIndex ? 0 : -1}\n type=\"button\"\n >\n {item.name}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n};\n\nexport { TabOverflowMenu };\nexport type { OverflowItem };\n","\"use client\";\n\nimport type { TabNode, TabsetNode } from \"@dashfoo/core\";\nimport type { CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { useEffect, useMemo, useRef } from \"react\";\n\nimport { useDashfooContext } from \"../hooks/context\";\nimport { useTabOverflow } from \"../hooks/use-tab-overflow\";\nimport { panelDomId, tabDomId } from \"../lib/tab-ids\";\nimport { fallbackSelectedIndex } from \"../lib/tab-selection\";\n\nimport { useDragSubject, useTabsetDraggable, useTabsetDroppable } from \"./drag-adapter\";\nimport { TabButton } from \"./tab-button\";\nimport { TabOverflowMenu } from \"./tab-overflow\";\nimport { GripIcon, MaximizeIcon } from \"./tabset-icons\";\n\n// height/width 100% (not flex:1) so the tabset fills its parent whether that\n// parent is a flex item or a plain block — rrp wraps panel content in a block\n// div, where flex:1 would collapse to content height.\nconst tabsetStyle: CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n height: \"100%\",\n minHeight: 0,\n minWidth: 0,\n width: \"100%\",\n};\n\n// The strip is a presentational row: the tablist (the actual tabs) plus room for\n// a trailing toolbar. role=\"tablist\" lives on the inner tablist so the toolbar is\n// not announced as a tab.\nconst stripStyle: CSSProperties = { display: \"flex\", flexShrink: 0 };\n\nconst tablistStyle: CSSProperties = { display: \"flex\", minWidth: 0, overflowX: \"auto\" };\n\nconst contentStyle: CSSProperties = { flex: 1, minHeight: 0, overflow: \"auto\" };\n\nconst toolbarStyle: CSSProperties = {\n alignItems: \"center\",\n display: \"flex\",\n flexShrink: 0,\n marginInlineStart: \"auto\",\n};\n\n// renderTab is the consumer's content factory; calling it through a component (not\n// inline) gives the panel a stable identity so React keeps its state across\n// TabsetView re-renders instead of remounting the inline result.\nconst TabPanel = ({ tab }: { tab: TabNode }): ReactNode => {\n const { renderTab } = useDashfooContext();\n return renderTab(tab);\n};\n\nconst TabsetView = ({ node }: { node: TabsetNode }): ReactNode => {\n const {\n closableTabs,\n dispatch,\n draggableTabsets,\n keepMounted,\n maximizable,\n maximizedTabsetId,\n renamableTabs,\n renderTabsetToolbar,\n tabLocation,\n tabStripEnabled,\n } = useDashfooContext();\n const { ref } = useTabsetDroppable(node.id);\n const dragSubject = useDragSubject();\n const isMaximized = maximizedTabsetId === node.id;\n const tablistRef = useRef<HTMLDivElement>(null);\n const tabsetRef = useRef<HTMLDivElement>(null);\n // Set when a close was triggered by keyboard/pointer so the post-delete effect\n // knows to move focus to the newly-selected tab (instead of leaving it on body).\n const restoreFocus = useRef(false);\n const tabsClosable = closableTabs && node.enableClose !== false;\n const showMaximize = maximizable && node.enableMaximize !== false;\n\n // While the selected tab is being dragged out of THIS tabset, show a neighbour\n // as active so the strip + content preview \"as if that tab were already gone\".\n // Purely visual — the model selection is untouched and resumes on drop/cancel.\n const draggingTabIndex =\n dragSubject?.kind === \"tab\" ? node.children.findIndex((tab) => tab.id === dragSubject.id) : -1;\n const visualSelected =\n draggingTabIndex !== -1 && draggingTabIndex === node.selected\n ? fallbackSelectedIndex(node.children.length, draggingTabIndex)\n : node.selected;\n const active = node.children[visualSelected];\n const isDragSource = dragSubject?.kind === \"tabset\" && dragSubject.id === node.id;\n\n // The grip carries the active tab's name so the drag overlay can label the chip.\n const { ref: gripRef } = useTabsetDraggable(\n node.id,\n !draggableTabsets || isMaximized,\n active?.name ?? \"\",\n );\n\n const handleMaximize = (): void => {\n dispatch({ tabsetId: isMaximized ? null : node.id, type: \"setMaximizedTabset\" });\n };\n\n // A rename or scroll changes clipping without changing the count, so the\n // signature folds each tab's id+name into the deps the hook watches.\n const overflowSignature = node.children.map((tab) => `${tab.id}:${tab.name}`).join(\"|\");\n const overflow = useTabOverflow(tablistRef, overflowSignature);\n // Resolve overflowing ids to names once per change instead of a find() per item.\n const overflowItems = useMemo(() => {\n const names = new Map(node.children.map((tab) => [tab.id, tab.name]));\n return overflow.map((id) => ({ id, name: names.get(id) ?? id }));\n }, [overflow, node.children]);\n\n const handleOverflowSelect = (id: string): void => {\n const index = node.children.findIndex((tab) => tab.id === id);\n if (index === -1) {\n return;\n }\n dispatch({ index, tabsetId: node.id, type: \"selectTab\" });\n tablistRef.current\n ?.querySelector<HTMLElement>(`#${CSS.escape(tabDomId(node.id, id))}`)\n ?.scrollIntoView({ block: \"nearest\", inline: \"nearest\" });\n };\n\n // Closing is lifted here (not in TabButton) so we can return focus to the new\n // active tab after the delete re-renders — otherwise focus falls to <body>.\n const handleTabClose = (tabId: string): void => {\n restoreFocus.current = true;\n dispatch({ tabId, type: \"deleteTab\" });\n };\n\n // Runs after the delete commits. Focus the now-active tab's button, or fall back\n // to the tabset container if the tabset emptied so focus never lands on <body>.\n useEffect(() => {\n if (!restoreFocus.current) {\n return;\n }\n restoreFocus.current = false;\n const activeTabId = node.children[node.selected]?.id;\n if (activeTabId) {\n tablistRef.current\n ?.querySelector<HTMLElement>(`#${CSS.escape(tabDomId(node.id, activeTabId))}`)\n ?.focus();\n } else {\n tabsetRef.current?.focus();\n }\n }, [node.children, node.selected, node.id]);\n\n // Roving-tabindex keyboard model (WAI-ARIA APG Tabs): arrows move and select,\n // Home/End jump to the ends, and focus follows the new selection.\n const handleTablistKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n // Only act when the key comes from a tab — otherwise a focused close button\n // (a tablist child) would have its arrows/Home/End hijacked.\n if (!(event.target instanceof HTMLElement) || !event.target.closest('[role=\"tab\"]')) {\n return;\n }\n const count = node.children.length;\n if (count === 0) {\n return;\n }\n const targets: Record<string, number> = {\n ArrowLeft: (node.selected - 1 + count) % count,\n ArrowRight: (node.selected + 1) % count,\n End: count - 1,\n Home: 0,\n };\n const next = targets[event.key];\n if (next === undefined) {\n return;\n }\n event.preventDefault();\n dispatch({ index: next, tabsetId: node.id, type: \"selectTab\" });\n const tabId = node.children[next]?.id;\n if (tabId) {\n tablistRef.current\n ?.querySelector<HTMLElement>(`#${CSS.escape(tabDomId(node.id, tabId))}`)\n ?.focus();\n }\n };\n\n const strip = tabStripEnabled ? (\n <div data-dashfoo=\"tabstrip\" style={stripStyle}>\n <div\n aria-label={node.name ?? \"Tabs\"}\n aria-orientation=\"horizontal\"\n data-dashfoo=\"tablist\"\n onKeyDown={handleTablistKeyDown}\n ref={tablistRef}\n role=\"tablist\"\n style={tablistStyle}\n tabIndex={-1}\n >\n {node.children.map((tab, index) => (\n <TabButton\n closable={tabsClosable && tab.enableClose !== false}\n index={index}\n key={tab.id}\n onClose={handleTabClose}\n renamable={renamableTabs && tab.enableRename !== false}\n selected={visualSelected}\n tab={tab}\n tabsetId={node.id}\n />\n ))}\n </div>\n {showMaximize ||\n renderTabsetToolbar ||\n overflowItems.length > 0 ||\n (draggableTabsets && !isMaximized) ? (\n <div data-dashfoo=\"tabset-toolbar\" style={toolbarStyle}>\n {overflowItems.length > 0 ? (\n <TabOverflowMenu items={overflowItems} onSelect={handleOverflowSelect} />\n ) : null}\n {draggableTabsets && !isMaximized ? (\n <button aria-label=\"Move tabset\" data-dashfoo=\"tabset-grip\" ref={gripRef} type=\"button\">\n <GripIcon />\n </button>\n ) : null}\n {renderTabsetToolbar?.(node)}\n {showMaximize ? (\n <button\n aria-label={isMaximized ? \"Restore\" : \"Maximize\"}\n aria-pressed={isMaximized}\n data-dashfoo=\"tabset-maximize\"\n onClick={handleMaximize}\n type=\"button\"\n >\n <MaximizeIcon maximized={isMaximized} />\n </button>\n ) : null}\n </div>\n ) : null}\n </div>\n ) : null;\n\n // keepMounted renders every panel (inactive ones hidden) so their state\n // survives a tab switch; otherwise only the active panel mounts.\n const renderContent = (): ReactNode => {\n if (keepMounted && node.children.length > 0) {\n return node.children.map((tab, index) => (\n <div\n aria-labelledby={tabDomId(node.id, tab.id)}\n data-dashfoo=\"tabcontent\"\n hidden={index !== visualSelected || undefined}\n id={index === visualSelected ? panelDomId(node.id) : undefined}\n key={tab.id}\n role={index === visualSelected ? \"tabpanel\" : undefined}\n style={contentStyle}\n tabIndex={index === visualSelected ? 0 : undefined}\n >\n <TabPanel tab={tab} />\n </div>\n ));\n }\n if (active) {\n return (\n <div\n aria-labelledby={tabDomId(node.id, active.id)}\n data-dashfoo=\"tabcontent\"\n id={panelDomId(node.id)}\n role=\"tabpanel\"\n style={contentStyle}\n tabIndex={0}\n >\n <TabPanel tab={active} />\n </div>\n );\n }\n return <div data-dashfoo=\"tabcontent\" style={contentStyle} />;\n };\n\n const content = renderContent();\n\n return (\n <div\n data-dashfoo=\"tabset\"\n data-dragging-source={isDragSource || undefined}\n data-tab-location={tabLocation}\n ref={(element) => {\n ref(element);\n tabsetRef.current = element;\n }}\n style={tabsetStyle}\n // -1 so an emptied tabset can receive focus on close without entering the\n // Tab order.\n tabIndex={-1}\n >\n {tabLocation === \"bottom\" ? (\n <>\n {content}\n {strip}\n </>\n ) : (\n <>\n {strip}\n {content}\n </>\n )}\n </div>\n );\n};\n\nexport { TabsetView };\n","\"use client\";\n\nimport type { Dimension, RowNode } from \"@dashfoo/core\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { Fragment, useLayoutEffect, useMemo, useRef } from \"react\";\nimport type { GroupImperativeHandle, Layout, Orientation } from \"react-resizable-panels\";\nimport { Group, Panel, Separator } from \"react-resizable-panels\";\n\nimport { useDashfooContext } from \"../hooks/context\";\n\nimport { TabsetView } from \"./tabset-view\";\n\n// This module is the resize adapter: the only place that imports\n// react-resizable-panels. It maps the model's responsive weights to rrp's\n// percentage layout and unit-typed min/max constraints to rrp sizes, and commits\n// a drag (on release) back to the document as an adjustSplit action.\n\nconst dimensionToSize = (dimension: Dimension): string => `${dimension.value}${dimension.unit}`;\nconst dimensionToPixels = (dimension: Dimension | undefined): number | undefined =>\n dimension?.unit === \"px\" ? dimension.value : undefined;\n\nconst groupStyle: CSSProperties = { display: \"flex\", flex: 1, minHeight: 0, minWidth: 0 };\nconst WEIGHT_EPSILON = 0.01;\n\ntype LayoutChild = RowNode[\"children\"][number];\n\nconst layoutFromWeights = (children: RowNode[\"children\"], total: number): Layout =>\n children.reduce<Layout>((layout, child) => {\n layout[child.id] = ((child.weight ?? 1) / total) * 100;\n return layout;\n }, {});\n\nconst layoutsMatch = (current: Layout, expected: Layout): boolean => {\n const currentIds = Object.keys(current);\n const expectedIds = Object.keys(expected);\n if (currentIds.length !== expectedIds.length) {\n return false;\n }\n return expectedIds.every(\n (id) => Math.abs((current[id] ?? Number.NaN) - expected[id]) <= WEIGHT_EPSILON,\n );\n};\n\nconst descendantMinSize = (\n child: LayoutChild,\n axis: RowNode[\"orientation\"],\n tabsetMinSize: number,\n): number | undefined => {\n const explicit = dimensionToPixels(child.min);\n if (explicit !== undefined) {\n return explicit;\n }\n if (child.min !== undefined) {\n return undefined;\n }\n if (child.type === \"tabset\") {\n return tabsetMinSize;\n }\n\n const childMinimums: Array<number> = [];\n for (const grandchild of child.children) {\n const minimum = descendantMinSize(grandchild, axis, tabsetMinSize);\n if (minimum === undefined) {\n return undefined;\n }\n childMinimums.push(minimum);\n }\n if (childMinimums.length === 0) {\n return undefined;\n }\n\n return child.orientation === axis\n ? childMinimums.reduce((sum, value) => sum + value, 0)\n : Math.max(...childMinimums);\n};\n\nconst RowView = ({ node }: { node: RowNode }): ReactNode => {\n const { dispatch, tabsetMinSize } = useDashfooContext();\n const orientation: Orientation = node.orientation === \"row\" ? \"horizontal\" : \"vertical\";\n const total = node.children.reduce((sum, child) => sum + (child.weight ?? 1), 0);\n const desiredLayout = useMemo(\n () => layoutFromWeights(node.children, total),\n [node.children, total],\n );\n\n // rrp fires onLayoutChanged once on mount with its measured layout. That is not\n // a user resize — committing it would rewrite the authored weights and push a\n // spurious undo entry — so the first call (per mount) is ignored.\n const measured = useRef(false);\n const groupRef = useRef<GroupImperativeHandle | null>(null);\n const syncing = useRef(false);\n\n useLayoutEffect(() => {\n const group = groupRef.current;\n if (!group) {\n return;\n }\n if (layoutsMatch(group.getLayout(), desiredLayout)) {\n return;\n }\n\n syncing.current = true;\n group.setLayout(desiredLayout);\n const frame = requestAnimationFrame(() => {\n syncing.current = false;\n });\n return () => {\n cancelAnimationFrame(frame);\n syncing.current = false;\n };\n }, [desiredLayout]);\n\n const handleLayoutChanged = (layout: Layout): void => {\n if (!measured.current) {\n measured.current = true;\n return;\n }\n if (syncing.current) {\n return;\n }\n const weights = node.children.map((child) => layout[child.id] ?? child.weight ?? 1);\n const weightsChanged = weights.some(\n (weight, index) => Math.abs(weight - (node.children[index]?.weight ?? 1)) > WEIGHT_EPSILON,\n );\n\n if (!weightsChanged) {\n return;\n }\n\n dispatch({\n rowId: node.id,\n type: \"adjustSplit\",\n weights,\n });\n };\n\n return (\n <Group\n data-dashfoo=\"row\"\n groupRef={groupRef}\n key={node.children.map((child) => child.id).join(\"|\")}\n onLayoutChanged={handleLayoutChanged}\n orientation={orientation}\n style={groupStyle}\n >\n {node.children.map((child, index) => {\n const percent = ((child.weight ?? 1) / total) * 100;\n let min = child.min ? dimensionToSize(child.min) : undefined;\n if (min === undefined) {\n const minimum = descendantMinSize(child, node.orientation, tabsetMinSize);\n min = minimum === undefined ? undefined : `${minimum}px`;\n }\n const max = child.max ? dimensionToSize(child.max) : undefined;\n\n return (\n <Fragment key={child.id}>\n {index > 0 ? <Separator data-dashfoo=\"splitter\" /> : null}\n <Panel defaultSize={`${percent}%`} id={child.id} maxSize={max} minSize={min}>\n {child.type === \"row\" ? <RowView node={child} /> : <TabsetView node={child} />}\n </Panel>\n </Fragment>\n );\n })}\n </Group>\n );\n};\n\nexport { RowView };\n","\"use client\";\n\nimport type { Action, Dashfoo, DockLocation, TabNode, TabsetNode } from \"@dashfoo/core\";\nimport { findTabset } from \"@dashfoo/core\";\nimport type { ComponentType, CSSProperties, ReactNode } from \"react\";\nimport { forwardRef, useCallback, useImperativeHandle, useMemo } from \"react\";\n\nimport { DashfooContext } from \"../hooks/context\";\nimport type { PersistConfig, StorageAdapter } from \"../hooks/persistence\";\nimport { localStorageAdapter, usePersistence } from \"../hooks/persistence\";\nimport { useDashfooStore } from \"../hooks/store\";\n\nimport { DragProvider } from \"./drag-adapter\";\nimport { RowView } from \"./row-view\";\nimport { TabsetView } from \"./tabset-view\";\n\nconst DEFAULT_PERSIST_DEBOUNCE_MS = 300;\nconst DEFAULT_TABSET_MIN_SIZE = 320;\n\n// `persist` accepts a bare localStorage key or a full target (custom storage,\n// debounce). Controlled mode (a `model` prop, no `defaultModel`) skips it —\n// persistence implies the library owns the state.\ntype PersistInput = string | { debounceMs?: number; key: string; storage?: StorageAdapter };\n\nconst resolvePersist = (\n persist: PersistInput | undefined,\n hasDefaultModel: boolean,\n): PersistConfig | null => {\n if (persist === undefined || !hasDefaultModel) {\n return null;\n }\n if (typeof persist === \"string\") {\n return { debounceMs: DEFAULT_PERSIST_DEBOUNCE_MS, key: persist, storage: localStorageAdapter };\n }\n return {\n debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,\n key: persist.key,\n storage: persist.storage ?? localStorageAdapter,\n };\n};\n\ntype TabComponent = ComponentType<{ node: TabNode }>;\n\n// Component names already warned about, so the dev warning fires at most once each.\nconst warnedComponents = new Set<string>();\n\n// Imperative API exposed via ref — lets a host drive the layout from a toolbar,\n// command palette, or keyboard shortcuts without rebuilding the store's plumbing.\ntype DashfooHandle = {\n addTab: (\n tab: TabNode,\n target: { index?: number; location?: DockLocation; targetId: string },\n ) => void;\n canRedo: () => boolean;\n canUndo: () => boolean;\n closeTab: (tabId: string) => void;\n dispatch: (action: Action) => void;\n getModel: () => Dashfoo;\n maximizeTabset: (tabsetId: string | null) => void;\n redo: () => void;\n renameTab: (tabId: string, name: string) => void;\n // Reset to the original defaultModel, clearing undo history and any persisted\n // copy. No-op in controlled mode (the host owns the model).\n resetLayout: () => void;\n selectTab: (tabsetId: string, index: number) => void;\n undo: () => void;\n};\n\ntype DashfooLayoutProps = {\n closableTabs?: boolean;\n components?: Record<string, TabComponent>;\n defaultModel?: Dashfoo;\n draggableTabsets?: boolean;\n factory?: (tab: TabNode) => ReactNode;\n keepMounted?: boolean;\n maximizable?: boolean;\n model?: Dashfoo;\n onAction?: (action: Action) => Action | null;\n onActiveTabsetChange?: (tabsetId: string | undefined) => void;\n onMaximizedTabsetChange?: (tabsetId: string | undefined) => void;\n onModelChange?: (model: Dashfoo, action?: Action) => void;\n persist?: PersistInput;\n renamableTabs?: boolean;\n renderTabLabel?: (tab: TabNode) => ReactNode;\n renderTabsetToolbar?: (tabset: TabsetNode) => ReactNode;\n};\n\nconst rootStyle = { display: \"flex\", height: \"100%\", width: \"100%\" } as const;\n\n// The top-level component. Owns the store (controlled or uncontrolled), resolves\n// tab content via a components registry or a factory, and renders the layout tree.\n// forwardRef (not the React 19 ref-as-prop) keeps the React 18 peer working.\nconst DashfooLayout = forwardRef<DashfooHandle, DashfooLayoutProps>((props, ref): ReactNode => {\n const {\n closableTabs = true,\n components,\n defaultModel,\n draggableTabsets = true,\n factory,\n keepMounted = false,\n maximizable = true,\n model,\n onAction,\n onActiveTabsetChange,\n onMaximizedTabsetChange,\n onModelChange,\n persist,\n renamableTabs = true,\n renderTabLabel,\n renderTabsetToolbar,\n } = props;\n\n const persistConfig = useMemo(\n () => resolvePersist(persist, defaultModel !== undefined),\n [persist, defaultModel],\n );\n const persistence = usePersistence(persistConfig, defaultModel);\n\n // The save side of persistence rides onModelChange; the consumer's own handler\n // still fires after it.\n const handleModelChange = useCallback(\n (next: Dashfoo, action?: Action): void => {\n persistence.save(next);\n onModelChange?.(next, action);\n },\n [onModelChange, persistence],\n );\n\n const store = useDashfooStore({\n defaultModel: persistence.initialModel ?? defaultModel,\n model,\n onAction,\n onActiveTabsetChange,\n onMaximizedTabsetChange,\n onModelChange: handleModelChange,\n });\n\n useImperativeHandle(\n ref,\n () => ({\n addTab: (tab, target) =>\n store.dispatch({\n index: target.index,\n location: target.location ?? \"center\",\n tab,\n targetId: target.targetId,\n type: \"addNode\",\n }),\n canRedo: store.canRedo,\n canUndo: store.canUndo,\n closeTab: (tabId) => store.dispatch({ tabId, type: \"deleteTab\" }),\n dispatch: store.dispatch,\n getModel: () => store.model,\n maximizeTabset: (tabsetId) => store.dispatch({ tabsetId, type: \"setMaximizedTabset\" }),\n redo: store.redo,\n renameTab: (tabId, name) => store.dispatch({ name, tabId, type: \"renameTab\" }),\n resetLayout: () => {\n persistence.clear();\n if (defaultModel !== undefined) {\n store.setModel(defaultModel);\n }\n },\n selectTab: (tabsetId, index) => store.dispatch({ index, tabsetId, type: \"selectTab\" }),\n undo: store.undo,\n }),\n [defaultModel, persistence, store],\n );\n\n const renderTab = useCallback(\n (tab: TabNode): ReactNode => {\n if (factory) {\n return factory(tab);\n }\n const Component = components?.[tab.component];\n if (!Component) {\n if (components && !warnedComponents.has(tab.component)) {\n warnedComponents.add(tab.component);\n // A tab whose component is not registered renders nothing — surface it\n // once so the misconfiguration is not silent.\n // oxlint-disable-next-line no-console\n console.warn(`[dashfoo] no component registered for \"${tab.component}\"`);\n }\n return null;\n }\n return <Component node={tab} />;\n },\n [components, factory],\n );\n\n // Model-level globals act as a default layer under the component props: a\n // feature is on unless the prop, the global, or the per-node flag turns it off.\n const global = store.model.global;\n const effectiveClosable = closableTabs && global.tabEnableClose !== false;\n const effectiveRenamable = renamableTabs && global.tabEnableRename !== false;\n const effectiveMaximizable = maximizable && global.tabSetEnableMaximize !== false;\n const tabLocation = global.tabLocation ?? \"top\";\n const tabsetMinSize = global.tabSetMinSize ?? DEFAULT_TABSET_MIN_SIZE;\n const tabStripEnabled = global.tabSetEnableTabStrip !== false;\n\n const contextValue = useMemo(\n () => ({\n closableTabs: effectiveClosable,\n dispatch: store.dispatch,\n draggableTabsets,\n keepMounted,\n maximizable: effectiveMaximizable,\n maximizedTabsetId: store.model.maximizedTabsetId,\n renamableTabs: effectiveRenamable,\n renderTab,\n renderTabLabel,\n renderTabsetToolbar,\n tabLocation,\n tabsetMinSize,\n tabStripEnabled,\n }),\n [\n draggableTabsets,\n effectiveClosable,\n effectiveMaximizable,\n effectiveRenamable,\n keepMounted,\n renderTab,\n renderTabLabel,\n renderTabsetToolbar,\n store.dispatch,\n store.model.maximizedTabsetId,\n tabLocation,\n tabsetMinSize,\n tabStripEnabled,\n ],\n );\n\n const handleCommit = store.dispatch;\n const splitDock = global.enableSplitDock !== false;\n\n const layoutStyle: CSSProperties =\n global.splitterSize === undefined\n ? rootStyle\n : ({ ...rootStyle, \"--dashfoo-splitter-size\": `${global.splitterSize}px` } as CSSProperties);\n\n // A maximized tabset fills the frame on its own; otherwise the row tree renders.\n const maximized = store.model.maximizedTabsetId\n ? findTabset(store.model, store.model.maximizedTabsetId)\n : undefined;\n\n return (\n <DashfooContext.Provider value={contextValue}>\n <DragProvider onCommit={handleCommit} splitDock={splitDock}>\n <div data-dashfoo=\"layout\" style={layoutStyle}>\n {maximized ? <TabsetView node={maximized} /> : <RowView node={store.model.layout} />}\n </div>\n </DragProvider>\n </DashfooContext.Provider>\n );\n});\n\nDashfooLayout.displayName = \"DashfooLayout\";\n\nexport { DashfooLayout };\nexport type { DashfooHandle, DashfooLayoutProps, TabComponent };\n","\"use client\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\n\ntype PanelRootProps = ComponentProps<\"div\">;\ntype PanelHeaderProps = ComponentProps<\"div\">;\ntype PanelBodyProps = ComponentProps<\"div\">;\ntype PanelTitleProps = ComponentProps<\"span\">;\ntype PanelIconProps = ComponentProps<\"span\">;\ntype PanelBadgeProps = ComponentProps<\"span\">;\n\nconst PanelRoot = (props: PanelRootProps): ReactNode => <div {...props} data-dashfoo=\"panel\" />;\nconst PanelHeader = (props: PanelHeaderProps): ReactNode => (\n <div {...props} data-dashfoo=\"panel-header\" />\n);\nconst PanelBody = (props: PanelBodyProps): ReactNode => (\n <div {...props} data-dashfoo=\"panel-body\" />\n);\nconst PanelTitle = (props: PanelTitleProps): ReactNode => (\n <span {...props} data-dashfoo=\"panel-title\" />\n);\nconst PanelIcon = (props: PanelIconProps): ReactNode => (\n <span {...props} data-dashfoo=\"panel-icon\" />\n);\nconst PanelBadge = (props: PanelBadgeProps): ReactNode => (\n <span {...props} data-dashfoo=\"panel-badge\" />\n);\n\nconst Panel = {\n Badge: PanelBadge,\n Body: PanelBody,\n Header: PanelHeader,\n Icon: PanelIcon,\n Root: PanelRoot,\n Title: PanelTitle,\n} as const;\n\nexport { Panel };\nexport type {\n PanelBadgeProps,\n PanelBodyProps,\n PanelHeaderProps,\n PanelIconProps,\n PanelRootProps,\n PanelTitleProps,\n};\n","\"use client\";\n\nimport type { Dashfoo } from \"@dashfoo/core\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\ntype Breakpoint = {\n id: string;\n model: Dashfoo;\n query?: { maxWidth: number } | { media: string };\n};\n\ntype UseResponsiveModelOptions = { breakpoints: Array<Breakpoint> };\n\ntype ResponsiveModel = {\n breakpoint: string;\n containerRef: (element: HTMLElement | null) => void;\n defaultModel: Dashfoo;\n key: string;\n};\n\nconst hasMatchMedia = (): boolean =>\n typeof window !== \"undefined\" && typeof window.matchMedia === \"function\";\n\n// Whether a breakpoint applies at a given container width. No query = catch-all;\n// a maxWidth matches at or below the width; a media query consults matchMedia.\nconst matchBreakpoint = (breakpoint: Breakpoint, width: number): boolean => {\n const { query } = breakpoint;\n if (!query) {\n return true;\n }\n if (\"maxWidth\" in query) {\n return width <= query.maxWidth;\n }\n return hasMatchMedia() && window.matchMedia(query.media).matches;\n};\n\n// The first breakpoint that matches, else the last (the expected catch-all).\nconst activeBreakpoint = (breakpoints: Array<Breakpoint>, width: number): Breakpoint => {\n const fallback = breakpoints.at(-1);\n if (!fallback) {\n throw new Error(\"useResponsiveModel requires at least one breakpoint.\");\n }\n return breakpoints.find((breakpoint) => matchBreakpoint(breakpoint, width)) ?? fallback;\n};\n\n// Picks a model for the active breakpoint, keyed off the container's own width\n// (ResizeObserver) and/or media queries. Spread the result onto DashfooLayout\n// and remount on `key`.\nconst useResponsiveModel = ({ breakpoints }: UseResponsiveModelOptions): ResponsiveModel => {\n // seed the catch-all width so server and client agree on the first render\n // (no hydration mismatch); the ResizeObserver supplies the real container\n // width on mount. innerWidth is the viewport, not our container, so it would\n // pick the wrong breakpoint and thrash the DashfooLayout key={active.id} remount.\n const [width, setWidth] = useState<number>(Number.POSITIVE_INFINITY);\n const [, setMediaTick] = useState(0);\n const observerRef = useRef<ResizeObserver | null>(null);\n\n const containerRef = useCallback((element: HTMLElement | null): void => {\n observerRef.current?.disconnect();\n if (!element) {\n return;\n }\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (entry) {\n setWidth(entry.contentRect.width);\n }\n });\n observer.observe(element);\n observerRef.current = observer;\n }, []);\n\n useEffect(\n () => () => {\n observerRef.current?.disconnect();\n },\n [],\n );\n\n useEffect(() => {\n if (!hasMatchMedia()) {\n return;\n }\n const lists = breakpoints.flatMap((breakpoint) =>\n breakpoint.query && \"media\" in breakpoint.query\n ? [window.matchMedia(breakpoint.query.media)]\n : [],\n );\n if (lists.length === 0) {\n return;\n }\n const handleChange = (): void => {\n setMediaTick((tick) => tick + 1);\n };\n for (const list of lists) {\n list.addEventListener(\"change\", handleChange);\n }\n return () => {\n for (const list of lists) {\n list.removeEventListener(\"change\", handleChange);\n }\n };\n }, [breakpoints]);\n\n const active = activeBreakpoint(breakpoints, width);\n return { breakpoint: active.id, containerRef, defaultModel: active.model, key: active.id };\n};\n\nexport { activeBreakpoint, matchBreakpoint, useResponsiveModel };\nexport type { Breakpoint, ResponsiveModel, UseResponsiveModelOptions };\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@dashfoo/react",
3
+ "version": "0.1.0",
4
+ "description": "Headless React layer for dashfoo: DashfooLayout, hooks, slots, and the react-resizable-panels + dnd-kit adapters.",
5
+ "license": "MIT",
6
+ "author": "Pedro Filho <pedro@filho.me>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/pedroapfilho/dashfoo.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@dnd-kit/dom": "0.4.0",
29
+ "@xstate/react": "^5.0.0",
30
+ "react-resizable-panels": "^4.0.0",
31
+ "xstate": "^5.0.0",
32
+ "@dashfoo/core": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@testing-library/jest-dom": "^6.9.1",
36
+ "@testing-library/react": "^16.3.2",
37
+ "@types/react": "^19.2.15",
38
+ "@types/react-dom": "^19.2.3",
39
+ "jsdom": "^29.1.1",
40
+ "react": "^19.2.6",
41
+ "react-dom": "^19.2.6",
42
+ "typescript": "^6.0.3",
43
+ "vitest": "^4.1.8",
44
+ "@repo/config-vitest": "0.0.0",
45
+ "@repo/typescript-config": "0.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "react": "^18.3.1 || ^19.0.0",
49
+ "react-dom": "^18.3.1 || ^19.0.0"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown",
53
+ "clean": "rm -rf dist",
54
+ "dev": "tsdown --watch",
55
+ "lint": "oxlint src",
56
+ "test": "vitest run --passWithNoTests",
57
+ "test:coverage": "vitest run --coverage --passWithNoTests",
58
+ "typecheck": "tsc -p tsconfig.json --noEmit"
59
+ }
60
+ }