@labpane/os 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1968 @@
1
+ // workspace.tsx
2
+ import {
3
+ useEffect as useEffect2,
4
+ useId as useId2,
5
+ useLayoutEffect as useLayoutEffect4,
6
+ useRef as useRef5,
7
+ useState as useState4
8
+ } from "react";
9
+
10
+ // context.ts
11
+ import {
12
+ createContext,
13
+ useContext,
14
+ useMemo
15
+ } from "react";
16
+ var WorkspaceId = createContext("");
17
+ var densityPresets = {
18
+ comfortable: {
19
+ value: 100,
20
+ width: 192,
21
+ height: 44,
22
+ contentHeight: 36,
23
+ columnGap: 8,
24
+ rowGap: 8,
25
+ logoSize: 24,
26
+ borderWidth: 1
27
+ },
28
+ compact: {
29
+ value: 50,
30
+ width: 168,
31
+ height: 34,
32
+ contentHeight: 30,
33
+ columnGap: 6,
34
+ rowGap: 4,
35
+ logoSize: 20,
36
+ borderWidth: 0.75
37
+ },
38
+ dense: {
39
+ value: 0,
40
+ width: 152,
41
+ height: 26,
42
+ contentHeight: 24,
43
+ columnGap: 4,
44
+ rowGap: 2,
45
+ logoSize: 16,
46
+ borderWidth: 0.5
47
+ }
48
+ };
49
+ function densitySize(density) {
50
+ const value = Number.isFinite(density) ? Math.max(0, Math.min(100, density)) : 100;
51
+ const { dense, compact, comfortable } = densityPresets;
52
+ const [from, to] = value < compact.value ? [dense, compact] : [compact, comfortable];
53
+ const amount = (value - from.value) / (to.value - from.value);
54
+ return Object.fromEntries(
55
+ Object.keys(from).map((key) => [
56
+ key,
57
+ from[key] + (to[key] - from[key]) * amount
58
+ ])
59
+ );
60
+ }
61
+ var WorkspaceDensity = createContext(100);
62
+ var fitTabWidth = (width, available, count) => Math.min(width, Math.max(128, (available - 16) / Math.max(1, count)));
63
+ function useTabSize() {
64
+ const density = useContext(WorkspaceDensity);
65
+ return useMemo(() => densitySize(density), [density]);
66
+ }
67
+ function useWorkspaceIds() {
68
+ const prefix = useContext(WorkspaceId);
69
+ return {
70
+ tab: (key) => `${prefix}-tab-${encodeURIComponent(key)}`,
71
+ panel: (key) => `${prefix}-panel-${encodeURIComponent(key)}`
72
+ };
73
+ }
74
+
75
+ // surface.tsx
76
+ import {
77
+ Fragment,
78
+ useLayoutEffect,
79
+ useRef,
80
+ useState
81
+ } from "react";
82
+
83
+ // layout.ts
84
+ var gutter = { x: 8, y: 8 };
85
+ var minimum = { x: 280, y: 180 };
86
+ var isSplit = (node) => "axis" in node;
87
+ var panes = (node) => isSplit(node) ? [...panes(node.first), ...panes(node.second)] : [node];
88
+ var clamp = (n, low, high) => Math.max(low, Math.min(high, n));
89
+ function updateLayout(node, id, change) {
90
+ if (node.id === id) return change(node);
91
+ if (!isSplit(node)) return node;
92
+ const first = updateLayout(node.first, id, change), second = updateLayout(node.second, id, change);
93
+ return first === node.first && second === node.second ? node : { ...node, first, second };
94
+ }
95
+ function reconcileLayout(node, keys, active, preferred) {
96
+ if (!keys.length)
97
+ return !isSplit(node) && !node.tabs.length && !node.active ? node : { id: "primary", tabs: [] };
98
+ const seen = /* @__PURE__ */ new Set();
99
+ const clean = (node2) => {
100
+ if (isSplit(node2)) {
101
+ const first = clean(node2.first), second = clean(node2.second);
102
+ return first && second ? first === node2.first && second === node2.second ? node2 : { ...node2, first, second } : first ?? second;
103
+ }
104
+ const tabs = node2.tabs.filter(
105
+ (key) => keys.includes(key) && !seen.has(key) && !!seen.add(key)
106
+ );
107
+ if (!tabs.length) return void 0;
108
+ const selected = tabs.includes(active ?? "") ? active : tabs.includes(node2.active ?? "") ? node2.active : tabs[0];
109
+ return tabs.length === node2.tabs.length && selected === node2.active ? node2 : { ...node2, tabs, active: selected };
110
+ };
111
+ let next = clean(node) ?? { id: "primary", tabs: [] };
112
+ const missing = keys.filter((key) => !seen.has(key));
113
+ if (missing.length) {
114
+ const leaves = panes(next), target = leaves.find((p) => p.id === preferred) ?? leaves[0];
115
+ next = updateLayout(next, target.id, (node2) => ({
116
+ ...node2,
117
+ tabs: [...target.tabs, ...missing],
118
+ active: missing.includes(active ?? "") ? active : target.active ?? missing[0]
119
+ }));
120
+ }
121
+ return next;
122
+ }
123
+ function restoreLayout(value, keys) {
124
+ const ids = /* @__PURE__ */ new Set();
125
+ function read(value2, depth = 0) {
126
+ const n = value2;
127
+ if (!n || depth > 12 || typeof n.id !== "string" || !n.id || ids.has(n.id))
128
+ throw Error("Invalid layout");
129
+ ids.add(n.id);
130
+ if (n.axis === "x" || n.axis === "y") {
131
+ if (typeof n.ratio !== "number" || !Number.isFinite(n.ratio))
132
+ throw Error("Invalid ratio");
133
+ return {
134
+ id: n.id,
135
+ axis: n.axis,
136
+ ratio: clamp(n.ratio, 0.05, 0.95),
137
+ first: read(n.first, depth + 1),
138
+ second: read(n.second, depth + 1)
139
+ };
140
+ }
141
+ if (!Array.isArray(n.tabs) || n.tabs.some((key) => typeof key !== "string"))
142
+ throw Error("Invalid tabs");
143
+ return {
144
+ id: n.id,
145
+ tabs: n.tabs,
146
+ active: typeof n.active === "string" ? n.active : void 0
147
+ };
148
+ }
149
+ try {
150
+ return reconcileLayout(read(JSON.parse(value ?? "null")), keys);
151
+ } catch {
152
+ return { id: "primary", tabs: keys, active: keys[0] };
153
+ }
154
+ }
155
+ var newId = () => crypto.randomUUID?.() ?? Array.from(
156
+ crypto.getRandomValues(new Uint8Array(16)),
157
+ (byte) => byte.toString(16).padStart(2, "0")
158
+ ).join("");
159
+ function dockTab(layout, key, target, edge, index) {
160
+ const leaves = panes(layout), source = leaves.find((p) => p.tabs.includes(key)), destination = leaves.find((p) => p.id === target);
161
+ if (!source || !destination || source === destination && (!edge || source.tabs.length === 1))
162
+ return layout;
163
+ let next = updateLayout(layout, source.id, (node) => {
164
+ const pane = node, index2 = pane.tabs.indexOf(key), tabs = pane.tabs.filter((k) => k !== key);
165
+ return {
166
+ ...pane,
167
+ tabs,
168
+ active: pane.active === key ? tabs[Math.min(index2, tabs.length - 1)] : pane.active
169
+ };
170
+ });
171
+ next = updateLayout(next, target, (node) => {
172
+ const pane = node;
173
+ if (!edge) {
174
+ const tabs = [...pane.tabs];
175
+ tabs.splice(index ?? tabs.length, 0, key);
176
+ return { ...pane, tabs, active: key };
177
+ }
178
+ const added = { id: newId(), tabs: [key], active: key };
179
+ return {
180
+ id: newId(),
181
+ axis: edge === "bottom" ? "y" : "x",
182
+ ratio: edge === "bottom" ? 2 / 3 : 0.5,
183
+ first: edge === "left" ? added : pane,
184
+ second: edge === "left" ? pane : added
185
+ };
186
+ });
187
+ return reconcileLayout(
188
+ next,
189
+ leaves.flatMap((p) => p.tabs),
190
+ key
191
+ );
192
+ }
193
+ function minimumSize(node, gap = gutter) {
194
+ if (!isSplit(node)) return minimum;
195
+ const a = minimumSize(node.first, gap), b = minimumSize(node.second, gap);
196
+ return node.axis === "x" ? { x: a.x + b.x + gap.x, y: Math.max(a.y, b.y) } : { x: Math.max(a.x, b.x), y: a.y + b.y + gap.y };
197
+ }
198
+ function layoutBoxes(layout, width, height, gap = gutter) {
199
+ const leaves = [], dividers = [];
200
+ function walk(node, box) {
201
+ if (!isSplit(node)) {
202
+ leaves.push({ ...box, pane: node });
203
+ return;
204
+ }
205
+ const axis = node.axis, dimension = axis === "x" ? "width" : "height", spacing = gap[axis], size = Math.max(1, box[dimension] - spacing);
206
+ const low = minimumSize(node.first, gap)[axis] / size, high = 1 - minimumSize(node.second, gap)[axis] / size;
207
+ const ratio = clamp(node.ratio, low, high), distance = clamp(Math.round(size * ratio), size * low, size * high);
208
+ const first = { ...box, [dimension]: distance };
209
+ const second = {
210
+ ...box,
211
+ [axis]: box[axis] + distance + spacing,
212
+ [dimension]: size - distance
213
+ };
214
+ dividers.push({
215
+ ...box,
216
+ [axis]: box[axis] + distance,
217
+ [dimension]: spacing,
218
+ split: node,
219
+ bounds: box,
220
+ ratio,
221
+ low,
222
+ high
223
+ });
224
+ walk(node.first, first);
225
+ walk(node.second, second);
226
+ }
227
+ const required = minimumSize(layout, gap);
228
+ walk(layout, {
229
+ x: 0,
230
+ y: 0,
231
+ width: Math.max(width, required.x),
232
+ height: Math.max(height, required.y)
233
+ });
234
+ return { leaves, dividers };
235
+ }
236
+ function dropRegion(box, edge, gap = gutter) {
237
+ if (edge === "left") return { ...box, width: (box.width - gap.x) / 2 };
238
+ if (edge === "right")
239
+ return {
240
+ ...box,
241
+ x: box.x + (box.width + gap.x) / 2,
242
+ width: (box.width - gap.x) / 2
243
+ };
244
+ if (edge === "bottom") {
245
+ const height = Math.max(minimum.y, (box.height - gap.y) / 3);
246
+ return { ...box, y: box.y + box.height - height, height };
247
+ }
248
+ return box;
249
+ }
250
+
251
+ // surface.tsx
252
+ import { jsx, jsxs } from "react/jsx-runtime";
253
+ var position = (box) => ({
254
+ left: box.x,
255
+ top: box.y,
256
+ width: box.width,
257
+ height: box.height
258
+ });
259
+ function WorkspaceSurface({
260
+ layout,
261
+ change,
262
+ header,
263
+ content,
264
+ menu
265
+ }) {
266
+ const ids = useWorkspaceIds();
267
+ const {
268
+ height: tabHeight,
269
+ width: tabWidth,
270
+ columnGap,
271
+ rowGap
272
+ } = useTabSize();
273
+ const gutter2 = { x: columnGap, y: rowGap };
274
+ const board = useRef(null);
275
+ const [size, setSize] = useState({ width: 0, height: 0 });
276
+ const [preview, setPreview] = useState();
277
+ const previewRef = useRef(void 0);
278
+ const [resizing, setResizing] = useState(false);
279
+ const geometry = [
280
+ size.width,
281
+ size.height,
282
+ tabHeight,
283
+ columnGap,
284
+ rowGap
285
+ ].join();
286
+ const previousGeometry = useRef(geometry);
287
+ const stableGeometry = previousGeometry.current === geometry;
288
+ useLayoutEffect(() => {
289
+ previousGeometry.current = geometry;
290
+ }, [geometry]);
291
+ useLayoutEffect(() => {
292
+ const observer = new ResizeObserver(([entry]) => {
293
+ const { width, height } = entry.contentRect;
294
+ setSize({ width, height });
295
+ });
296
+ observer.observe(board.current);
297
+ return () => observer.disconnect();
298
+ }, []);
299
+ const { leaves, dividers } = layoutBoxes(
300
+ layout,
301
+ size.width,
302
+ size.height,
303
+ gutter2
304
+ ), required = minimumSize(layout, gutter2);
305
+ const show = (next) => {
306
+ if (previewRef.current?.pane === next?.pane && previewRef.current?.edge === next?.edge && previewRef.current?.index === next?.index && previewRef.current?.key === next?.key)
307
+ return;
308
+ previewRef.current = next;
309
+ setPreview(next);
310
+ };
311
+ const canSplit = (box, edge) => edge === "bottom" ? box.height >= minimum.y * 2 + rowGap : box.width >= minimum.x * 2 + columnGap;
312
+ const docking = {
313
+ paneCount: leaves.length,
314
+ next: (key) => {
315
+ const index = leaves.findIndex((p) => p.pane.tabs.includes(key));
316
+ change(
317
+ dockTab(layout, key, leaves[(index + 1) % leaves.length].pane.id)
318
+ );
319
+ },
320
+ move: ({ key, x, y, detached: wasDetached }) => {
321
+ const bounds = board.current.getBoundingClientRect(), localX = x - bounds.left, localY = y - bounds.top;
322
+ const target = leaves.find(
323
+ (p) => localX >= p.x && localX <= p.x + p.width && localY >= p.y && localY <= p.y + p.height
324
+ );
325
+ const source = leaves.find((p) => p.pane.tabs.includes(key));
326
+ const margin = wasDetached ? 8 : 24;
327
+ const detached = localY > source.y + tabHeight + margin || localY < source.y - margin || !!target && (localX < source.x - margin || localX > source.x + source.width + margin);
328
+ if (!detached) {
329
+ show();
330
+ return false;
331
+ }
332
+ let edge;
333
+ if (target && localY > target.y + tabHeight) {
334
+ if (localY > target.y + target.height * 0.72) edge = "bottom";
335
+ else if (localX < target.x + target.width * 0.28) edge = "left";
336
+ else if (localX > target.x + target.width * 0.72) edge = "right";
337
+ }
338
+ const valid = target && (!edge || canSplit(target, edge)) && (!target.pane.tabs.includes(key) || !!edge && target.pane.tabs.length > 1);
339
+ let index;
340
+ if (valid && !edge) {
341
+ const nav = board.current.querySelector(
342
+ `[data-pane-header="${CSS.escape(target.pane.id)}"] [role="tablist"]`
343
+ );
344
+ const header2 = nav?.closest("[data-pane-header]");
345
+ index = target.pane.tabs.length;
346
+ if (nav && header2 && localY <= target.y + tabHeight) {
347
+ const strip = nav.parentElement;
348
+ const inset = strip.getBoundingClientRect().left - header2.getBoundingClientRect().left;
349
+ const trailing = strip.parentElement.nextElementSibling?.getBoundingClientRect().width ?? 0;
350
+ const available = target.width - inset - trailing - 4;
351
+ const width = fitTabWidth(
352
+ tabWidth,
353
+ available,
354
+ target.pane.tabs.length + 1
355
+ );
356
+ const overflow = width * (target.pane.tabs.length + 1) + 16 - available;
357
+ const arrows = overflow > 1 ? Math.min(28, tabHeight) : 0;
358
+ const scroll = Math.min(
359
+ nav.scrollLeft,
360
+ Math.max(0, overflow + arrows * 2)
361
+ );
362
+ index = Math.max(
363
+ 0,
364
+ Math.min(
365
+ target.pane.tabs.length,
366
+ Math.round(
367
+ (localX - target.x - inset - arrows + scroll - 8) / width
368
+ )
369
+ )
370
+ );
371
+ }
372
+ }
373
+ show(valid ? { pane: target.pane.id, key, edge, index } : { key });
374
+ return true;
375
+ },
376
+ drop: () => {
377
+ const target = previewRef.current;
378
+ show();
379
+ if (!target?.pane) return false;
380
+ change(
381
+ dockTab(layout, target.key, target.pane, target.edge, target.index)
382
+ );
383
+ return true;
384
+ },
385
+ cancel: () => show(),
386
+ split: (key, edge) => {
387
+ const target = leaves.find((p) => p.pane.tabs.includes(key));
388
+ if (!target || target.pane.tabs.length < 2 || !canSplit(target, edge))
389
+ return false;
390
+ change(dockTab(layout, key, target.pane.id, edge));
391
+ return true;
392
+ }
393
+ };
394
+ const lifted = preview?.key;
395
+ const regions = leaves.map(({ pane, ...box }) => {
396
+ if (lifted && pane.tabs.includes(lifted)) {
397
+ const remaining = pane.tabs.filter((key) => key !== lifted);
398
+ pane = {
399
+ ...pane,
400
+ active: remaining[Math.min(pane.tabs.indexOf(lifted), remaining.length - 1)]
401
+ };
402
+ }
403
+ if (preview?.pane !== pane.id || !preview.edge) return { pane, box };
404
+ const drop = dropRegion(box, preview.edge, gutter2);
405
+ if (preview.edge === "left")
406
+ return {
407
+ pane,
408
+ box: { ...box, x: drop.x + drop.width + columnGap, width: drop.width }
409
+ };
410
+ if (preview.edge === "right")
411
+ return { pane, box: { ...box, width: drop.width } };
412
+ return { pane, box: { ...box, height: drop.y - box.y - rowGap } };
413
+ });
414
+ const transition = !resizing && size.width > 0 && stableGeometry;
415
+ const top = Math.min(...leaves.map((leaf) => leaf.y));
416
+ const topRight = leaves.filter((leaf) => leaf.y === top).reduce(
417
+ (best, leaf) => !best || leaf.x > best.x ? leaf : best,
418
+ void 0
419
+ )?.pane.id;
420
+ return /* @__PURE__ */ jsx("div", { className: "lab-surface", children: /* @__PURE__ */ jsxs(
421
+ "div",
422
+ {
423
+ ref: board,
424
+ className: "lab-board",
425
+ style: {
426
+ minWidth: required.x,
427
+ minHeight: required.y
428
+ },
429
+ children: [
430
+ regions.map(({ pane, box }, index) => /* @__PURE__ */ jsx(
431
+ "div",
432
+ {
433
+ "data-pane-header": pane.id,
434
+ className: "lab-pane-header",
435
+ style: {
436
+ ...position({ ...box, height: tabHeight }),
437
+ transitionDuration: transition ? "var(--workspace-duration, 180ms)" : "0ms"
438
+ },
439
+ children: header(
440
+ pane,
441
+ index === 0,
442
+ pane.id === topRight,
443
+ {
444
+ ...docking,
445
+ insertion: preview?.pane === pane.id ? preview.index : void 0
446
+ },
447
+ (key = pane.active) => board.current?.querySelector(
448
+ key ? `[id="${CSS.escape(ids.panel(key))}"]` : `[data-pane-sheet="${CSS.escape(pane.id)}"]:not([hidden]):not([data-drag-content])`
449
+ ) ?? null
450
+ )
451
+ },
452
+ pane.id
453
+ )),
454
+ content(regions, transition, lifted),
455
+ dividers.map(({ split, bounds, ratio, low, high, ...box }) => {
456
+ const axis = split.axis, horizontal = axis === "x", dimension = horizontal ? "width" : "height";
457
+ const primary = panes(split.first).filter((p) => p.active);
458
+ const resize = (value) => change(
459
+ updateLayout(layout, split.id, (node) => ({
460
+ ...node,
461
+ ratio: Math.max(low, Math.min(high, value))
462
+ }))
463
+ );
464
+ const target = /* @__PURE__ */ jsx(
465
+ "div",
466
+ {
467
+ role: "separator",
468
+ "aria-label": horizontal ? "Resize columns" : "Resize rows",
469
+ "aria-labelledby": primary.map((p) => ids.tab(p.active)).join(" "),
470
+ "aria-controls": primary.map((p) => ids.panel(p.active)).join(" "),
471
+ "aria-valuetext": `${Math.round(ratio * 100)}% ${horizontal ? "width" : "height"}`,
472
+ title: menu ? "Drag to resize, use arrow keys, or open resize actions with right-click or Shift+F10" : "Drag to resize or use arrow keys",
473
+ "aria-orientation": horizontal ? "vertical" : "horizontal",
474
+ "aria-valuenow": Math.round(ratio * 100),
475
+ "aria-valuemin": Math.round(low * 100),
476
+ "aria-valuemax": Math.round(high * 100),
477
+ tabIndex: 0,
478
+ className: "lab-divider",
479
+ style: position({
480
+ ...box,
481
+ [axis]: box[axis] + gutter2[axis] / 2 - 12,
482
+ [dimension]: 24
483
+ }),
484
+ onDoubleClick: () => resize(horizontal ? 0.5 : 2 / 3),
485
+ onPointerDown: (event) => {
486
+ if (event.button !== 0) return;
487
+ event.preventDefault();
488
+ event.currentTarget.setPointerCapture(event.pointerId);
489
+ setResizing(true);
490
+ },
491
+ onPointerMove: (event) => {
492
+ if (!event.currentTarget.hasPointerCapture(event.pointerId))
493
+ return;
494
+ const b = board.current.getBoundingClientRect();
495
+ const coordinate = horizontal ? event.clientX - b.left : event.clientY - b.top;
496
+ resize(
497
+ (coordinate - bounds[axis]) / (bounds[dimension] - gutter2[axis])
498
+ );
499
+ },
500
+ onLostPointerCapture: () => setResizing(false),
501
+ onKeyDown: (event) => {
502
+ const step = event.shiftKey ? 0.1 : 0.02;
503
+ const values = {
504
+ [horizontal ? "ArrowLeft" : "ArrowUp"]: ratio - step,
505
+ [horizontal ? "ArrowRight" : "ArrowDown"]: ratio + step,
506
+ Home: low,
507
+ End: high
508
+ };
509
+ const value = values[event.key];
510
+ if (value === void 0) return;
511
+ event.preventDefault();
512
+ resize(value);
513
+ }
514
+ }
515
+ );
516
+ return /* @__PURE__ */ jsx(Fragment, { children: menu ? menu(target, [
517
+ {
518
+ label: horizontal ? "Narrower" : "Shorter",
519
+ onSelect: () => resize(ratio - 0.1)
520
+ },
521
+ {
522
+ label: horizontal ? "Wider" : "Taller",
523
+ onSelect: () => resize(ratio + 0.1)
524
+ },
525
+ {
526
+ label: "Reset size",
527
+ onSelect: () => resize(horizontal ? 0.5 : 2 / 3)
528
+ }
529
+ ]) : target }, split.id);
530
+ }),
531
+ /* @__PURE__ */ jsx("span", { role: "status", className: "lab-sr-only", children: preview?.pane && (preview.edge ? `Split ${preview.edge}` : "Move tab here") })
532
+ ]
533
+ }
534
+ ) });
535
+ }
536
+
537
+ // toolbar.tsx
538
+ import { useState as useState3 } from "react";
539
+
540
+ // tabs.tsx
541
+ import * as React from "react";
542
+ import { createPortal } from "react-dom";
543
+
544
+ // icons.tsx
545
+ import { jsx as jsx2 } from "react/jsx-runtime";
546
+ var icon = (strokeWidth, ...paths) => function Icon(props) {
547
+ return /* @__PURE__ */ jsx2(
548
+ "svg",
549
+ {
550
+ width: 16,
551
+ height: 16,
552
+ viewBox: "0 0 16 16",
553
+ fill: "none",
554
+ stroke: "currentColor",
555
+ strokeWidth,
556
+ strokeLinecap: "round",
557
+ strokeLinejoin: "round",
558
+ "aria-hidden": "true",
559
+ ...props,
560
+ children: paths.map((d) => /* @__PURE__ */ jsx2("path", { d }, d))
561
+ }
562
+ );
563
+ };
564
+ var ChevronLeftIcon = icon(4 / 3, "M10 4 6 8l4 4");
565
+ var ChevronRightIcon = icon(4 / 3, "m6 4 4 4-4 4");
566
+ var XIcon = icon(7 / 6, "m12 4-8 8", "m4 4 8 8");
567
+
568
+ // frame.tsx
569
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
570
+ var reach = 16;
571
+ var shoulder = [
572
+ [0, 43.5],
573
+ [8.56, 43.5],
574
+ [13, 30],
575
+ [16, 21],
576
+ [19, 12],
577
+ [22, 8],
578
+ [30, 8]
579
+ ];
580
+ function corner(gap) {
581
+ if (gap >= reach) return shoulder;
582
+ const wall = 0.5 - gap, radius = Math.max(0, gap - 0.5);
583
+ const source = [
584
+ [wall, 59],
585
+ [wall, 50.44],
586
+ [wall + radius * 0.4477, 43.5],
587
+ [wall + radius, 43.5],
588
+ [wall + radius * 1.5523, 43.5],
589
+ ...shoulder.slice(2)
590
+ ];
591
+ const target = [
592
+ [wall, 59],
593
+ [wall, 47.33],
594
+ [wall, 28.4183],
595
+ [wall, 24],
596
+ [wall, 19.5817],
597
+ [wall + 1.7909, 15.5817],
598
+ [wall + 4.6863, 12.6863],
599
+ [wall + 7.5817, 9.7909],
600
+ [wall + 11.5817, 8],
601
+ [wall + 16, 8]
602
+ ];
603
+ const t = 1 - gap / reach, amount = t * t * (3 - 2 * t);
604
+ return source.map(([x, y], i) => [
605
+ x + (target[i][0] - x) * amount,
606
+ y + (target[i][1] - y) * amount
607
+ ]);
608
+ }
609
+ function tabShape(leftGap = reach, rightGap = reach, size = densityPresets.comfortable) {
610
+ const width = size.width + 16;
611
+ const left = corner(leftGap);
612
+ const right = corner(rightGap).map(([x, y]) => [width - x, y]).reverse();
613
+ const coords = ([x, y]) => [
614
+ x,
615
+ size.height - 0.5 + (y - 43.5) * (y < 43.5 ? (size.contentHeight - 0.5) / 35.5 : 1)
616
+ ].map((v) => Math.round(v * 1e3) / 1e3).join(" ");
617
+ const curves = (points) => points.slice(1).map((p, i) => (i % 3 === 0 ? " C" : " ") + coords(p)).join("");
618
+ const outline = `M${coords(left[0])}${curves(left)} H${right[0][0]}${curves(right)}`;
619
+ return [
620
+ `${outline} V${size.height + (leftGap < reach || rightGap < reach ? 15 : 0)} H${left[0][0]} Z`,
621
+ outline
622
+ ];
623
+ }
624
+ function paneShape(leftGap, rightGap, bottom, morph = true, size = densityPresets.comfortable) {
625
+ const width = size.width + 16, baseline = size.height - 0.5, cornerBottom = size.height + 15;
626
+ const left = 0.5 - leftGap, right = width + rightGap - 0.5;
627
+ const rightCorner = morph && rightGap < reach ? "" : ` H${right - 15.5} A15.5 15.5 0 0 1 ${right} ${cornerBottom}`;
628
+ const leftCorner = morph && leftGap < reach ? "" : ` A15.5 15.5 0 0 1 ${left + 15.5} ${baseline} H0`;
629
+ return `${tabShape(morph ? leftGap : reach, morph ? rightGap : reach, size)[1]}${rightCorner} V${bottom - 15.5} A15.5 15.5 0 0 1 ${right - 15.5} ${bottom} H${left + 15.5} A15.5 15.5 0 0 1 ${left} ${bottom - 15.5} V${cornerBottom}${leftCorner} Z`;
630
+ }
631
+ function TabSurface({
632
+ selected,
633
+ keyboardFocus = false,
634
+ width
635
+ }) {
636
+ const density = useTabSize(), size = { ...density, width: width ?? density.width }, viewWidth = size.width + 16;
637
+ const [fill, outline] = tabShape(void 0, void 0, size);
638
+ return /* @__PURE__ */ jsxs2(
639
+ "svg",
640
+ {
641
+ "aria-hidden": "true",
642
+ viewBox: `0 0 ${viewWidth} ${size.height}`,
643
+ strokeWidth: size.borderWidth,
644
+ preserveAspectRatio: "none",
645
+ className: "lab-tab-surface",
646
+ children: [
647
+ /* @__PURE__ */ jsx3(
648
+ "path",
649
+ {
650
+ d: fill,
651
+ className: "lab-tab-fill",
652
+ "data-selected": selected || void 0
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsx3("path", { d: outline, "data-tab-outline": true, className: "lab-outline" }),
656
+ keyboardFocus && /* @__PURE__ */ jsx3("path", { d: outline, className: "lab-tab-focus" }),
657
+ !selected && /* @__PURE__ */ jsx3(
658
+ "path",
659
+ {
660
+ d: `M0 ${size.height - 0.5} H${viewWidth}`,
661
+ className: "lab-outline"
662
+ }
663
+ )
664
+ ]
665
+ }
666
+ );
667
+ }
668
+
669
+ // drag-content.ts
670
+ import { useEffect, useRef as useRef2 } from "react";
671
+ function useDragContent() {
672
+ const generation = useRef2(0);
673
+ const current = useRef2(void 0);
674
+ const release = (animate = true) => {
675
+ const preview = current.current;
676
+ if (!preview) return;
677
+ current.current = void 0;
678
+ const version = ++generation.current;
679
+ const { element, inert } = preview;
680
+ const from = element.getBoundingClientRect();
681
+ element.removeAttribute("data-drag-content");
682
+ element.inert = inert;
683
+ element.style.transition = "none";
684
+ for (const name of ["x", "y", "width", "height"])
685
+ element.style.removeProperty(`--lab-drag-${name}`);
686
+ requestAnimationFrame(() => {
687
+ if (version !== generation.current) return;
688
+ const to = element.getBoundingClientRect();
689
+ element.style.removeProperty("transition");
690
+ if (!animate || !element.isConnected || !to.width || matchMedia("(prefers-reduced-motion: reduce)").matches)
691
+ return;
692
+ const resized = Math.abs(from.width - to.width) > 0.5 || Math.abs(from.height - to.height) > 0.5;
693
+ element.animate(
694
+ [
695
+ {
696
+ transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)`,
697
+ width: `${from.width}px`,
698
+ height: `${from.height}px`,
699
+ filter: "blur(0)",
700
+ opacity: 1
701
+ },
702
+ {
703
+ filter: resized ? "blur(3px)" : "blur(0)",
704
+ opacity: resized ? 0.85 : 1,
705
+ offset: 0.35
706
+ },
707
+ {
708
+ transform: "none",
709
+ width: `${to.width}px`,
710
+ height: `${to.height}px`,
711
+ filter: "blur(0)",
712
+ opacity: 1
713
+ }
714
+ ],
715
+ { duration: 220, easing: "cubic-bezier(.2,.8,.2,1)" }
716
+ );
717
+ });
718
+ };
719
+ useEffect(() => () => release(false), []);
720
+ return {
721
+ release,
722
+ lift(element, tab, fallback) {
723
+ if (current.current?.element !== element) {
724
+ release(false);
725
+ generation.current++;
726
+ element.getAnimations().forEach((animation) => animation.cancel());
727
+ const bounds = element.hidden ? fallback : element.getBoundingClientRect();
728
+ current.current = {
729
+ element,
730
+ width: bounds.width,
731
+ height: bounds.height,
732
+ offset: Math.max(
733
+ 8,
734
+ Math.min(
735
+ tab.getBoundingClientRect().left - bounds.left,
736
+ bounds.width - tab.offsetWidth - 8
737
+ )
738
+ ),
739
+ inert: element.inert
740
+ };
741
+ for (const [name, value] of Object.entries({
742
+ width: `${bounds.width}px`,
743
+ height: `${bounds.height}px`
744
+ }))
745
+ element.style.setProperty(`--lab-drag-${name}`, String(value));
746
+ element.inert = true;
747
+ element.setAttribute("data-drag-content", "");
748
+ }
749
+ return current.current;
750
+ },
751
+ move(x, y) {
752
+ const element = current.current?.element;
753
+ element?.style.setProperty("--lab-drag-x", `${x}px`);
754
+ element?.style.setProperty("--lab-drag-y", `${y}px`);
755
+ }
756
+ };
757
+ }
758
+
759
+ // tabs.tsx
760
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
761
+ var motion = { duration: 160, easing: "cubic-bezier(.2,.8,.2,1)" };
762
+ function WorkspaceTabs({
763
+ items,
764
+ active,
765
+ onClose,
766
+ onDuplicate,
767
+ onReorder,
768
+ onSelect,
769
+ dragBoundary,
770
+ surface,
771
+ docking,
772
+ menu
773
+ }) {
774
+ const ids = useWorkspaceIds();
775
+ const content = useDragContent();
776
+ const density = useTabSize();
777
+ const [metrics, setMetrics] = React.useState({
778
+ available: Infinity,
779
+ overflow: false,
780
+ left: false,
781
+ right: false
782
+ });
783
+ const measured = React.useRef(metrics);
784
+ const { available } = metrics;
785
+ const size = {
786
+ ...density,
787
+ width: fitTabWidth(
788
+ density.width,
789
+ available,
790
+ items.length + (docking.insertion === void 0 ? 0 : 1)
791
+ )
792
+ }, { width: tabWidth, height, contentHeight } = size, width = tabWidth + 16;
793
+ const links = items.map(({ key, label }) => [key, label]);
794
+ const tabOrder = JSON.stringify(items.map(({ key }) => key));
795
+ const labels = new Map(links);
796
+ const navRef = React.useRef(null);
797
+ const stripRef = React.useRef(null);
798
+ const helpId = React.useId();
799
+ const menuFocus = React.useRef(void 0);
800
+ const drag = React.useRef(void 0);
801
+ const [dragging, setDragging] = React.useState();
802
+ const [pointerFocus, setPointerFocus] = React.useState(false);
803
+ const [announcement, announce] = React.useState("");
804
+ const ghostRef = React.useRef(null);
805
+ const ghost = React.useCallback((element) => {
806
+ ghostRef.current = element;
807
+ if (element) element.inert = true;
808
+ }, []);
809
+ const frameRef = React.useRef(null);
810
+ const maskedSheet = React.useRef(null);
811
+ const maskedOutline = React.useRef(null);
812
+ const restoreFrame = () => {
813
+ maskedSheet.current?.style.removeProperty("border-color");
814
+ maskedOutline.current?.style.removeProperty("visibility");
815
+ maskedSheet.current = null;
816
+ maskedOutline.current = null;
817
+ };
818
+ const settling = React.useRef(void 0);
819
+ const positions = React.useRef({
820
+ order: "",
821
+ tabs: /* @__PURE__ */ new Map()
822
+ });
823
+ const frame = React.useRef(0);
824
+ const suppressClick = React.useRef(false);
825
+ const tabElement = (key) => navRef.current?.querySelector(
826
+ `a[data-tab="${CSS.escape(key)}"]`
827
+ )?.parentElement;
828
+ const reveal = (key = active) => {
829
+ const nav = navRef.current, tab = tabElement(key);
830
+ if (!nav || !tab || drag.current?.moved || docking.insertion !== void 0)
831
+ return;
832
+ nav.scrollLeft = Math.max(
833
+ tab.offsetLeft + tab.offsetWidth + 8 - nav.clientWidth,
834
+ Math.min(nav.scrollLeft, tab.offsetLeft - 8)
835
+ );
836
+ };
837
+ const draw = () => {
838
+ const current = drag.current, ghost2 = ghostRef.current, nav = navRef.current, bounds = dragBoundary?.getBoundingClientRect(), floating = !!ghost2 && !!current?.detached, tab = (floating ? null : ghost2) ?? tabElement(active), frame2 = frameRef.current;
839
+ if (ghost2 && current?.moved && bounds) {
840
+ const sheet2 = surface(current.key);
841
+ const preview = floating && sheet2 ? content.lift(sheet2, ghost2, current.sheet) : void 0;
842
+ const left2 = floating ? 8 : bounds.left + 8, right2 = floating ? innerWidth - 8 : bounds.right - 8;
843
+ const x2 = Math.max(
844
+ left2,
845
+ Math.min(current.x - current.offset, right2 - ghost2.offsetWidth)
846
+ );
847
+ const y = floating ? Math.max(
848
+ 0,
849
+ Math.min(current.y - current.offsetY, innerHeight - height - 8)
850
+ ) : nav.getBoundingClientRect().top;
851
+ ghost2.style.left = `${x2}px`;
852
+ ghost2.style.top = `${y}px`;
853
+ if (preview) {
854
+ content.move(x2 - preview.offset, y + height - 1);
855
+ const left3 = preview.offset - 8, right3 = preview.width - preview.offset - tabWidth - 8;
856
+ ghost2.querySelector("svg > path").setAttribute("d", tabShape(left3, right3, size)[0]);
857
+ const outline = ghost2.querySelector("[data-tab-outline]");
858
+ outline.style.visibility = "visible";
859
+ outline.setAttribute(
860
+ "d",
861
+ paneShape(left3, right3, height + preview.height - 1.5, true, size)
862
+ );
863
+ } else content.release();
864
+ }
865
+ restoreFrame();
866
+ if (!tab || !bounds || !frame2) {
867
+ if (frame2) frame2.style.visibility = "hidden";
868
+ return;
869
+ }
870
+ const box = tab.getBoundingClientRect();
871
+ const sheet = surface(), sheetBox = sheet?.getBoundingClientRect();
872
+ const attached = Math.abs(box.top - bounds.top) < 0.5 && !!sheet;
873
+ const left = attached ? box.left - 8 - sheetBox.left : 16, right = attached ? sheetBox.right - box.right - 8 : 16;
874
+ if (ghost2 && !floating) {
875
+ const [fill, outline] = tabShape(
876
+ Math.max(0, left),
877
+ Math.max(0, right),
878
+ size
879
+ );
880
+ ghost2.querySelector("svg > path").setAttribute("d", fill);
881
+ ghost2.querySelector("[data-tab-outline]").setAttribute("d", outline);
882
+ }
883
+ const stroke = tab.querySelector("[data-tab-outline]");
884
+ stroke.style.visibility = attached ? "hidden" : "visible";
885
+ maskedOutline.current = stroke;
886
+ frame2.style.visibility = attached ? "visible" : "hidden";
887
+ if (!attached) return;
888
+ sheet.style.borderColor = "transparent";
889
+ maskedSheet.current = sheet;
890
+ const x = box.left - 8, bottom = sheetBox.bottom - box.top - 0.5;
891
+ const origin = frame2.parentElement.getBoundingClientRect();
892
+ frame2.style.left = `${x - origin.left}px`;
893
+ frame2.style.top = `${box.top - origin.top}px`;
894
+ const viewport = ghost2 && !floating ? sheetBox : nav.getBoundingClientRect(), clipLeft = Math.max(-left, viewport.left - x), clipRight = Math.min(width + right, viewport.right - x);
895
+ frame2.style.clipPath = `polygon(${clipLeft}px 0px, ${clipRight}px 0px, ${clipRight}px ${height - 1}px, ${width + right}px ${height - 1}px, ${width + right}px ${bottom + 1}px, ${-left}px ${bottom + 1}px, ${-left}px ${height - 1}px, ${clipLeft}px ${height - 1}px)`;
896
+ frame2.querySelector("path").setAttribute(
897
+ "d",
898
+ paneShape(left, right, bottom, !!ghost2 && !floating, size) + ((!ghost2 || floating) && clipLeft > 0 ? ` M0 ${height - 0.5} H${Math.min(width, clipLeft)}` : "") + ((!ghost2 || floating) && clipRight < width ? ` M${Math.max(0, clipRight)} ${height - 0.5} H${width}` : "")
899
+ );
900
+ };
901
+ const animate = (element, from) => {
902
+ const transform = getComputedStyle(element).transform;
903
+ if (transform !== "none") from += new DOMMatrixReadOnly(transform).m41;
904
+ element.getAnimations().forEach((animation) => animation.cancel());
905
+ if (!matchMedia("(prefers-reduced-motion: reduce)").matches)
906
+ element.animate(
907
+ [{ transform: `translateX(${from}px)` }, { transform: "none" }],
908
+ motion
909
+ );
910
+ };
911
+ const settle = (key) => requestAnimationFrame(() => {
912
+ if (drag.current) return;
913
+ reveal(key);
914
+ const ghost2 = ghostRef.current, slot = tabElement(key);
915
+ if (!ghost2 || !slot || matchMedia("(prefers-reduced-motion: reduce)").matches) {
916
+ setDragging(void 0);
917
+ return;
918
+ }
919
+ const from = ghost2.getBoundingClientRect(), to = slot.getBoundingClientRect();
920
+ settling.current = ghost2.animate(
921
+ [
922
+ { transform: "none" },
923
+ {
924
+ transform: `translate(${to.left - from.left}px,${to.top - from.top}px)`
925
+ }
926
+ ],
927
+ { ...motion, fill: "forwards" }
928
+ );
929
+ settling.current.onfinish = () => setDragging(void 0);
930
+ latest.current.refresh();
931
+ });
932
+ const finish = (cancel = false) => {
933
+ const current = drag.current;
934
+ if (!current) return;
935
+ drag.current = void 0;
936
+ suppressClick.current = current.moved;
937
+ content.release();
938
+ if (cancel) {
939
+ docking.cancel();
940
+ if (current.moved) onReorder(current.order);
941
+ announce("");
942
+ }
943
+ if (current.moved && (cancel || !docking.drop())) settle(current.key);
944
+ else setDragging(void 0);
945
+ };
946
+ const move = (from, to) => {
947
+ const order = links.map(([key]) => key), index = order.indexOf(to);
948
+ if (from === to || index < 0) return;
949
+ order.splice(order.indexOf(from), 1);
950
+ order.splice(index, 0, from);
951
+ onReorder(order);
952
+ announce(`${labels.get(from)}: position ${index + 1} of ${order.length}`);
953
+ };
954
+ const focusTab = (key) => requestAnimationFrame(() => document.getElementById(ids.tab(key))?.focus());
955
+ const split = (key, edge) => {
956
+ announce(
957
+ docking.split(key, edge) ? `${labels.get(key)} split ${edge}` : "Open another tab or make this pane larger to split it"
958
+ );
959
+ focusTab(key);
960
+ };
961
+ const keyDown = (event, key, index) => {
962
+ if (event.key === "Escape") return finish(true);
963
+ const edges = {
964
+ ArrowLeft: "left",
965
+ ArrowRight: "right",
966
+ ArrowDown: "bottom"
967
+ };
968
+ const edge = edges[event.key];
969
+ if (event.altKey && edge) {
970
+ if (event.shiftKey) split(key, edge);
971
+ else if (edge !== "bottom")
972
+ move(key, items[index + (edge === "left" ? -1 : 1)]?.key ?? key);
973
+ else return;
974
+ } else {
975
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
976
+ const targets = {
977
+ Home: 0,
978
+ End: items.length - 1,
979
+ ArrowLeft: (index + items.length - 1) % items.length,
980
+ ArrowRight: (index + 1) % items.length
981
+ };
982
+ const target = targets[event.key];
983
+ if (target !== void 0)
984
+ document.getElementById(ids.tab(items[target].key))?.focus();
985
+ else if (event.key === "Delete") onClose(key);
986
+ else if (event.key === " ") onSelect(key);
987
+ else return;
988
+ }
989
+ event.preventDefault();
990
+ };
991
+ const measure = () => {
992
+ const nav = navRef.current, strip = stripRef.current;
993
+ if (!nav || !strip || !dragBoundary) return;
994
+ const trailing = strip.parentElement.nextElementSibling?.getBoundingClientRect().width ?? 0;
995
+ const availableWidth = Math.max(
996
+ 0,
997
+ dragBoundary.getBoundingClientRect().right - strip.getBoundingClientRect().left - trailing - 4
998
+ );
999
+ const inserting = docking.insertion === void 0 ? 0 : 1;
1000
+ const count = items.length - (drag.current?.detached ? 1 : 0) + inserting;
1001
+ const next = {
1002
+ available: availableWidth,
1003
+ overflow: count * fitTabWidth(density.width, availableWidth, items.length + inserting) + 16 > availableWidth + 1,
1004
+ left: nav.scrollLeft > 1,
1005
+ right: nav.scrollLeft + nav.clientWidth < nav.scrollWidth - 1
1006
+ };
1007
+ const current = measured.current;
1008
+ if (next.available !== current.available || next.overflow !== current.overflow || next.left !== current.left || next.right !== current.right) {
1009
+ measured.current = next;
1010
+ setMetrics(next);
1011
+ }
1012
+ };
1013
+ const follow = () => {
1014
+ const nav = navRef.current;
1015
+ if ([surface(), nav, ghostRef.current].some(
1016
+ (element) => element?.getAnimations({ subtree: element === nav }).some((animation) => animation.playState === "running")
1017
+ ))
1018
+ frame.current = requestAnimationFrame(() => latest.current.refresh());
1019
+ };
1020
+ const refresh = (resized = false) => {
1021
+ cancelAnimationFrame(frame.current);
1022
+ measure();
1023
+ if (resized) reveal();
1024
+ draw();
1025
+ follow();
1026
+ };
1027
+ const latest = React.useRef({ refresh, finish });
1028
+ const detached = !!dragging && !!drag.current?.detached;
1029
+ React.useLayoutEffect(() => {
1030
+ latest.current = { refresh, finish };
1031
+ const order = JSON.stringify([tabOrder, docking.insertion]);
1032
+ const previous = positions.current;
1033
+ positions.current = { order, tabs: /* @__PURE__ */ new Map() };
1034
+ for (const element of navRef.current.children) {
1035
+ const key = element.querySelector("a")?.dataset.tab;
1036
+ if (!key) continue;
1037
+ const left = element.offsetLeft, from = previous.tabs.get(key);
1038
+ if (previous.order !== order && key !== dragging && from !== void 0 && from !== left)
1039
+ animate(element, from - left);
1040
+ positions.current.tabs.set(key, left);
1041
+ }
1042
+ cancelAnimationFrame(frame.current);
1043
+ draw();
1044
+ follow();
1045
+ });
1046
+ React.useLayoutEffect(
1047
+ () => refresh(),
1048
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- the inputs refresh measures
1049
+ [
1050
+ dragBoundary,
1051
+ active,
1052
+ tabOrder,
1053
+ docking.insertion,
1054
+ tabWidth,
1055
+ height,
1056
+ metrics.overflow,
1057
+ dragging,
1058
+ detached
1059
+ ]
1060
+ );
1061
+ React.useLayoutEffect(() => reveal(), [active, tabOrder, docking.insertion]);
1062
+ React.useLayoutEffect(() => {
1063
+ const nav = navRef.current;
1064
+ const resize = () => latest.current.refresh(true), scroll = () => latest.current.refresh(), blur = () => latest.current.finish(true);
1065
+ const wheel = (event) => {
1066
+ if (event.ctrlKey || Math.abs(event.deltaX) >= Math.abs(event.deltaY) || nav.scrollWidth <= nav.clientWidth)
1067
+ return;
1068
+ event.preventDefault();
1069
+ nav.scrollLeft += event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? nav.clientWidth : 1);
1070
+ };
1071
+ const observer = new ResizeObserver(resize);
1072
+ for (const element of [
1073
+ nav,
1074
+ stripRef.current,
1075
+ dragBoundary,
1076
+ ...dragBoundary?.children ?? [],
1077
+ surface()
1078
+ ])
1079
+ if (element) observer.observe(element);
1080
+ addEventListener("scroll", scroll, true);
1081
+ addEventListener("blur", blur);
1082
+ nav.addEventListener("wheel", wheel, { passive: false });
1083
+ return () => {
1084
+ observer.disconnect();
1085
+ removeEventListener("scroll", scroll, true);
1086
+ removeEventListener("blur", blur);
1087
+ nav.removeEventListener("wheel", wheel);
1088
+ cancelAnimationFrame(frame.current);
1089
+ restoreFrame();
1090
+ };
1091
+ }, [active, dragBoundary]);
1092
+ const scrollButton = (direction) => metrics.overflow && /* @__PURE__ */ jsx4(
1093
+ "button",
1094
+ {
1095
+ type: "button",
1096
+ className: "lab-scroll",
1097
+ style: { width: Math.min(28, height), height: Math.min(28, height) },
1098
+ "aria-label": direction < 0 ? "Scroll tabs left" : "Scroll tabs right",
1099
+ title: direction < 0 ? "Scroll tabs left" : "Scroll tabs right",
1100
+ disabled: direction < 0 ? !metrics.left : !metrics.right,
1101
+ onClick: () => {
1102
+ const nav = navRef.current;
1103
+ nav.scrollBy({
1104
+ left: direction * Math.max(tabWidth, nav.clientWidth - tabWidth),
1105
+ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth"
1106
+ });
1107
+ },
1108
+ children: direction < 0 ? /* @__PURE__ */ jsx4(ChevronLeftIcon, { className: "lab-icon" }) : /* @__PURE__ */ jsx4(ChevronRightIcon, { className: "lab-icon" })
1109
+ }
1110
+ );
1111
+ return /* @__PURE__ */ jsxs3("div", { ref: stripRef, className: "lab-tabs", children: [
1112
+ scrollButton(-1),
1113
+ /* @__PURE__ */ jsxs3(
1114
+ "div",
1115
+ {
1116
+ ref: navRef,
1117
+ role: links.length ? "tablist" : void 0,
1118
+ "aria-label": links.length ? `Workspace tabs: ${labels.get(active)}` : void 0,
1119
+ "aria-describedby": links.length ? helpId : void 0,
1120
+ onKeyDownCapture: () => setPointerFocus(false),
1121
+ onBlurCapture: () => setPointerFocus(false),
1122
+ onPointerDownCapture: () => {
1123
+ content.release(false);
1124
+ settling.current?.cancel();
1125
+ setDragging(void 0);
1126
+ drag.current = void 0;
1127
+ suppressClick.current = false;
1128
+ },
1129
+ onDragStart: (event) => event.preventDefault(),
1130
+ onClickCapture: (event) => {
1131
+ if (suppressClick.current) {
1132
+ event.preventDefault();
1133
+ event.stopPropagation();
1134
+ suppressClick.current = false;
1135
+ }
1136
+ },
1137
+ onPointerMove: (event) => {
1138
+ const current = drag.current, nav = navRef.current, x = event.clientX;
1139
+ if (!(event.buttons & 1) || !current || !nav) return;
1140
+ if (!current.moved && Math.hypot(x - current.x, event.clientY - current.y) < 6)
1141
+ return;
1142
+ if (!current.moved && active !== current.key) onSelect(current.key);
1143
+ current.moved = true;
1144
+ if (!nav.hasPointerCapture(event.pointerId))
1145
+ nav.setPointerCapture(event.pointerId);
1146
+ setDragging(current.key);
1147
+ event.preventDefault();
1148
+ current.x = x;
1149
+ current.y = event.clientY;
1150
+ current.detached = docking.move(current);
1151
+ if (!current.detached) {
1152
+ const { left, right } = nav.getBoundingClientRect();
1153
+ nav.scrollLeft += x < left + 24 ? -16 : x > right - 24 ? 16 : 0;
1154
+ const index = Math.max(
1155
+ 0,
1156
+ Math.min(
1157
+ items.length - 1,
1158
+ Math.floor(
1159
+ (Math.max(left + 8, Math.min(x, right - 8)) - left + nav.scrollLeft - 8) / tabWidth
1160
+ )
1161
+ )
1162
+ );
1163
+ move(current.key, items[index].key);
1164
+ }
1165
+ refresh();
1166
+ },
1167
+ onPointerUp: () => finish(),
1168
+ onPointerCancel: () => finish(true),
1169
+ className: "lab-tablist",
1170
+ children: [
1171
+ links.map(([key, text], index) => {
1172
+ const actions = [
1173
+ ...onDuplicate ? [
1174
+ {
1175
+ label: "Duplicate tab",
1176
+ onSelect: () => {
1177
+ menuFocus.current = onDuplicate(key);
1178
+ }
1179
+ }
1180
+ ] : [],
1181
+ {
1182
+ label: "Move left",
1183
+ onSelect: () => move(key, items[index - 1].key),
1184
+ disabled: !index
1185
+ },
1186
+ {
1187
+ label: "Move right",
1188
+ onSelect: () => move(key, items[index + 1].key),
1189
+ disabled: index === items.length - 1
1190
+ },
1191
+ {
1192
+ label: "Move to next pane",
1193
+ onSelect: () => docking.next(key),
1194
+ disabled: docking.paneCount < 2
1195
+ },
1196
+ null,
1197
+ ...["left", "right", "bottom"].map((edge) => ({
1198
+ label: `Split ${edge === "bottom" ? "below" : edge}`,
1199
+ onSelect: () => split(key, edge),
1200
+ disabled: items.length < 2
1201
+ })),
1202
+ null,
1203
+ { label: "Close tab", onSelect: () => onClose(key) }
1204
+ ];
1205
+ const target = /* @__PURE__ */ jsxs3(
1206
+ "div",
1207
+ {
1208
+ role: "presentation",
1209
+ className: "lab-tab",
1210
+ "data-dragging": dragging === key || void 0,
1211
+ style: {
1212
+ paddingTop: height - contentHeight,
1213
+ marginRight: dragging === key && drag.current?.detached ? -tabWidth : 0,
1214
+ marginLeft: docking.insertion === index ? tabWidth : 0,
1215
+ zIndex: active === key ? links.length + 1 : links.length - index
1216
+ },
1217
+ children: [
1218
+ /* @__PURE__ */ jsx4(
1219
+ TabSurface,
1220
+ {
1221
+ width: tabWidth,
1222
+ selected: active === key,
1223
+ keyboardFocus: !pointerFocus
1224
+ }
1225
+ ),
1226
+ /* @__PURE__ */ jsxs3(
1227
+ "a",
1228
+ {
1229
+ id: ids.tab(key),
1230
+ role: "tab",
1231
+ tabIndex: active === key ? 0 : -1,
1232
+ "aria-selected": active === key,
1233
+ "aria-controls": ids.panel(key),
1234
+ href: items[index].href,
1235
+ "data-tab": key,
1236
+ style: { width: tabWidth, height: contentHeight },
1237
+ onClick: (event) => {
1238
+ const { metaKey, ctrlKey, shiftKey, altKey } = event;
1239
+ if (metaKey || ctrlKey || shiftKey || altKey) return;
1240
+ event.preventDefault();
1241
+ onSelect(key);
1242
+ },
1243
+ onFocus: () => onSelect(key),
1244
+ draggable: false,
1245
+ title: text,
1246
+ "aria-keyshortcuts": "ArrowLeft ArrowRight Home End Delete Alt+ArrowLeft Alt+ArrowRight Alt+Shift+ArrowLeft Alt+Shift+ArrowRight Alt+Shift+ArrowDown",
1247
+ onKeyDown: (event) => keyDown(event, key, index),
1248
+ onPointerDown: (event) => {
1249
+ if (event.button !== 0 || !event.isPrimary) return;
1250
+ if (event.pointerType === "mouse") event.preventDefault();
1251
+ event.currentTarget.setPointerCapture(event.pointerId);
1252
+ event.currentTarget.focus({ preventScroll: true });
1253
+ setPointerFocus(true);
1254
+ const box = event.currentTarget.parentElement.getBoundingClientRect();
1255
+ drag.current = {
1256
+ key,
1257
+ offset: event.clientX - box.left,
1258
+ offsetY: event.clientY - box.top,
1259
+ sheet: surface().getBoundingClientRect(),
1260
+ x: event.clientX,
1261
+ y: event.clientY,
1262
+ detached: false,
1263
+ moved: false,
1264
+ order: links.map(([path]) => path)
1265
+ };
1266
+ },
1267
+ className: "lab-tab-link",
1268
+ children: [
1269
+ items[index].icon && /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "lab-tab-icon", children: items[index].icon }),
1270
+ /* @__PURE__ */ jsx4("span", { className: "lab-tab-label", children: text })
1271
+ ]
1272
+ }
1273
+ ),
1274
+ /* @__PURE__ */ jsx4(CloseTab, { label: text, onClick: () => onClose(key) })
1275
+ ]
1276
+ }
1277
+ );
1278
+ return /* @__PURE__ */ jsx4(React.Fragment, { children: menu ? menu(
1279
+ target,
1280
+ actions,
1281
+ () => {
1282
+ focusTab(menuFocus.current ?? key);
1283
+ menuFocus.current = void 0;
1284
+ },
1285
+ key
1286
+ ) : target }, key);
1287
+ }),
1288
+ docking.insertion === items.length && /* @__PURE__ */ jsx4(
1289
+ "div",
1290
+ {
1291
+ "aria-hidden": "true",
1292
+ style: { width: tabWidth },
1293
+ className: "lab-tab-placeholder"
1294
+ }
1295
+ )
1296
+ ]
1297
+ }
1298
+ ),
1299
+ scrollButton(1),
1300
+ /* @__PURE__ */ jsx4("span", { className: "lab-sr-only", "aria-live": "polite", children: announcement }),
1301
+ /* @__PURE__ */ jsxs3("span", { id: helpId, className: "lab-sr-only", children: [
1302
+ "Arrow keys switch tabs. Home and End select the first and last tab. Delete closes a tab. Alt plus Left or Right reorders. Alt plus Shift and Left, Right, or Down splits a pane. F6 moves between panes; Shift plus F6 moves back.",
1303
+ menu && " Right-click or press Shift plus F10 for tab actions without dragging."
1304
+ ] }),
1305
+ active && /* @__PURE__ */ jsx4(
1306
+ "svg",
1307
+ {
1308
+ ref: frameRef,
1309
+ "aria-hidden": "true",
1310
+ "data-pane-frame": true,
1311
+ viewBox: `0 0 ${width} ${height}`,
1312
+ strokeWidth: size.borderWidth,
1313
+ style: { width, height },
1314
+ className: "lab-pane-frame",
1315
+ children: /* @__PURE__ */ jsx4("path", { className: "lab-outline" })
1316
+ }
1317
+ ),
1318
+ dragging && createPortal(
1319
+ /* @__PURE__ */ jsxs3(
1320
+ "div",
1321
+ {
1322
+ ref: ghost,
1323
+ "data-dragged-tab": true,
1324
+ "aria-hidden": "true",
1325
+ style: {
1326
+ width: tabWidth,
1327
+ height,
1328
+ paddingTop: height - contentHeight
1329
+ },
1330
+ className: "lab-ghost",
1331
+ children: [
1332
+ /* @__PURE__ */ jsx4(TabSurface, { selected: true, width: tabWidth }),
1333
+ /* @__PURE__ */ jsxs3(
1334
+ "span",
1335
+ {
1336
+ style: { height: contentHeight },
1337
+ className: "lab-ghost-content",
1338
+ children: [
1339
+ items.find((item) => item.key === dragging)?.icon && /* @__PURE__ */ jsx4("span", { className: "lab-tab-icon", children: items.find((item) => item.key === dragging).icon }),
1340
+ /* @__PURE__ */ jsx4("span", { className: "lab-tab-label", children: labels.get(dragging) })
1341
+ ]
1342
+ }
1343
+ ),
1344
+ /* @__PURE__ */ jsx4(CloseTab, { label: labels.get(dragging) })
1345
+ ]
1346
+ }
1347
+ ),
1348
+ document.body
1349
+ )
1350
+ ] });
1351
+ }
1352
+ function CloseTab({ label, onClick }) {
1353
+ const { height, contentHeight } = useTabSize();
1354
+ return /* @__PURE__ */ jsx4(
1355
+ "button",
1356
+ {
1357
+ type: "button",
1358
+ tabIndex: -1,
1359
+ "aria-label": `Close ${label}`,
1360
+ title: `Close ${label}`,
1361
+ style: { top: height - contentHeight / 2 },
1362
+ className: "lab-tab-close",
1363
+ onClick,
1364
+ children: /* @__PURE__ */ jsx4(XIcon, { className: "lab-tab-close-icon" })
1365
+ }
1366
+ );
1367
+ }
1368
+
1369
+ // toolbar.tsx
1370
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1371
+ function WorkspaceToolbar({
1372
+ active,
1373
+ leading,
1374
+ reserve,
1375
+ end,
1376
+ onActivate,
1377
+ ...tabProps
1378
+ }) {
1379
+ const { height } = useTabSize();
1380
+ const [header, setHeader] = useState3(null);
1381
+ return /* @__PURE__ */ jsxs4(
1382
+ "div",
1383
+ {
1384
+ ref: setHeader,
1385
+ style: { height },
1386
+ className: "lab-toolbar",
1387
+ "data-leading": leading ? "" : void 0,
1388
+ onPointerDownCapture: onActivate,
1389
+ onFocusCapture: onActivate,
1390
+ children: [
1391
+ leading,
1392
+ /* @__PURE__ */ jsx5("div", { className: "lab-toolbar-tabs", children: /* @__PURE__ */ jsx5(
1393
+ WorkspaceTabs,
1394
+ {
1395
+ ...tabProps,
1396
+ dragBoundary: header,
1397
+ active: active ?? ""
1398
+ }
1399
+ ) }),
1400
+ /* @__PURE__ */ jsxs4("div", { className: "lab-toolbar-end", children: [
1401
+ end,
1402
+ tabProps.items.find((tab) => tab.key === active)?.actions
1403
+ ] }),
1404
+ !!reserve && /* @__PURE__ */ jsx5("div", { className: "lab-toolbar-trailing", style: { width: reserve } })
1405
+ ]
1406
+ }
1407
+ );
1408
+ }
1409
+
1410
+ // unsaved-changes.ts
1411
+ import * as React2 from "react";
1412
+ var UnsavedChanges = React2.createContext(void 0);
1413
+ function useUnsavedChanges(dirty) {
1414
+ const report = React2.useContext(UnsavedChanges);
1415
+ const source = React2.useRef(/* @__PURE__ */ Symbol());
1416
+ React2.useLayoutEffect(() => {
1417
+ const id = source.current;
1418
+ report?.(id, dirty);
1419
+ return () => report?.(id, false);
1420
+ }, [dirty, report]);
1421
+ }
1422
+
1423
+ // workspace.tsx
1424
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1425
+ function Workspace(props) {
1426
+ const id = useId2();
1427
+ return /* @__PURE__ */ jsx6(WorkspaceId.Provider, { value: id, children: /* @__PURE__ */ jsx6(WorkspaceDensity.Provider, { value: props.density ?? 100, children: /* @__PURE__ */ jsx6(WorkspaceContent, { ...props }) }) });
1428
+ }
1429
+ function WorkspaceContent({
1430
+ density = 100,
1431
+ tabs,
1432
+ active,
1433
+ onSelect,
1434
+ onClose,
1435
+ onDuplicate,
1436
+ leading,
1437
+ trailing,
1438
+ toolbar,
1439
+ menu,
1440
+ confirmClose,
1441
+ layout,
1442
+ onLayoutChange: change,
1443
+ className,
1444
+ children,
1445
+ empty
1446
+ }) {
1447
+ const ids = useWorkspaceIds();
1448
+ const { height, logoSize } = useTabSize();
1449
+ const corner2 = useRef5(null);
1450
+ const [reserve, setReserve] = useState4(0);
1451
+ const hasTrailing = !!trailing;
1452
+ useLayoutEffect4(() => {
1453
+ const element = corner2.current;
1454
+ if (!element) return setReserve(0);
1455
+ const observer = new ResizeObserver(() => setReserve(element.offsetWidth));
1456
+ observer.observe(element);
1457
+ return () => observer.disconnect();
1458
+ }, [hasTrailing]);
1459
+ const root = useRef5(null);
1460
+ const preferredPane = useRef5(void 0);
1461
+ const visibleLayout = reconcileLayout(
1462
+ layout,
1463
+ tabs.map((t) => t.key),
1464
+ active,
1465
+ preferredPane.current
1466
+ );
1467
+ useLayoutEffect4(() => {
1468
+ if (visibleLayout !== layout) change(visibleLayout);
1469
+ }, [visibleLayout, layout]);
1470
+ const dirty = useRef5(/* @__PURE__ */ new Map());
1471
+ useEffect2(() => {
1472
+ const warn = (event) => {
1473
+ if (dirty.current.size) event.preventDefault();
1474
+ };
1475
+ addEventListener("beforeunload", warn);
1476
+ return () => removeEventListener("beforeunload", warn);
1477
+ }, []);
1478
+ function close(key) {
1479
+ const pane = panes(visibleLayout).find((p) => p.tabs.includes(key));
1480
+ if (!pane) return;
1481
+ const remaining = pane.tabs.filter((k) => k !== key);
1482
+ const opener = tabs.find((tab) => tab.key === key)?.opener;
1483
+ const neighbor = remaining.find((key2) => key2 === opener) ?? remaining[Math.min(pane.tabs.indexOf(key), remaining.length - 1)] ?? tabs.find((t) => t.key !== key)?.key;
1484
+ onClose(key, neighbor);
1485
+ requestAnimationFrame(
1486
+ () => (document.getElementById(
1487
+ ids.tab(key === pane.active ? neighbor ?? "" : pane.active ?? "")
1488
+ ) ?? root.current?.querySelector(
1489
+ ".lab-toolbar-end button, [data-pane-sheet]"
1490
+ ))?.focus({ preventScroll: true })
1491
+ );
1492
+ }
1493
+ const focus = (key) => requestAnimationFrame(() => document.getElementById(ids.tab(key))?.focus());
1494
+ const paneOf = (pane) => ({
1495
+ id: pane.id,
1496
+ tabs: pane.tabs,
1497
+ active: pane.active,
1498
+ focus
1499
+ });
1500
+ return /* @__PURE__ */ jsx6(Fragment3, { children: /* @__PURE__ */ jsxs5(
1501
+ "div",
1502
+ {
1503
+ ref: root,
1504
+ role: "region",
1505
+ "aria-label": "Workspace",
1506
+ "data-density": density,
1507
+ style: {
1508
+ "--lab-tab-height": `${height}px`,
1509
+ "--lab-logo-size": `${logoSize}px`
1510
+ },
1511
+ className: className ? `lab-workspace ${className}` : "lab-workspace",
1512
+ onKeyDown: (event) => {
1513
+ if (event.key !== "F6") return;
1514
+ event.preventDefault();
1515
+ const selected = Array.from(
1516
+ event.currentTarget.querySelectorAll(
1517
+ '[role="tab"][aria-selected="true"]'
1518
+ )
1519
+ );
1520
+ const owner = document.activeElement?.closest(
1521
+ '[data-pane-header], [role="tabpanel"]'
1522
+ );
1523
+ const index = selected.findIndex(
1524
+ (tab) => owner?.contains(tab) || tab.getAttribute("aria-controls") === owner?.id
1525
+ );
1526
+ selected[(index + (event.shiftKey ? selected.length - 1 : 1)) % selected.length]?.focus();
1527
+ },
1528
+ children: [
1529
+ trailing && /* @__PURE__ */ jsx6("div", { ref: corner2, className: "lab-trailing", style: { height }, children: trailing }),
1530
+ /* @__PURE__ */ jsx6(
1531
+ WorkspaceSurface,
1532
+ {
1533
+ layout: visibleLayout,
1534
+ change,
1535
+ menu,
1536
+ header: (pane, first, last, docking, surface) => /* @__PURE__ */ jsx6(
1537
+ WorkspaceToolbar,
1538
+ {
1539
+ leading: first ? leading : void 0,
1540
+ reserve: last ? reserve : void 0,
1541
+ end: toolbar?.(paneOf(pane)),
1542
+ menu,
1543
+ onActivate: () => {
1544
+ preferredPane.current = pane.id;
1545
+ },
1546
+ docking,
1547
+ surface,
1548
+ items: pane.tabs.map((key) => tabs.find((t) => t.key === key)),
1549
+ active: pane.active,
1550
+ onSelect,
1551
+ onDuplicate: onDuplicate ? (key) => {
1552
+ const copy = onDuplicate(key), index = pane.tabs.indexOf(key) + 1;
1553
+ change(
1554
+ updateLayout(visibleLayout, pane.id, () => ({
1555
+ ...pane,
1556
+ tabs: [
1557
+ ...pane.tabs.slice(0, index),
1558
+ copy,
1559
+ ...pane.tabs.slice(index)
1560
+ ],
1561
+ active: copy
1562
+ }))
1563
+ );
1564
+ return copy;
1565
+ } : void 0,
1566
+ onClose: (key) => {
1567
+ if (![...dirty.current.values()].includes(key))
1568
+ return close(key);
1569
+ const tab = tabs.find((tab2) => tab2.key === key);
1570
+ const element = document.getElementById(ids.tab(key));
1571
+ Promise.resolve(
1572
+ confirmClose ? confirmClose(tab) : confirm(`Discard changes in ${tab.label}?`)
1573
+ ).then((discard) => {
1574
+ if (discard) close(key);
1575
+ else requestAnimationFrame(() => element?.focus());
1576
+ });
1577
+ },
1578
+ onReorder: (keys) => change(
1579
+ updateLayout(visibleLayout, pane.id, (node) => ({
1580
+ ...node,
1581
+ tabs: keys
1582
+ }))
1583
+ )
1584
+ }
1585
+ ),
1586
+ content: (regions, transition, lifted) => /* @__PURE__ */ jsxs5(Fragment3, { children: [
1587
+ tabs.map((tab) => {
1588
+ const region = regions.find(
1589
+ (r) => r.pane.tabs.includes(tab.key)
1590
+ );
1591
+ return /* @__PURE__ */ jsx6(
1592
+ TabPanel,
1593
+ {
1594
+ tab,
1595
+ hidden: tab.key !== region.pane.active && tab.key !== lifted,
1596
+ pane: region.pane.id,
1597
+ box: region.box,
1598
+ transition,
1599
+ focus: () => {
1600
+ preferredPane.current = region.pane.id;
1601
+ if (active !== tab.key) onSelect(tab.key);
1602
+ },
1603
+ children: /* @__PURE__ */ jsx6(
1604
+ UnsavedChanges.Provider,
1605
+ {
1606
+ value: (source, changed) => changed ? dirty.current.set(source, tab.key) : dirty.current.delete(source),
1607
+ children: children(tab, paneOf(region.pane))
1608
+ }
1609
+ )
1610
+ },
1611
+ tab.key
1612
+ );
1613
+ }),
1614
+ !tabs.length && /* @__PURE__ */ jsx6(
1615
+ TabPanel,
1616
+ {
1617
+ hidden: false,
1618
+ pane: regions[0].pane.id,
1619
+ box: regions[0].box,
1620
+ transition,
1621
+ children: empty(paneOf(regions[0].pane))
1622
+ }
1623
+ )
1624
+ ] })
1625
+ }
1626
+ )
1627
+ ]
1628
+ }
1629
+ ) });
1630
+ }
1631
+ function TabPanel({
1632
+ tab,
1633
+ hidden,
1634
+ pane,
1635
+ box,
1636
+ transition,
1637
+ focus,
1638
+ children
1639
+ }) {
1640
+ const ids = useWorkspaceIds();
1641
+ const { height, borderWidth } = useTabSize();
1642
+ const viewport = useRef5(null);
1643
+ useLayoutEffect4(() => {
1644
+ viewport.current?.scrollTo(0, 0);
1645
+ }, [tab?.scrollKey]);
1646
+ return (
1647
+ // oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- focusable panel
1648
+ /* @__PURE__ */ jsx6(
1649
+ "section",
1650
+ {
1651
+ ref: viewport,
1652
+ id: tab ? ids.panel(tab.key) : void 0,
1653
+ role: tab ? "tabpanel" : void 0,
1654
+ "aria-labelledby": tab ? ids.tab(tab.key) : void 0,
1655
+ tabIndex: 0,
1656
+ "data-pane-sheet": pane,
1657
+ hidden,
1658
+ className: "lab-panel",
1659
+ style: {
1660
+ borderWidth,
1661
+ ...tab?.fill ? { padding: 0, overflow: "hidden" } : {},
1662
+ ...position({
1663
+ ...box,
1664
+ y: box.y + height - 1,
1665
+ height: box.height - height + 1
1666
+ }),
1667
+ transitionDuration: transition ? "var(--workspace-duration, 180ms)" : "0ms"
1668
+ },
1669
+ onPointerDownCapture: (event) => {
1670
+ event.currentTarget.dataset.pointer = "";
1671
+ focus?.();
1672
+ },
1673
+ onFocusCapture: focus,
1674
+ onBlur: (event) => {
1675
+ if (!event.currentTarget.contains(event.relatedTarget))
1676
+ delete event.currentTarget.dataset.pointer;
1677
+ },
1678
+ children
1679
+ }
1680
+ )
1681
+ );
1682
+ }
1683
+
1684
+ // workspace-frame.tsx
1685
+ import {
1686
+ useLayoutEffect as useLayoutEffect5,
1687
+ useRef as useRef6,
1688
+ useState as useState5
1689
+ } from "react";
1690
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1691
+ function WorkspaceFrame({
1692
+ expanded,
1693
+ change,
1694
+ children
1695
+ }) {
1696
+ const viewport = useRef6(null);
1697
+ const drag = useRef6(void 0);
1698
+ const [available, setAvailable] = useState5(0);
1699
+ const [draft, setDraft] = useState5();
1700
+ const compact = Math.min(1600, available);
1701
+ const width = draft ?? (expanded ? available : compact);
1702
+ const cancel = () => {
1703
+ drag.current = void 0;
1704
+ setDraft(void 0);
1705
+ };
1706
+ useLayoutEffect5(() => {
1707
+ const observer = new ResizeObserver(() => {
1708
+ setAvailable(viewport.current.clientWidth);
1709
+ cancel();
1710
+ });
1711
+ observer.observe(viewport.current);
1712
+ addEventListener("blur", cancel);
1713
+ return () => {
1714
+ observer.disconnect();
1715
+ removeEventListener("blur", cancel);
1716
+ };
1717
+ }, []);
1718
+ return /* @__PURE__ */ jsx7("div", { ref: viewport, className: "lab-frame", children: /* @__PURE__ */ jsxs6(
1719
+ "div",
1720
+ {
1721
+ className: "lab-frame-content",
1722
+ style: {
1723
+ width: available ? width : "100%",
1724
+ maxWidth: expanded ? void 0 : draft ?? 1600,
1725
+ "--workspace-duration": draft === void 0 ? void 0 : "0ms"
1726
+ },
1727
+ children: [
1728
+ children,
1729
+ available > compact && ["left", "right"].map((side) => /* @__PURE__ */ jsx7(
1730
+ "div",
1731
+ {
1732
+ role: "separator",
1733
+ "aria-label": `Resize workspace from ${side}`,
1734
+ "aria-orientation": "vertical",
1735
+ "aria-valuemin": 0,
1736
+ "aria-valuemax": 100,
1737
+ "aria-valuenow": Math.round(
1738
+ (width - compact) / (available - compact) * 100
1739
+ ),
1740
+ "aria-valuetext": width === compact ? "Centered" : width === available ? "Full width" : "Resizing",
1741
+ title: "Drag outward for full width or inward to center. Double-click to toggle; arrow keys also resize.",
1742
+ tabIndex: 0,
1743
+ className: "lab-frame-handle",
1744
+ style: { [side]: -24 },
1745
+ onDoubleClick: () => change(!expanded),
1746
+ onPointerDown: (event) => {
1747
+ if (event.button !== 0) return;
1748
+ event.preventDefault();
1749
+ event.currentTarget.focus({ preventScroll: true });
1750
+ drag.current = { x: event.clientX, width };
1751
+ setDraft(width);
1752
+ event.currentTarget.setPointerCapture(event.pointerId);
1753
+ },
1754
+ onPointerMove: (event) => {
1755
+ if (!drag.current || !event.currentTarget.hasPointerCapture(event.pointerId))
1756
+ return;
1757
+ const next = drag.current.width + (event.clientX - drag.current.x) * (side === "left" ? -2 : 2);
1758
+ const snap = Math.min(32, (available - compact) / 4);
1759
+ setDraft(
1760
+ next > available - snap ? available : next < compact + snap ? compact : next
1761
+ );
1762
+ },
1763
+ onPointerUp: (event) => {
1764
+ if (!drag.current) return;
1765
+ change(width > (compact + available) / 2);
1766
+ cancel();
1767
+ event.currentTarget.releasePointerCapture(event.pointerId);
1768
+ },
1769
+ onLostPointerCapture: cancel,
1770
+ onPointerCancel: cancel,
1771
+ onKeyDown: (event) => {
1772
+ if (event.key === "Escape" && drag.current) {
1773
+ event.preventDefault();
1774
+ cancel();
1775
+ } else if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
1776
+ event.preventDefault();
1777
+ cancel();
1778
+ change(
1779
+ event.key === "End" || event.key === (side === "left" ? "ArrowLeft" : "ArrowRight")
1780
+ );
1781
+ }
1782
+ }
1783
+ },
1784
+ side
1785
+ ))
1786
+ ]
1787
+ }
1788
+ ) });
1789
+ }
1790
+
1791
+ // window.tsx
1792
+ import {
1793
+ useEffect as useEffect3,
1794
+ useLayoutEffect as useLayoutEffect6,
1795
+ useRef as useRef7,
1796
+ useState as useState6
1797
+ } from "react";
1798
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1799
+ var client = typeof window !== "undefined";
1800
+ var maximized = () => client && outerWidth >= screen.availWidth && outerHeight >= screen.availHeight;
1801
+ var isFullscreen = (host) => client && (host?.fullscreen ? innerWidth >= screen.width && innerHeight >= screen.height : !!document.fullscreenElement);
1802
+ var setFullscreen = (host, on) => {
1803
+ if (host?.fullscreen) host.fullscreen(on);
1804
+ else if (on) void document.documentElement.requestFullscreen?.();
1805
+ else if (document.fullscreenElement) void document.exitFullscreen();
1806
+ };
1807
+ var interactive = "a, button, input, select, textarea, label, summary, [contenteditable], [role=button], [role=tab], [role=separator], [role=menuitem], .lab-tab";
1808
+ var draggable = (target) => target instanceof Element && !target.closest(interactive) && (!!target.closest(".lab-toolbar, [data-window-drag]") || target.matches(".lab-workspace") || !!target.querySelector(".lab-workspace"));
1809
+ function useFullscreen(host) {
1810
+ const [full, setFull] = useState6(() => isFullscreen(host));
1811
+ useEffect3(() => {
1812
+ const update = () => setFull(isFullscreen(host));
1813
+ update();
1814
+ window.addEventListener("resize", update);
1815
+ document.addEventListener("fullscreenchange", update);
1816
+ return () => {
1817
+ window.removeEventListener("resize", update);
1818
+ document.removeEventListener("fullscreenchange", update);
1819
+ };
1820
+ }, [host]);
1821
+ return [full, (on) => setFullscreen(host, on)];
1822
+ }
1823
+ function useWindowFrame(host) {
1824
+ useEffect3(() => {
1825
+ if (!host) return;
1826
+ const root = document.documentElement;
1827
+ let armed = false;
1828
+ let cursor = "";
1829
+ const edgeAt = (x, y) => {
1830
+ if (!host.resize || maximized()) return "";
1831
+ const vertical = y < 6 ? "n" : y > innerHeight - 6 ? "s" : "";
1832
+ const horizontal = x < 6 ? "w" : x > innerWidth - 6 ? "e" : "";
1833
+ return vertical + horizontal;
1834
+ };
1835
+ const move = (event) => {
1836
+ if (armed && event.buttons === 1) {
1837
+ armed = false;
1838
+ host.drag();
1839
+ } else if (!event.buttons) {
1840
+ const edge = edgeAt(event.clientX, event.clientY);
1841
+ const next = edge && `${edge}-resize`;
1842
+ if (next !== cursor) root.style.cursor = cursor = next;
1843
+ }
1844
+ };
1845
+ const down = (event) => {
1846
+ if (event.button !== 0) return;
1847
+ const edge = edgeAt(event.clientX, event.clientY);
1848
+ if (edge) {
1849
+ event.preventDefault();
1850
+ host.resize(edge);
1851
+ } else
1852
+ armed = !!host.drag && event.detail === 1 && draggable(event.target);
1853
+ };
1854
+ const up = () => {
1855
+ armed = false;
1856
+ };
1857
+ const dblclick = (event) => {
1858
+ if (draggable(event.target)) host.toggleMaximize();
1859
+ };
1860
+ const key = (event) => {
1861
+ const shortcut = host.platform === "macos" ? event.ctrlKey && event.metaKey && event.code === "KeyF" : event.key === "F11";
1862
+ if (!host.fullscreen || !shortcut) return;
1863
+ event.preventDefault();
1864
+ host.fullscreen(!isFullscreen(host));
1865
+ };
1866
+ window.addEventListener("mousemove", move);
1867
+ window.addEventListener("mousedown", down);
1868
+ window.addEventListener("mouseup", up);
1869
+ window.addEventListener("dblclick", dblclick);
1870
+ window.addEventListener("keydown", key);
1871
+ return () => {
1872
+ window.removeEventListener("mousemove", move);
1873
+ window.removeEventListener("mousedown", down);
1874
+ window.removeEventListener("mouseup", up);
1875
+ window.removeEventListener("dblclick", dblclick);
1876
+ window.removeEventListener("keydown", key);
1877
+ if (cursor) root.style.cursor = "";
1878
+ };
1879
+ }, [host]);
1880
+ }
1881
+ var glyph = (children) => /* @__PURE__ */ jsx8(
1882
+ "svg",
1883
+ {
1884
+ "aria-hidden": "true",
1885
+ viewBox: "0 0 10 10",
1886
+ width: "10",
1887
+ height: "10",
1888
+ fill: "none",
1889
+ stroke: "currentColor",
1890
+ strokeWidth: "1",
1891
+ children
1892
+ }
1893
+ );
1894
+ function WindowControls({ host }) {
1895
+ const [full] = useFullscreen(host);
1896
+ const [max, setMax] = useState6(maximized);
1897
+ const [top, setTop] = useState6(0);
1898
+ const spacer = useRef7(null);
1899
+ const hidden = host.platform === "macos" || full;
1900
+ const measure = () => {
1901
+ setMax(maximized());
1902
+ if (spacer.current) setTop(spacer.current.getBoundingClientRect().top);
1903
+ };
1904
+ useLayoutEffect6(measure);
1905
+ useEffect3(() => {
1906
+ window.addEventListener("resize", measure);
1907
+ return () => window.removeEventListener("resize", measure);
1908
+ }, []);
1909
+ if (hidden) return null;
1910
+ const button = (label, action, icon2, close) => /* @__PURE__ */ jsx8(
1911
+ "button",
1912
+ {
1913
+ type: "button",
1914
+ tabIndex: -1,
1915
+ "aria-label": label,
1916
+ title: label,
1917
+ className: close ? "lab-window-button lab-window-close" : "lab-window-button",
1918
+ onClick: action,
1919
+ children: icon2
1920
+ }
1921
+ );
1922
+ return /* @__PURE__ */ jsx8(
1923
+ "div",
1924
+ {
1925
+ ref: spacer,
1926
+ className: "lab-window-controls",
1927
+ style: { "--lab-window-top": `${top}px` },
1928
+ children: /* @__PURE__ */ jsxs7("div", { className: "lab-window-buttons", children: [
1929
+ button(
1930
+ "Minimize",
1931
+ () => host.minimize(),
1932
+ glyph(/* @__PURE__ */ jsx8("path", { d: "M0 5.5h10" }))
1933
+ ),
1934
+ button(
1935
+ max ? "Restore" : "Maximize",
1936
+ () => host.toggleMaximize(),
1937
+ max ? glyph(
1938
+ /* @__PURE__ */ jsxs7(Fragment4, { children: [
1939
+ /* @__PURE__ */ jsx8("rect", { x: "0.5", y: "2.5", width: "7", height: "7", rx: "1" }),
1940
+ /* @__PURE__ */ jsx8("path", { d: "M2.5 2.5v-1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-1" })
1941
+ ] })
1942
+ ) : glyph(/* @__PURE__ */ jsx8("rect", { x: "0.5", y: "0.5", width: "9", height: "9", rx: "1" }))
1943
+ ),
1944
+ button(
1945
+ "Close",
1946
+ () => host.close(),
1947
+ glyph(/* @__PURE__ */ jsx8("path", { d: "m0.5 0.5 9 9m0-9-9 9" })),
1948
+ true
1949
+ )
1950
+ ] })
1951
+ }
1952
+ );
1953
+ }
1954
+ export {
1955
+ WindowControls,
1956
+ Workspace,
1957
+ WorkspaceFrame,
1958
+ densityPresets,
1959
+ densitySize,
1960
+ dockTab,
1961
+ panes,
1962
+ reconcileLayout,
1963
+ restoreLayout,
1964
+ useFullscreen,
1965
+ useUnsavedChanges,
1966
+ useWindowFrame
1967
+ };
1968
+ //# sourceMappingURL=index.js.map