@labpane/os 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -12
- package/dist/index.js +474 -154
- package/dist/index.js.map +4 -4
- package/dist/os/browser-history.d.ts +6 -0
- package/dist/os/context.d.ts +6 -0
- package/dist/os/index.d.ts +5 -1
- package/dist/os/leave-tab.d.ts +4 -0
- package/dist/os/overlay.d.ts +1 -0
- package/dist/os/tab-history.d.ts +17 -0
- package/dist/os/tab-map.d.ts +2 -0
- package/dist/os/tab-state.d.ts +10 -0
- package/dist/os/window.d.ts +15 -0
- package/dist/os/workspace.d.ts +2 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// workspace.tsx
|
|
2
2
|
import {
|
|
3
|
-
useEffect as
|
|
3
|
+
useEffect as useEffect4,
|
|
4
4
|
useId as useId2,
|
|
5
|
-
useLayoutEffect as
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
useLayoutEffect as useLayoutEffect8,
|
|
6
|
+
useMemo as useMemo2,
|
|
7
|
+
useRef as useRef9,
|
|
8
|
+
useState as useState5
|
|
8
9
|
} from "react";
|
|
9
10
|
|
|
10
11
|
// context.ts
|
|
@@ -52,9 +53,9 @@ function densitySize(density) {
|
|
|
52
53
|
const [from, to] = value < compact.value ? [dense, compact] : [compact, comfortable];
|
|
53
54
|
const amount = (value - from.value) / (to.value - from.value);
|
|
54
55
|
return Object.fromEntries(
|
|
55
|
-
Object.keys(from).map((
|
|
56
|
-
|
|
57
|
-
from[
|
|
56
|
+
Object.keys(from).map((key2) => [
|
|
57
|
+
key2,
|
|
58
|
+
from[key2] + (to[key2] - from[key2]) * amount
|
|
58
59
|
])
|
|
59
60
|
);
|
|
60
61
|
}
|
|
@@ -67,8 +68,20 @@ function useTabSize() {
|
|
|
67
68
|
function useWorkspaceIds() {
|
|
68
69
|
const prefix = useContext(WorkspaceId);
|
|
69
70
|
return {
|
|
70
|
-
tab: (
|
|
71
|
-
panel: (
|
|
71
|
+
tab: (key2) => `${prefix}-tab-${encodeURIComponent(key2)}`,
|
|
72
|
+
panel: (key2) => `${prefix}-panel-${encodeURIComponent(key2)}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
var TabLinks = createContext(/* @__PURE__ */ new Map());
|
|
76
|
+
function useTabLinks() {
|
|
77
|
+
const links = useContext(TabLinks);
|
|
78
|
+
return {
|
|
79
|
+
register: (key2) => (element) => {
|
|
80
|
+
if (element) links.set(key2, element);
|
|
81
|
+
else links.delete(key2);
|
|
82
|
+
},
|
|
83
|
+
get: (key2) => links.get(key2),
|
|
84
|
+
focus: (key2) => requestAnimationFrame(() => links.get(key2)?.focus())
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -102,14 +115,14 @@ function reconcileLayout(node, keys, active, preferred) {
|
|
|
102
115
|
return first && second ? first === node2.first && second === node2.second ? node2 : { ...node2, first, second } : first ?? second;
|
|
103
116
|
}
|
|
104
117
|
const tabs = node2.tabs.filter(
|
|
105
|
-
(
|
|
118
|
+
(key2) => keys.includes(key2) && !seen.has(key2) && !!seen.add(key2)
|
|
106
119
|
);
|
|
107
120
|
if (!tabs.length) return void 0;
|
|
108
121
|
const selected = tabs.includes(active ?? "") ? active : tabs.includes(node2.active ?? "") ? node2.active : tabs[0];
|
|
109
122
|
return tabs.length === node2.tabs.length && selected === node2.active ? node2 : { ...node2, tabs, active: selected };
|
|
110
123
|
};
|
|
111
124
|
let next = clean(node) ?? { id: "primary", tabs: [] };
|
|
112
|
-
const missing = keys.filter((
|
|
125
|
+
const missing = keys.filter((key2) => !seen.has(key2));
|
|
113
126
|
if (missing.length) {
|
|
114
127
|
const leaves = panes(next), target = leaves.find((p) => p.id === preferred) ?? leaves[0];
|
|
115
128
|
next = updateLayout(next, target.id, (node2) => ({
|
|
@@ -122,7 +135,7 @@ function reconcileLayout(node, keys, active, preferred) {
|
|
|
122
135
|
}
|
|
123
136
|
function restoreLayout(value, keys) {
|
|
124
137
|
const ids = /* @__PURE__ */ new Set();
|
|
125
|
-
function
|
|
138
|
+
function read2(value2, depth = 0) {
|
|
126
139
|
const n = value2;
|
|
127
140
|
if (!n || depth > 12 || typeof n.id !== "string" || !n.id || ids.has(n.id))
|
|
128
141
|
throw Error("Invalid layout");
|
|
@@ -134,11 +147,11 @@ function restoreLayout(value, keys) {
|
|
|
134
147
|
id: n.id,
|
|
135
148
|
axis: n.axis,
|
|
136
149
|
ratio: clamp(n.ratio, 0.05, 0.95),
|
|
137
|
-
first:
|
|
138
|
-
second:
|
|
150
|
+
first: read2(n.first, depth + 1),
|
|
151
|
+
second: read2(n.second, depth + 1)
|
|
139
152
|
};
|
|
140
153
|
}
|
|
141
|
-
if (!Array.isArray(n.tabs) || n.tabs.some((
|
|
154
|
+
if (!Array.isArray(n.tabs) || n.tabs.some((key2) => typeof key2 !== "string"))
|
|
142
155
|
throw Error("Invalid tabs");
|
|
143
156
|
return {
|
|
144
157
|
id: n.id,
|
|
@@ -147,7 +160,7 @@ function restoreLayout(value, keys) {
|
|
|
147
160
|
};
|
|
148
161
|
}
|
|
149
162
|
try {
|
|
150
|
-
return reconcileLayout(
|
|
163
|
+
return reconcileLayout(read2(JSON.parse(value ?? "null")), keys);
|
|
151
164
|
} catch {
|
|
152
165
|
return { id: "primary", tabs: keys, active: keys[0] };
|
|
153
166
|
}
|
|
@@ -156,26 +169,26 @@ var newId = () => crypto.randomUUID?.() ?? Array.from(
|
|
|
156
169
|
crypto.getRandomValues(new Uint8Array(16)),
|
|
157
170
|
(byte) => byte.toString(16).padStart(2, "0")
|
|
158
171
|
).join("");
|
|
159
|
-
function dockTab(layout,
|
|
160
|
-
const leaves = panes(layout), source = leaves.find((p) => p.tabs.includes(
|
|
172
|
+
function dockTab(layout, key2, target, edge, index) {
|
|
173
|
+
const leaves = panes(layout), source = leaves.find((p) => p.tabs.includes(key2)), destination = leaves.find((p) => p.id === target);
|
|
161
174
|
if (!source || !destination || source === destination && (!edge || source.tabs.length === 1))
|
|
162
175
|
return layout;
|
|
163
176
|
let next = updateLayout(layout, source.id, (node) => {
|
|
164
|
-
const pane = node, index2 = pane.tabs.indexOf(
|
|
177
|
+
const pane = node, index2 = pane.tabs.indexOf(key2), tabs = pane.tabs.filter((k) => k !== key2);
|
|
165
178
|
return {
|
|
166
179
|
...pane,
|
|
167
180
|
tabs,
|
|
168
|
-
active: pane.active ===
|
|
181
|
+
active: pane.active === key2 ? tabs[Math.min(index2, tabs.length - 1)] : pane.active
|
|
169
182
|
};
|
|
170
183
|
});
|
|
171
184
|
next = updateLayout(next, target, (node) => {
|
|
172
185
|
const pane = node;
|
|
173
186
|
if (!edge) {
|
|
174
187
|
const tabs = [...pane.tabs];
|
|
175
|
-
tabs.splice(index ?? tabs.length, 0,
|
|
176
|
-
return { ...pane, tabs, active:
|
|
188
|
+
tabs.splice(index ?? tabs.length, 0, key2);
|
|
189
|
+
return { ...pane, tabs, active: key2 };
|
|
177
190
|
}
|
|
178
|
-
const added = { id: newId(), tabs: [
|
|
191
|
+
const added = { id: newId(), tabs: [key2], active: key2 };
|
|
179
192
|
return {
|
|
180
193
|
id: newId(),
|
|
181
194
|
axis: edge === "bottom" ? "y" : "x",
|
|
@@ -187,7 +200,7 @@ function dockTab(layout, key, target, edge, index) {
|
|
|
187
200
|
return reconcileLayout(
|
|
188
201
|
next,
|
|
189
202
|
leaves.flatMap((p) => p.tabs),
|
|
190
|
-
|
|
203
|
+
key2
|
|
191
204
|
);
|
|
192
205
|
}
|
|
193
206
|
function minimumSize(node, gap = gutter) {
|
|
@@ -248,6 +261,13 @@ function dropRegion(box, edge, gap = gutter) {
|
|
|
248
261
|
return box;
|
|
249
262
|
}
|
|
250
263
|
|
|
264
|
+
// overlay.ts
|
|
265
|
+
var overlays = ["dialog", "alertdialog", "menu", "listbox"].map((role) => `[role="${role}"]`).concat("[data-labpane-overlay]").join(", ");
|
|
266
|
+
function hasOpenOverlay() {
|
|
267
|
+
const full = document.fullscreenElement;
|
|
268
|
+
return !!full && full !== document.documentElement || !!document.querySelector(overlays);
|
|
269
|
+
}
|
|
270
|
+
|
|
251
271
|
// surface.tsx
|
|
252
272
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
253
273
|
var position = (box) => ({
|
|
@@ -311,18 +331,18 @@ function WorkspaceSurface({
|
|
|
311
331
|
const canSplit = (box, edge) => edge === "bottom" ? box.height >= minimum.y * 2 + rowGap : box.width >= minimum.x * 2 + columnGap;
|
|
312
332
|
const docking = {
|
|
313
333
|
paneCount: leaves.length,
|
|
314
|
-
next: (
|
|
315
|
-
const index = leaves.findIndex((p) => p.pane.tabs.includes(
|
|
334
|
+
next: (key2) => {
|
|
335
|
+
const index = leaves.findIndex((p) => p.pane.tabs.includes(key2));
|
|
316
336
|
change(
|
|
317
|
-
dockTab(layout,
|
|
337
|
+
dockTab(layout, key2, leaves[(index + 1) % leaves.length].pane.id)
|
|
318
338
|
);
|
|
319
339
|
},
|
|
320
|
-
move: ({ key, x, y, detached: wasDetached }) => {
|
|
340
|
+
move: ({ key: key2, x, y, detached: wasDetached }) => {
|
|
321
341
|
const bounds = board.current.getBoundingClientRect(), localX = x - bounds.left, localY = y - bounds.top;
|
|
322
342
|
const target = leaves.find(
|
|
323
343
|
(p) => localX >= p.x && localX <= p.x + p.width && localY >= p.y && localY <= p.y + p.height
|
|
324
344
|
);
|
|
325
|
-
const source = leaves.find((p) => p.pane.tabs.includes(
|
|
345
|
+
const source = leaves.find((p) => p.pane.tabs.includes(key2));
|
|
326
346
|
const margin = wasDetached ? 8 : 24;
|
|
327
347
|
const detached = localY > source.y + tabHeight + margin || localY < source.y - margin || !!target && (localX < source.x - margin || localX > source.x + source.width + margin);
|
|
328
348
|
if (!detached) {
|
|
@@ -335,7 +355,7 @@ function WorkspaceSurface({
|
|
|
335
355
|
else if (localX < target.x + target.width * 0.28) edge = "left";
|
|
336
356
|
else if (localX > target.x + target.width * 0.72) edge = "right";
|
|
337
357
|
}
|
|
338
|
-
const valid = target && (!edge || canSplit(target, edge)) && (!target.pane.tabs.includes(
|
|
358
|
+
const valid = target && (!edge || canSplit(target, edge)) && (!target.pane.tabs.includes(key2) || !!edge && target.pane.tabs.length > 1);
|
|
339
359
|
let index;
|
|
340
360
|
if (valid && !edge) {
|
|
341
361
|
const nav = board.current.querySelector(
|
|
@@ -370,7 +390,7 @@ function WorkspaceSurface({
|
|
|
370
390
|
);
|
|
371
391
|
}
|
|
372
392
|
}
|
|
373
|
-
show(valid ? { pane: target.pane.id, key, edge, index } : { key });
|
|
393
|
+
show(valid ? { pane: target.pane.id, key: key2, edge, index } : { key: key2 });
|
|
374
394
|
return true;
|
|
375
395
|
},
|
|
376
396
|
drop: () => {
|
|
@@ -383,18 +403,18 @@ function WorkspaceSurface({
|
|
|
383
403
|
return true;
|
|
384
404
|
},
|
|
385
405
|
cancel: () => show(),
|
|
386
|
-
split: (
|
|
387
|
-
const target = leaves.find((p) => p.pane.tabs.includes(
|
|
406
|
+
split: (key2, edge) => {
|
|
407
|
+
const target = leaves.find((p) => p.pane.tabs.includes(key2));
|
|
388
408
|
if (!target || target.pane.tabs.length < 2 || !canSplit(target, edge))
|
|
389
409
|
return false;
|
|
390
|
-
change(dockTab(layout,
|
|
410
|
+
change(dockTab(layout, key2, target.pane.id, edge));
|
|
391
411
|
return true;
|
|
392
412
|
}
|
|
393
413
|
};
|
|
394
414
|
const lifted = preview?.key;
|
|
395
415
|
const regions = leaves.map(({ pane, ...box }) => {
|
|
396
416
|
if (lifted && pane.tabs.includes(lifted)) {
|
|
397
|
-
const remaining = pane.tabs.filter((
|
|
417
|
+
const remaining = pane.tabs.filter((key2) => key2 !== lifted);
|
|
398
418
|
pane = {
|
|
399
419
|
...pane,
|
|
400
420
|
active: remaining[Math.min(pane.tabs.indexOf(lifted), remaining.length - 1)]
|
|
@@ -431,6 +451,7 @@ function WorkspaceSurface({
|
|
|
431
451
|
"div",
|
|
432
452
|
{
|
|
433
453
|
"data-pane-header": pane.id,
|
|
454
|
+
"data-top": box.y === 0 || void 0,
|
|
434
455
|
className: "lab-pane-header",
|
|
435
456
|
style: {
|
|
436
457
|
...position({ ...box, height: tabHeight }),
|
|
@@ -444,8 +465,8 @@ function WorkspaceSurface({
|
|
|
444
465
|
...docking,
|
|
445
466
|
insertion: preview?.pane === pane.id ? preview.index : void 0
|
|
446
467
|
},
|
|
447
|
-
(
|
|
448
|
-
|
|
468
|
+
(key2 = pane.active) => board.current?.querySelector(
|
|
469
|
+
key2 ? `[id="${CSS.escape(ids.panel(key2))}"]` : `[data-pane-sheet="${CSS.escape(pane.id)}"]:not([hidden]):not([data-drag-content])`
|
|
449
470
|
) ?? null
|
|
450
471
|
)
|
|
451
472
|
},
|
|
@@ -499,6 +520,7 @@ function WorkspaceSurface({
|
|
|
499
520
|
},
|
|
500
521
|
onLostPointerCapture: () => setResizing(false),
|
|
501
522
|
onKeyDown: (event) => {
|
|
523
|
+
if (hasOpenOverlay()) return;
|
|
502
524
|
const step = event.shiftKey ? 0.1 : 0.02;
|
|
503
525
|
const values = {
|
|
504
526
|
[horizontal ? "ArrowLeft" : "ArrowUp"]: ratio - step,
|
|
@@ -772,6 +794,7 @@ function WorkspaceTabs({
|
|
|
772
794
|
menu
|
|
773
795
|
}) {
|
|
774
796
|
const ids = useWorkspaceIds();
|
|
797
|
+
const tabLinks = useTabLinks();
|
|
775
798
|
const content = useDragContent();
|
|
776
799
|
const density = useTabSize();
|
|
777
800
|
const [metrics, setMetrics] = React.useState({
|
|
@@ -790,8 +813,8 @@ function WorkspaceTabs({
|
|
|
790
813
|
items.length + (docking.insertion === void 0 ? 0 : 1)
|
|
791
814
|
)
|
|
792
815
|
}, { width: tabWidth, height, contentHeight } = size, width = tabWidth + 16;
|
|
793
|
-
const links = items.map(({ key, label }) => [
|
|
794
|
-
const tabOrder = JSON.stringify(items.map(({ key }) =>
|
|
816
|
+
const links = items.map(({ key: key2, label }) => [key2, label]);
|
|
817
|
+
const tabOrder = JSON.stringify(items.map(({ key: key2 }) => key2));
|
|
795
818
|
const labels = new Map(links);
|
|
796
819
|
const navRef = React.useRef(null);
|
|
797
820
|
const stripRef = React.useRef(null);
|
|
@@ -822,11 +845,11 @@ function WorkspaceTabs({
|
|
|
822
845
|
});
|
|
823
846
|
const frame = React.useRef(0);
|
|
824
847
|
const suppressClick = React.useRef(false);
|
|
825
|
-
const tabElement = (
|
|
826
|
-
`a[data-tab="${CSS.escape(
|
|
848
|
+
const tabElement = (key2) => navRef.current?.querySelector(
|
|
849
|
+
`a[data-tab="${CSS.escape(key2)}"]`
|
|
827
850
|
)?.parentElement;
|
|
828
|
-
const reveal = (
|
|
829
|
-
const nav = navRef.current, tab = tabElement(
|
|
851
|
+
const reveal = (key2 = active) => {
|
|
852
|
+
const nav = navRef.current, tab = tabElement(key2);
|
|
830
853
|
if (!nav || !tab || drag.current?.moved || docking.insertion !== void 0)
|
|
831
854
|
return;
|
|
832
855
|
nav.scrollLeft = Math.max(
|
|
@@ -908,10 +931,10 @@ function WorkspaceTabs({
|
|
|
908
931
|
motion
|
|
909
932
|
);
|
|
910
933
|
};
|
|
911
|
-
const settle = (
|
|
934
|
+
const settle = (key2) => requestAnimationFrame(() => {
|
|
912
935
|
if (drag.current) return;
|
|
913
|
-
reveal(
|
|
914
|
-
const ghost2 = ghostRef.current, slot = tabElement(
|
|
936
|
+
reveal(key2);
|
|
937
|
+
const ghost2 = ghostRef.current, slot = tabElement(key2);
|
|
915
938
|
if (!ghost2 || !slot || matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
916
939
|
setDragging(void 0);
|
|
917
940
|
return;
|
|
@@ -944,21 +967,21 @@ function WorkspaceTabs({
|
|
|
944
967
|
else setDragging(void 0);
|
|
945
968
|
};
|
|
946
969
|
const move = (from, to) => {
|
|
947
|
-
const order = links.map(([
|
|
970
|
+
const order = links.map(([key2]) => key2), index = order.indexOf(to);
|
|
948
971
|
if (from === to || index < 0) return;
|
|
949
972
|
order.splice(order.indexOf(from), 1);
|
|
950
973
|
order.splice(index, 0, from);
|
|
951
974
|
onReorder(order);
|
|
952
975
|
announce(`${labels.get(from)}: position ${index + 1} of ${order.length}`);
|
|
953
976
|
};
|
|
954
|
-
const
|
|
955
|
-
const split = (key, edge) => {
|
|
977
|
+
const split = (key2, edge) => {
|
|
956
978
|
announce(
|
|
957
|
-
docking.split(
|
|
979
|
+
docking.split(key2, edge) ? `${labels.get(key2)} split ${edge}` : "Open another tab or make this pane larger to split it"
|
|
958
980
|
);
|
|
959
|
-
|
|
981
|
+
tabLinks.focus(key2);
|
|
960
982
|
};
|
|
961
|
-
const keyDown = (event,
|
|
983
|
+
const keyDown = (event, key2, index) => {
|
|
984
|
+
if (hasOpenOverlay()) return;
|
|
962
985
|
if (event.key === "Escape") return finish(true);
|
|
963
986
|
const edges = {
|
|
964
987
|
ArrowLeft: "left",
|
|
@@ -967,9 +990,9 @@ function WorkspaceTabs({
|
|
|
967
990
|
};
|
|
968
991
|
const edge = edges[event.key];
|
|
969
992
|
if (event.altKey && edge) {
|
|
970
|
-
if (event.shiftKey) split(
|
|
993
|
+
if (event.shiftKey) split(key2, edge);
|
|
971
994
|
else if (edge !== "bottom")
|
|
972
|
-
move(
|
|
995
|
+
move(key2, items[index + (edge === "left" ? -1 : 1)]?.key ?? key2);
|
|
973
996
|
else return;
|
|
974
997
|
} else {
|
|
975
998
|
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
@@ -980,10 +1003,9 @@ function WorkspaceTabs({
|
|
|
980
1003
|
ArrowRight: (index + 1) % items.length
|
|
981
1004
|
};
|
|
982
1005
|
const target = targets[event.key];
|
|
983
|
-
if (target !== void 0)
|
|
984
|
-
|
|
985
|
-
else if (event.key === "
|
|
986
|
-
else if (event.key === " ") onSelect(key);
|
|
1006
|
+
if (target !== void 0) tabLinks.get(items[target].key)?.focus();
|
|
1007
|
+
else if (event.key === "Delete") onClose(key2);
|
|
1008
|
+
else if (event.key === " ") onSelect(key2);
|
|
987
1009
|
else return;
|
|
988
1010
|
}
|
|
989
1011
|
event.preventDefault();
|
|
@@ -1032,12 +1054,12 @@ function WorkspaceTabs({
|
|
|
1032
1054
|
const previous = positions.current;
|
|
1033
1055
|
positions.current = { order, tabs: /* @__PURE__ */ new Map() };
|
|
1034
1056
|
for (const element of navRef.current.children) {
|
|
1035
|
-
const
|
|
1036
|
-
if (!
|
|
1037
|
-
const left = element.offsetLeft, from = previous.tabs.get(
|
|
1038
|
-
if (previous.order !== order &&
|
|
1057
|
+
const key2 = element.querySelector("a")?.dataset.tab;
|
|
1058
|
+
if (!key2) continue;
|
|
1059
|
+
const left = element.offsetLeft, from = previous.tabs.get(key2);
|
|
1060
|
+
if (previous.order !== order && key2 !== dragging && from !== void 0 && from !== left)
|
|
1039
1061
|
animate(element, from - left);
|
|
1040
|
-
positions.current.tabs.set(
|
|
1062
|
+
positions.current.tabs.set(key2, left);
|
|
1041
1063
|
}
|
|
1042
1064
|
cancelAnimationFrame(frame.current);
|
|
1043
1065
|
draw();
|
|
@@ -1168,83 +1190,84 @@ function WorkspaceTabs({
|
|
|
1168
1190
|
onPointerCancel: () => finish(true),
|
|
1169
1191
|
className: "lab-tablist",
|
|
1170
1192
|
children: [
|
|
1171
|
-
links.map(([
|
|
1193
|
+
links.map(([key2, text], index) => {
|
|
1172
1194
|
const actions = [
|
|
1173
1195
|
...onDuplicate ? [
|
|
1174
1196
|
{
|
|
1175
1197
|
label: "Duplicate tab",
|
|
1176
1198
|
onSelect: () => {
|
|
1177
|
-
menuFocus.current = onDuplicate(
|
|
1199
|
+
menuFocus.current = onDuplicate(key2);
|
|
1178
1200
|
}
|
|
1179
1201
|
}
|
|
1180
1202
|
] : [],
|
|
1181
1203
|
{
|
|
1182
1204
|
label: "Move left",
|
|
1183
|
-
onSelect: () => move(
|
|
1205
|
+
onSelect: () => move(key2, items[index - 1].key),
|
|
1184
1206
|
disabled: !index
|
|
1185
1207
|
},
|
|
1186
1208
|
{
|
|
1187
1209
|
label: "Move right",
|
|
1188
|
-
onSelect: () => move(
|
|
1210
|
+
onSelect: () => move(key2, items[index + 1].key),
|
|
1189
1211
|
disabled: index === items.length - 1
|
|
1190
1212
|
},
|
|
1191
1213
|
{
|
|
1192
1214
|
label: "Move to next pane",
|
|
1193
|
-
onSelect: () => docking.next(
|
|
1215
|
+
onSelect: () => docking.next(key2),
|
|
1194
1216
|
disabled: docking.paneCount < 2
|
|
1195
1217
|
},
|
|
1196
1218
|
null,
|
|
1197
1219
|
...["left", "right", "bottom"].map((edge) => ({
|
|
1198
1220
|
label: `Split ${edge === "bottom" ? "below" : edge}`,
|
|
1199
|
-
onSelect: () => split(
|
|
1221
|
+
onSelect: () => split(key2, edge),
|
|
1200
1222
|
disabled: items.length < 2
|
|
1201
1223
|
})),
|
|
1202
1224
|
null,
|
|
1203
|
-
{ label: "Close tab", onSelect: () => onClose(
|
|
1225
|
+
{ label: "Close tab", onSelect: () => onClose(key2) }
|
|
1204
1226
|
];
|
|
1205
1227
|
const target = /* @__PURE__ */ jsxs3(
|
|
1206
1228
|
"div",
|
|
1207
1229
|
{
|
|
1208
1230
|
role: "presentation",
|
|
1209
1231
|
className: "lab-tab",
|
|
1210
|
-
"data-dragging": dragging ===
|
|
1232
|
+
"data-dragging": dragging === key2 || void 0,
|
|
1211
1233
|
style: {
|
|
1212
1234
|
paddingTop: height - contentHeight,
|
|
1213
|
-
marginRight: dragging ===
|
|
1235
|
+
marginRight: dragging === key2 && drag.current?.detached ? -tabWidth : 0,
|
|
1214
1236
|
marginLeft: docking.insertion === index ? tabWidth : 0,
|
|
1215
|
-
zIndex: active ===
|
|
1237
|
+
zIndex: active === key2 ? links.length + 1 : links.length - index
|
|
1216
1238
|
},
|
|
1217
1239
|
children: [
|
|
1218
1240
|
/* @__PURE__ */ jsx4(
|
|
1219
1241
|
TabSurface,
|
|
1220
1242
|
{
|
|
1221
1243
|
width: tabWidth,
|
|
1222
|
-
selected: active ===
|
|
1244
|
+
selected: active === key2,
|
|
1223
1245
|
keyboardFocus: !pointerFocus
|
|
1224
1246
|
}
|
|
1225
1247
|
),
|
|
1226
1248
|
/* @__PURE__ */ jsxs3(
|
|
1227
1249
|
"a",
|
|
1228
1250
|
{
|
|
1229
|
-
|
|
1251
|
+
ref: tabLinks.register(key2),
|
|
1252
|
+
id: ids.tab(key2),
|
|
1230
1253
|
role: "tab",
|
|
1231
|
-
tabIndex: active ===
|
|
1232
|
-
"aria-selected": active ===
|
|
1233
|
-
"aria-controls": ids.panel(
|
|
1254
|
+
tabIndex: active === key2 ? 0 : -1,
|
|
1255
|
+
"aria-selected": active === key2,
|
|
1256
|
+
"aria-controls": ids.panel(key2),
|
|
1234
1257
|
href: items[index].href,
|
|
1235
|
-
"data-tab":
|
|
1258
|
+
"data-tab": key2,
|
|
1236
1259
|
style: { width: tabWidth, height: contentHeight },
|
|
1237
1260
|
onClick: (event) => {
|
|
1238
1261
|
const { metaKey, ctrlKey, shiftKey, altKey } = event;
|
|
1239
1262
|
if (metaKey || ctrlKey || shiftKey || altKey) return;
|
|
1240
1263
|
event.preventDefault();
|
|
1241
|
-
onSelect(
|
|
1264
|
+
onSelect(key2);
|
|
1242
1265
|
},
|
|
1243
|
-
onFocus: () => onSelect(
|
|
1266
|
+
onFocus: () => onSelect(key2),
|
|
1244
1267
|
draggable: false,
|
|
1245
1268
|
title: text,
|
|
1246
1269
|
"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,
|
|
1270
|
+
onKeyDown: (event) => keyDown(event, key2, index),
|
|
1248
1271
|
onPointerDown: (event) => {
|
|
1249
1272
|
if (event.button !== 0 || !event.isPrimary) return;
|
|
1250
1273
|
if (event.pointerType === "mouse") event.preventDefault();
|
|
@@ -1253,7 +1276,7 @@ function WorkspaceTabs({
|
|
|
1253
1276
|
setPointerFocus(true);
|
|
1254
1277
|
const box = event.currentTarget.parentElement.getBoundingClientRect();
|
|
1255
1278
|
drag.current = {
|
|
1256
|
-
key,
|
|
1279
|
+
key: key2,
|
|
1257
1280
|
offset: event.clientX - box.left,
|
|
1258
1281
|
offsetY: event.clientY - box.top,
|
|
1259
1282
|
sheet: surface().getBoundingClientRect(),
|
|
@@ -1271,7 +1294,7 @@ function WorkspaceTabs({
|
|
|
1271
1294
|
]
|
|
1272
1295
|
}
|
|
1273
1296
|
),
|
|
1274
|
-
/* @__PURE__ */ jsx4(CloseTab, { label: text, onClick: () => onClose(
|
|
1297
|
+
/* @__PURE__ */ jsx4(CloseTab, { label: text, onClick: () => onClose(key2) })
|
|
1275
1298
|
]
|
|
1276
1299
|
}
|
|
1277
1300
|
);
|
|
@@ -1279,11 +1302,11 @@ function WorkspaceTabs({
|
|
|
1279
1302
|
target,
|
|
1280
1303
|
actions,
|
|
1281
1304
|
() => {
|
|
1282
|
-
|
|
1305
|
+
tabLinks.focus(menuFocus.current ?? key2);
|
|
1283
1306
|
menuFocus.current = void 0;
|
|
1284
1307
|
},
|
|
1285
|
-
|
|
1286
|
-
) : target },
|
|
1308
|
+
key2
|
|
1309
|
+
) : target }, key2);
|
|
1287
1310
|
}),
|
|
1288
1311
|
docking.insertion === items.length && /* @__PURE__ */ jsx4(
|
|
1289
1312
|
"div",
|
|
@@ -1420,11 +1443,226 @@ function useUnsavedChanges(dirty) {
|
|
|
1420
1443
|
}, [dirty, report]);
|
|
1421
1444
|
}
|
|
1422
1445
|
|
|
1446
|
+
// tab-state.ts
|
|
1447
|
+
import * as React3 from "react";
|
|
1448
|
+
var key = /* @__PURE__ */ Symbol.for("labpane.tab-state");
|
|
1449
|
+
var registry = globalThis;
|
|
1450
|
+
var TabStateContext = registry[key] ??= React3.createContext(
|
|
1451
|
+
{ active: true, visible: true }
|
|
1452
|
+
);
|
|
1453
|
+
function useTabActive() {
|
|
1454
|
+
return React3.useContext(TabStateContext).active;
|
|
1455
|
+
}
|
|
1456
|
+
function useTabVisible() {
|
|
1457
|
+
return React3.useContext(TabStateContext).visible;
|
|
1458
|
+
}
|
|
1459
|
+
function useTabVisibleRef() {
|
|
1460
|
+
const visible = useTabVisible();
|
|
1461
|
+
const ref = React3.useRef(visible);
|
|
1462
|
+
React3.useLayoutEffect(() => {
|
|
1463
|
+
ref.current = visible;
|
|
1464
|
+
}, [visible]);
|
|
1465
|
+
return ref;
|
|
1466
|
+
}
|
|
1467
|
+
function useShownOnce(visible) {
|
|
1468
|
+
const [shown, setShown] = React3.useState(visible);
|
|
1469
|
+
React3.useEffect(() => {
|
|
1470
|
+
if (visible) setShown(true);
|
|
1471
|
+
}, [visible]);
|
|
1472
|
+
return shown;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// leave-tab.ts
|
|
1476
|
+
import { createContext as createContext4, useContext as useContext4, useLayoutEffect as useLayoutEffect6, useRef as useRef7 } from "react";
|
|
1477
|
+
|
|
1478
|
+
// tab-map.ts
|
|
1479
|
+
import { useLayoutEffect as useLayoutEffect5, useRef as useRef6 } from "react";
|
|
1480
|
+
function useTabMap(tabs) {
|
|
1481
|
+
const map = useRef6(/* @__PURE__ */ new Map());
|
|
1482
|
+
useLayoutEffect5(() => {
|
|
1483
|
+
for (const key2 of map.current.keys())
|
|
1484
|
+
if (!tabs.some((tab) => tab.key === key2)) map.current.delete(key2);
|
|
1485
|
+
}, [tabs]);
|
|
1486
|
+
return map.current;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// leave-tab.ts
|
|
1490
|
+
function useOpeners(tabs, front) {
|
|
1491
|
+
const openers = useTabMap(tabs);
|
|
1492
|
+
const previous = useRef7({
|
|
1493
|
+
keys: /* @__PURE__ */ new Set()
|
|
1494
|
+
});
|
|
1495
|
+
useLayoutEffect6(() => {
|
|
1496
|
+
const keys = new Set(tabs.map((tab) => tab.key));
|
|
1497
|
+
const { keys: known, front: before } = previous.current;
|
|
1498
|
+
previous.current = { keys, front };
|
|
1499
|
+
if (before === void 0 || !keys.has(before)) return;
|
|
1500
|
+
for (const key2 of keys)
|
|
1501
|
+
if (key2 !== before && (!known.has(key2) || key2 === front))
|
|
1502
|
+
openers.set(key2, before);
|
|
1503
|
+
}, [tabs, front, openers]);
|
|
1504
|
+
return openers;
|
|
1505
|
+
}
|
|
1506
|
+
var LeaveTab = createContext4(() => {
|
|
1507
|
+
});
|
|
1508
|
+
function useLeaveTab() {
|
|
1509
|
+
return useContext4(LeaveTab);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// tab-history.ts
|
|
1513
|
+
import { createContext as createContext5, useContext as useContext5, useSyncExternalStore } from "react";
|
|
1514
|
+
function createTabHistory(initial) {
|
|
1515
|
+
const paths = [initial];
|
|
1516
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1517
|
+
let index = 0;
|
|
1518
|
+
const go = (to) => {
|
|
1519
|
+
index = to;
|
|
1520
|
+
current = snapshot();
|
|
1521
|
+
listeners.forEach((listener) => listener());
|
|
1522
|
+
};
|
|
1523
|
+
const navigate = (path) => {
|
|
1524
|
+
if (path === paths[index]) return;
|
|
1525
|
+
paths.splice(index + 1, Infinity, path);
|
|
1526
|
+
go(index + 1);
|
|
1527
|
+
};
|
|
1528
|
+
const back = () => {
|
|
1529
|
+
if (index > 0) go(index - 1);
|
|
1530
|
+
};
|
|
1531
|
+
const forward = () => {
|
|
1532
|
+
if (index < paths.length - 1) go(index + 1);
|
|
1533
|
+
};
|
|
1534
|
+
const snapshot = () => ({
|
|
1535
|
+
path: paths[index],
|
|
1536
|
+
canGoBack: index > 0,
|
|
1537
|
+
canGoForward: index < paths.length - 1,
|
|
1538
|
+
navigate,
|
|
1539
|
+
back,
|
|
1540
|
+
forward
|
|
1541
|
+
});
|
|
1542
|
+
let current = snapshot();
|
|
1543
|
+
return {
|
|
1544
|
+
subscribe: (listener) => {
|
|
1545
|
+
listeners.add(listener);
|
|
1546
|
+
return () => {
|
|
1547
|
+
listeners.delete(listener);
|
|
1548
|
+
};
|
|
1549
|
+
},
|
|
1550
|
+
get: () => current
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
var TabHistoryContext = createContext5(createTabHistory(""));
|
|
1554
|
+
function useTabHistory() {
|
|
1555
|
+
const store = useContext5(TabHistoryContext);
|
|
1556
|
+
return useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
1557
|
+
}
|
|
1558
|
+
function useTabHistories(tabs) {
|
|
1559
|
+
const stores = useTabMap(tabs);
|
|
1560
|
+
return (tab) => {
|
|
1561
|
+
let store = stores.get(tab.key);
|
|
1562
|
+
if (!store) stores.set(tab.key, store = createTabHistory(tab.href ?? ""));
|
|
1563
|
+
return store;
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// browser-history.ts
|
|
1568
|
+
import { useEffect as useEffect3, useLayoutEffect as useLayoutEffect7, useRef as useRef8 } from "react";
|
|
1569
|
+
var read = (state) => state?.labpane;
|
|
1570
|
+
function createBrowserHistory(front) {
|
|
1571
|
+
const session = Date.now();
|
|
1572
|
+
let serial = 0;
|
|
1573
|
+
let position2 = 0;
|
|
1574
|
+
let below = 0;
|
|
1575
|
+
let above = 0;
|
|
1576
|
+
let hopping = false;
|
|
1577
|
+
let queued = false;
|
|
1578
|
+
const mark = (serial2) => ({ labpane: { session, serial: serial2 } });
|
|
1579
|
+
const push = (path) => history.pushState(mark(++serial), "", path);
|
|
1580
|
+
const hop = () => {
|
|
1581
|
+
hopping = true;
|
|
1582
|
+
history.back();
|
|
1583
|
+
};
|
|
1584
|
+
const settle = () => {
|
|
1585
|
+
hopping = false;
|
|
1586
|
+
if (!queued) return;
|
|
1587
|
+
queued = false;
|
|
1588
|
+
sync();
|
|
1589
|
+
};
|
|
1590
|
+
const move = (to, direction) => {
|
|
1591
|
+
position2 = to;
|
|
1592
|
+
below = Math.max(0, below + direction);
|
|
1593
|
+
above = Math.max(0, above - direction);
|
|
1594
|
+
};
|
|
1595
|
+
function sync() {
|
|
1596
|
+
const tab = front();
|
|
1597
|
+
if (!tab) return;
|
|
1598
|
+
if (hopping) {
|
|
1599
|
+
queued = true;
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
history.replaceState(mark(position2), "", tab.path);
|
|
1603
|
+
if (tab.canGoBack && !below || !tab.canGoForward && above) {
|
|
1604
|
+
push(tab.path);
|
|
1605
|
+
position2 = serial;
|
|
1606
|
+
below += 1;
|
|
1607
|
+
above = 0;
|
|
1608
|
+
}
|
|
1609
|
+
if (tab.canGoForward && !above) {
|
|
1610
|
+
push(tab.path);
|
|
1611
|
+
above = 1;
|
|
1612
|
+
hop();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function pop(event) {
|
|
1616
|
+
const landed = read(event.state);
|
|
1617
|
+
if (!landed) return;
|
|
1618
|
+
if (landed.session !== session) return history.back();
|
|
1619
|
+
if (landed.serial === position2) return settle();
|
|
1620
|
+
const tab = front();
|
|
1621
|
+
if (landed.serial > position2) {
|
|
1622
|
+
if (!tab?.canGoForward) {
|
|
1623
|
+
above += 1;
|
|
1624
|
+
queued = true;
|
|
1625
|
+
return hop();
|
|
1626
|
+
}
|
|
1627
|
+
move(landed.serial, 1);
|
|
1628
|
+
tab.forward();
|
|
1629
|
+
} else {
|
|
1630
|
+
move(landed.serial, -1);
|
|
1631
|
+
if (tab?.canGoBack) tab.back();
|
|
1632
|
+
else history.back();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return { sync, pop };
|
|
1636
|
+
}
|
|
1637
|
+
function useBrowserHistory(front) {
|
|
1638
|
+
const latest = useRef8(front);
|
|
1639
|
+
useLayoutEffect7(() => {
|
|
1640
|
+
latest.current = front;
|
|
1641
|
+
});
|
|
1642
|
+
const browser = useRef8(
|
|
1643
|
+
void 0
|
|
1644
|
+
);
|
|
1645
|
+
useEffect3(() => {
|
|
1646
|
+
if (!front) return;
|
|
1647
|
+
const { sync, pop } = browser.current ??= createBrowserHistory(
|
|
1648
|
+
() => latest.current?.get()
|
|
1649
|
+
);
|
|
1650
|
+
addEventListener("popstate", pop);
|
|
1651
|
+
sync();
|
|
1652
|
+
const unsubscribe = front.subscribe(sync);
|
|
1653
|
+
return () => {
|
|
1654
|
+
unsubscribe();
|
|
1655
|
+
removeEventListener("popstate", pop);
|
|
1656
|
+
};
|
|
1657
|
+
}, [front]);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1423
1660
|
// workspace.tsx
|
|
1424
1661
|
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1425
1662
|
function Workspace(props) {
|
|
1426
1663
|
const id = useId2();
|
|
1427
|
-
|
|
1664
|
+
const [links] = useState5(() => /* @__PURE__ */ new Map());
|
|
1665
|
+
return /* @__PURE__ */ jsx6(WorkspaceId.Provider, { value: id, children: /* @__PURE__ */ jsx6(TabLinks.Provider, { value: links, children: /* @__PURE__ */ jsx6(WorkspaceDensity.Provider, { value: props.density ?? 100, children: /* @__PURE__ */ jsx6(WorkspaceContent, { ...props }) }) }) });
|
|
1428
1666
|
}
|
|
1429
1667
|
function WorkspaceContent({
|
|
1430
1668
|
density = 100,
|
|
@@ -1435,67 +1673,99 @@ function WorkspaceContent({
|
|
|
1435
1673
|
onDuplicate,
|
|
1436
1674
|
leading,
|
|
1437
1675
|
trailing,
|
|
1676
|
+
titleBar,
|
|
1438
1677
|
toolbar,
|
|
1439
1678
|
menu,
|
|
1440
1679
|
confirmClose,
|
|
1441
1680
|
layout,
|
|
1442
1681
|
onLayoutChange: change,
|
|
1682
|
+
history: history2,
|
|
1443
1683
|
className,
|
|
1444
1684
|
children,
|
|
1445
1685
|
empty
|
|
1446
1686
|
}) {
|
|
1447
|
-
const
|
|
1448
|
-
const { height, logoSize } = useTabSize();
|
|
1449
|
-
const corner2 =
|
|
1450
|
-
const [reserve, setReserve] =
|
|
1687
|
+
const links = useTabLinks();
|
|
1688
|
+
const { height, contentHeight, logoSize } = useTabSize();
|
|
1689
|
+
const corner2 = useRef9(null);
|
|
1690
|
+
const [reserve, setReserve] = useState5(0);
|
|
1451
1691
|
const hasTrailing = !!trailing;
|
|
1452
|
-
|
|
1692
|
+
useLayoutEffect8(() => {
|
|
1453
1693
|
const element = corner2.current;
|
|
1454
1694
|
if (!element) return setReserve(0);
|
|
1455
1695
|
const observer = new ResizeObserver(() => setReserve(element.offsetWidth));
|
|
1456
1696
|
observer.observe(element);
|
|
1457
1697
|
return () => observer.disconnect();
|
|
1458
1698
|
}, [hasTrailing]);
|
|
1459
|
-
const root =
|
|
1460
|
-
const
|
|
1699
|
+
const root = useRef9(null);
|
|
1700
|
+
const [inset, setInset] = useState5(0);
|
|
1701
|
+
useLayoutEffect8(() => {
|
|
1702
|
+
if (!titleBar) return setInset(0);
|
|
1703
|
+
const measure = () => setInset(root.current.getBoundingClientRect().top);
|
|
1704
|
+
measure();
|
|
1705
|
+
addEventListener("resize", measure);
|
|
1706
|
+
return () => removeEventListener("resize", measure);
|
|
1707
|
+
}, [titleBar]);
|
|
1708
|
+
const preferredPane = useRef9(void 0);
|
|
1461
1709
|
const visibleLayout = reconcileLayout(
|
|
1462
1710
|
layout,
|
|
1463
1711
|
tabs.map((t) => t.key),
|
|
1464
1712
|
active,
|
|
1465
1713
|
preferredPane.current
|
|
1466
1714
|
);
|
|
1467
|
-
|
|
1715
|
+
useLayoutEffect8(() => {
|
|
1468
1716
|
if (visibleLayout !== layout) change(visibleLayout);
|
|
1469
1717
|
}, [visibleLayout, layout]);
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1718
|
+
const front = active ?? panes(visibleLayout)[0]?.active;
|
|
1719
|
+
const openers = useOpeners(tabs, front);
|
|
1720
|
+
const historyOf = useTabHistories(tabs);
|
|
1721
|
+
const frontTab = tabs.find((tab) => tab.key === front);
|
|
1722
|
+
useBrowserHistory(history2 && frontTab ? historyOf(frontTab) : void 0);
|
|
1723
|
+
const dirty = useRef9(/* @__PURE__ */ new Map());
|
|
1724
|
+
useEffect4(() => {
|
|
1472
1725
|
const warn = (event) => {
|
|
1473
1726
|
if (dirty.current.size) event.preventDefault();
|
|
1474
1727
|
};
|
|
1475
1728
|
addEventListener("beforeunload", warn);
|
|
1476
1729
|
return () => removeEventListener("beforeunload", warn);
|
|
1477
1730
|
}, []);
|
|
1478
|
-
function close(
|
|
1479
|
-
const pane = panes(visibleLayout).find((p) => p.tabs.includes(
|
|
1731
|
+
function close(key2, next) {
|
|
1732
|
+
const pane = panes(visibleLayout).find((p) => p.tabs.includes(key2));
|
|
1480
1733
|
if (!pane) return;
|
|
1481
|
-
const remaining = pane.tabs.filter((k) => k !==
|
|
1482
|
-
const opener = tabs.find((tab) => tab.key ===
|
|
1483
|
-
const neighbor = remaining.find((
|
|
1484
|
-
onClose(
|
|
1734
|
+
const remaining = pane.tabs.filter((k) => k !== key2);
|
|
1735
|
+
const opener = tabs.find((tab) => tab.key === key2)?.opener;
|
|
1736
|
+
const neighbor = next ?? remaining.find((key3) => key3 === opener) ?? remaining[Math.min(pane.tabs.indexOf(key2), remaining.length - 1)] ?? tabs.find((t) => t.key !== key2)?.key;
|
|
1737
|
+
onClose(key2, neighbor);
|
|
1485
1738
|
requestAnimationFrame(
|
|
1486
|
-
() => (
|
|
1487
|
-
|
|
1739
|
+
() => (links.get(
|
|
1740
|
+
key2 === pane.active ? neighbor ?? "" : pane.active ?? ""
|
|
1488
1741
|
) ?? root.current?.querySelector(
|
|
1489
1742
|
".lab-toolbar-end button, [data-pane-sheet]"
|
|
1490
1743
|
))?.focus({ preventScroll: true })
|
|
1491
1744
|
);
|
|
1492
1745
|
}
|
|
1493
|
-
|
|
1746
|
+
function requestClose(key2, next) {
|
|
1747
|
+
if (![...dirty.current.values()].includes(key2)) return close(key2, next);
|
|
1748
|
+
const tab = tabs.find((tab2) => tab2.key === key2);
|
|
1749
|
+
const element = links.get(key2);
|
|
1750
|
+
Promise.resolve(
|
|
1751
|
+
confirmClose ? confirmClose(tab) : confirm(`Discard changes in ${tab.label}?`)
|
|
1752
|
+
).then((discard) => {
|
|
1753
|
+
if (discard) close(key2, next);
|
|
1754
|
+
else requestAnimationFrame(() => element?.focus());
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
function leave(key2) {
|
|
1758
|
+
const opener = openers.get(key2) ?? tabs.find((tab) => tab.key === key2)?.opener;
|
|
1759
|
+
requestClose(
|
|
1760
|
+
key2,
|
|
1761
|
+
tabs.some((tab) => tab.key === opener) ? opener : void 0
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1494
1764
|
const paneOf = (pane) => ({
|
|
1495
1765
|
id: pane.id,
|
|
1496
1766
|
tabs: pane.tabs,
|
|
1497
1767
|
active: pane.active,
|
|
1498
|
-
focus
|
|
1768
|
+
focus: links.focus
|
|
1499
1769
|
});
|
|
1500
1770
|
return /* @__PURE__ */ jsx6(Fragment3, { children: /* @__PURE__ */ jsxs5(
|
|
1501
1771
|
"div",
|
|
@@ -1504,13 +1774,16 @@ function WorkspaceContent({
|
|
|
1504
1774
|
role: "region",
|
|
1505
1775
|
"aria-label": "Workspace",
|
|
1506
1776
|
"data-density": density,
|
|
1777
|
+
"data-title-bar": titleBar || void 0,
|
|
1507
1778
|
style: {
|
|
1508
1779
|
"--lab-tab-height": `${height}px`,
|
|
1509
|
-
"--lab-
|
|
1780
|
+
"--lab-tab-content-height": `${contentHeight}px`,
|
|
1781
|
+
"--lab-logo-size": `${logoSize}px`,
|
|
1782
|
+
"--lab-title-inset": `${inset}px`
|
|
1510
1783
|
},
|
|
1511
1784
|
className: className ? `lab-workspace ${className}` : "lab-workspace",
|
|
1512
1785
|
onKeyDown: (event) => {
|
|
1513
|
-
if (event.key !== "F6") return;
|
|
1786
|
+
if (event.key !== "F6" || hasOpenOverlay()) return;
|
|
1514
1787
|
event.preventDefault();
|
|
1515
1788
|
const selected = Array.from(
|
|
1516
1789
|
event.currentTarget.querySelectorAll(
|
|
@@ -1545,11 +1818,11 @@ function WorkspaceContent({
|
|
|
1545
1818
|
},
|
|
1546
1819
|
docking,
|
|
1547
1820
|
surface,
|
|
1548
|
-
items: pane.tabs.map((
|
|
1821
|
+
items: pane.tabs.map((key2) => tabs.find((t) => t.key === key2)),
|
|
1549
1822
|
active: pane.active,
|
|
1550
1823
|
onSelect,
|
|
1551
|
-
onDuplicate: onDuplicate ? (
|
|
1552
|
-
const copy = onDuplicate(
|
|
1824
|
+
onDuplicate: onDuplicate ? (key2) => {
|
|
1825
|
+
const copy = onDuplicate(key2), index = pane.tabs.indexOf(key2) + 1;
|
|
1553
1826
|
change(
|
|
1554
1827
|
updateLayout(visibleLayout, pane.id, () => ({
|
|
1555
1828
|
...pane,
|
|
@@ -1563,18 +1836,7 @@ function WorkspaceContent({
|
|
|
1563
1836
|
);
|
|
1564
1837
|
return copy;
|
|
1565
1838
|
} : void 0,
|
|
1566
|
-
onClose:
|
|
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
|
-
},
|
|
1839
|
+
onClose: requestClose,
|
|
1578
1840
|
onReorder: (keys) => change(
|
|
1579
1841
|
updateLayout(visibleLayout, pane.id, (node) => ({
|
|
1580
1842
|
...node,
|
|
@@ -1588,11 +1850,12 @@ function WorkspaceContent({
|
|
|
1588
1850
|
const region = regions.find(
|
|
1589
1851
|
(r) => r.pane.tabs.includes(tab.key)
|
|
1590
1852
|
);
|
|
1853
|
+
const hidden = tab.key !== region.pane.active && tab.key !== lifted;
|
|
1591
1854
|
return /* @__PURE__ */ jsx6(
|
|
1592
1855
|
TabPanel,
|
|
1593
1856
|
{
|
|
1594
1857
|
tab,
|
|
1595
|
-
hidden
|
|
1858
|
+
hidden,
|
|
1596
1859
|
pane: region.pane.id,
|
|
1597
1860
|
box: region.box,
|
|
1598
1861
|
transition,
|
|
@@ -1601,10 +1864,19 @@ function WorkspaceContent({
|
|
|
1601
1864
|
if (active !== tab.key) onSelect(tab.key);
|
|
1602
1865
|
},
|
|
1603
1866
|
children: /* @__PURE__ */ jsx6(
|
|
1604
|
-
|
|
1867
|
+
TabScope,
|
|
1605
1868
|
{
|
|
1606
|
-
|
|
1607
|
-
|
|
1869
|
+
active: tab.key === front,
|
|
1870
|
+
visible: !hidden,
|
|
1871
|
+
leave: () => leave(tab.key),
|
|
1872
|
+
history: historyOf(tab),
|
|
1873
|
+
children: /* @__PURE__ */ jsx6(
|
|
1874
|
+
UnsavedChanges.Provider,
|
|
1875
|
+
{
|
|
1876
|
+
value: (source, changed) => changed ? dirty.current.set(source, tab.key) : dirty.current.delete(source),
|
|
1877
|
+
children: children(tab, paneOf(region.pane))
|
|
1878
|
+
}
|
|
1879
|
+
)
|
|
1608
1880
|
}
|
|
1609
1881
|
)
|
|
1610
1882
|
},
|
|
@@ -1628,6 +1900,16 @@ function WorkspaceContent({
|
|
|
1628
1900
|
}
|
|
1629
1901
|
) });
|
|
1630
1902
|
}
|
|
1903
|
+
function TabScope({
|
|
1904
|
+
active,
|
|
1905
|
+
visible,
|
|
1906
|
+
leave,
|
|
1907
|
+
history: history2,
|
|
1908
|
+
children
|
|
1909
|
+
}) {
|
|
1910
|
+
const state = useMemo2(() => ({ active, visible }), [active, visible]);
|
|
1911
|
+
return /* @__PURE__ */ jsx6(TabStateContext.Provider, { value: state, children: /* @__PURE__ */ jsx6(LeaveTab.Provider, { value: leave, children: /* @__PURE__ */ jsx6(TabHistoryContext.Provider, { value: history2, children }) }) });
|
|
1912
|
+
}
|
|
1631
1913
|
function TabPanel({
|
|
1632
1914
|
tab,
|
|
1633
1915
|
hidden,
|
|
@@ -1639,8 +1921,8 @@ function TabPanel({
|
|
|
1639
1921
|
}) {
|
|
1640
1922
|
const ids = useWorkspaceIds();
|
|
1641
1923
|
const { height, borderWidth } = useTabSize();
|
|
1642
|
-
const viewport =
|
|
1643
|
-
|
|
1924
|
+
const viewport = useRef9(null);
|
|
1925
|
+
useLayoutEffect8(() => {
|
|
1644
1926
|
viewport.current?.scrollTo(0, 0);
|
|
1645
1927
|
}, [tab?.scrollKey]);
|
|
1646
1928
|
return (
|
|
@@ -1683,9 +1965,9 @@ function TabPanel({
|
|
|
1683
1965
|
|
|
1684
1966
|
// workspace-frame.tsx
|
|
1685
1967
|
import {
|
|
1686
|
-
useLayoutEffect as
|
|
1687
|
-
useRef as
|
|
1688
|
-
useState as
|
|
1968
|
+
useLayoutEffect as useLayoutEffect9,
|
|
1969
|
+
useRef as useRef10,
|
|
1970
|
+
useState as useState6
|
|
1689
1971
|
} from "react";
|
|
1690
1972
|
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1691
1973
|
function WorkspaceFrame({
|
|
@@ -1693,17 +1975,17 @@ function WorkspaceFrame({
|
|
|
1693
1975
|
change,
|
|
1694
1976
|
children
|
|
1695
1977
|
}) {
|
|
1696
|
-
const viewport =
|
|
1697
|
-
const drag =
|
|
1698
|
-
const [available, setAvailable] =
|
|
1699
|
-
const [draft, setDraft] =
|
|
1978
|
+
const viewport = useRef10(null);
|
|
1979
|
+
const drag = useRef10(void 0);
|
|
1980
|
+
const [available, setAvailable] = useState6(0);
|
|
1981
|
+
const [draft, setDraft] = useState6();
|
|
1700
1982
|
const compact = Math.min(1600, available);
|
|
1701
1983
|
const width = draft ?? (expanded ? available : compact);
|
|
1702
1984
|
const cancel = () => {
|
|
1703
1985
|
drag.current = void 0;
|
|
1704
1986
|
setDraft(void 0);
|
|
1705
1987
|
};
|
|
1706
|
-
|
|
1988
|
+
useLayoutEffect9(() => {
|
|
1707
1989
|
const observer = new ResizeObserver(() => {
|
|
1708
1990
|
setAvailable(viewport.current.clientWidth);
|
|
1709
1991
|
cancel();
|
|
@@ -1790,10 +2072,10 @@ function WorkspaceFrame({
|
|
|
1790
2072
|
|
|
1791
2073
|
// window.tsx
|
|
1792
2074
|
import {
|
|
1793
|
-
useEffect as
|
|
1794
|
-
useLayoutEffect as
|
|
1795
|
-
useRef as
|
|
1796
|
-
useState as
|
|
2075
|
+
useEffect as useEffect5,
|
|
2076
|
+
useLayoutEffect as useLayoutEffect10,
|
|
2077
|
+
useRef as useRef11,
|
|
2078
|
+
useState as useState7
|
|
1797
2079
|
} from "react";
|
|
1798
2080
|
import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1799
2081
|
var client = typeof window !== "undefined";
|
|
@@ -1807,8 +2089,8 @@ var setFullscreen = (host, on) => {
|
|
|
1807
2089
|
var interactive = "a, button, input, select, textarea, label, summary, [contenteditable], [role=button], [role=tab], [role=separator], [role=menuitem], .lab-tab";
|
|
1808
2090
|
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
2091
|
function useFullscreen(host) {
|
|
1810
|
-
const [full, setFull] =
|
|
1811
|
-
|
|
2092
|
+
const [full, setFull] = useState7(() => isFullscreen(host));
|
|
2093
|
+
useEffect5(() => {
|
|
1812
2094
|
const update = () => setFull(isFullscreen(host));
|
|
1813
2095
|
update();
|
|
1814
2096
|
window.addEventListener("resize", update);
|
|
@@ -1821,7 +2103,7 @@ function useFullscreen(host) {
|
|
|
1821
2103
|
return [full, (on) => setFullscreen(host, on)];
|
|
1822
2104
|
}
|
|
1823
2105
|
function useWindowFrame(host) {
|
|
1824
|
-
|
|
2106
|
+
useEffect5(() => {
|
|
1825
2107
|
if (!host) return;
|
|
1826
2108
|
const root = document.documentElement;
|
|
1827
2109
|
let armed = false;
|
|
@@ -1857,7 +2139,7 @@ function useWindowFrame(host) {
|
|
|
1857
2139
|
const dblclick = (event) => {
|
|
1858
2140
|
if (draggable(event.target)) host.toggleMaximize();
|
|
1859
2141
|
};
|
|
1860
|
-
const
|
|
2142
|
+
const key2 = (event) => {
|
|
1861
2143
|
const shortcut = host.platform === "macos" ? event.ctrlKey && event.metaKey && event.code === "KeyF" : event.key === "F11";
|
|
1862
2144
|
if (!host.fullscreen || !shortcut) return;
|
|
1863
2145
|
event.preventDefault();
|
|
@@ -1867,13 +2149,13 @@ function useWindowFrame(host) {
|
|
|
1867
2149
|
window.addEventListener("mousedown", down);
|
|
1868
2150
|
window.addEventListener("mouseup", up);
|
|
1869
2151
|
window.addEventListener("dblclick", dblclick);
|
|
1870
|
-
window.addEventListener("keydown",
|
|
2152
|
+
window.addEventListener("keydown", key2);
|
|
1871
2153
|
return () => {
|
|
1872
2154
|
window.removeEventListener("mousemove", move);
|
|
1873
2155
|
window.removeEventListener("mousedown", down);
|
|
1874
2156
|
window.removeEventListener("mouseup", up);
|
|
1875
2157
|
window.removeEventListener("dblclick", dblclick);
|
|
1876
|
-
window.removeEventListener("keydown",
|
|
2158
|
+
window.removeEventListener("keydown", key2);
|
|
1877
2159
|
if (cursor) root.style.cursor = "";
|
|
1878
2160
|
};
|
|
1879
2161
|
}, [host]);
|
|
@@ -1891,18 +2173,47 @@ var glyph = (children) => /* @__PURE__ */ jsx8(
|
|
|
1891
2173
|
children
|
|
1892
2174
|
}
|
|
1893
2175
|
);
|
|
2176
|
+
function TrafficLights({ host }) {
|
|
2177
|
+
const [full] = useFullscreen(host);
|
|
2178
|
+
const spacer = useRef11(null);
|
|
2179
|
+
const shown = host.platform === "macos" && !full;
|
|
2180
|
+
useLayoutEffect10(() => {
|
|
2181
|
+
const element = spacer.current;
|
|
2182
|
+
const toolbar = element?.closest(".lab-toolbar");
|
|
2183
|
+
if (!element || !toolbar || !host.trafficLights) return;
|
|
2184
|
+
const place = () => {
|
|
2185
|
+
const center = toolbar.getBoundingClientRect().bottom / 2;
|
|
2186
|
+
host.trafficLights(center);
|
|
2187
|
+
const end = center * 2 + 46;
|
|
2188
|
+
const gap = parseFloat(getComputedStyle(toolbar).columnGap) || 0;
|
|
2189
|
+
element.style.width = `${end - element.getBoundingClientRect().left - gap}px`;
|
|
2190
|
+
};
|
|
2191
|
+
place();
|
|
2192
|
+
const observer = new ResizeObserver(place);
|
|
2193
|
+
observer.observe(toolbar);
|
|
2194
|
+
return () => observer.disconnect();
|
|
2195
|
+
}, [host, shown]);
|
|
2196
|
+
return shown ? /* @__PURE__ */ jsx8("div", { ref: spacer, className: "lab-traffic-lights" }) : null;
|
|
2197
|
+
}
|
|
2198
|
+
function desktopSlots(host, menu) {
|
|
2199
|
+
const place = (align) => typeof menu === "function" ? menu(align) : menu;
|
|
2200
|
+
if (!host) return { leading: place("start"), trailing: void 0 };
|
|
2201
|
+
if (host.platform === "macos")
|
|
2202
|
+
return { leading: /* @__PURE__ */ jsx8(TrafficLights, { host }), trailing: place("end") };
|
|
2203
|
+
return { leading: place("start"), trailing: /* @__PURE__ */ jsx8(WindowControls, { host }) };
|
|
2204
|
+
}
|
|
1894
2205
|
function WindowControls({ host }) {
|
|
1895
2206
|
const [full] = useFullscreen(host);
|
|
1896
|
-
const [max, setMax] =
|
|
1897
|
-
const [top, setTop] =
|
|
1898
|
-
const spacer =
|
|
2207
|
+
const [max, setMax] = useState7(maximized);
|
|
2208
|
+
const [top, setTop] = useState7(0);
|
|
2209
|
+
const spacer = useRef11(null);
|
|
1899
2210
|
const hidden = host.platform === "macos" || full;
|
|
1900
2211
|
const measure = () => {
|
|
1901
2212
|
setMax(maximized());
|
|
1902
2213
|
if (spacer.current) setTop(spacer.current.getBoundingClientRect().top);
|
|
1903
2214
|
};
|
|
1904
|
-
|
|
1905
|
-
|
|
2215
|
+
useLayoutEffect10(measure);
|
|
2216
|
+
useEffect5(() => {
|
|
1906
2217
|
window.addEventListener("resize", measure);
|
|
1907
2218
|
return () => window.removeEventListener("resize", measure);
|
|
1908
2219
|
}, []);
|
|
@@ -1952,16 +2263,25 @@ function WindowControls({ host }) {
|
|
|
1952
2263
|
);
|
|
1953
2264
|
}
|
|
1954
2265
|
export {
|
|
2266
|
+
TrafficLights,
|
|
1955
2267
|
WindowControls,
|
|
1956
2268
|
Workspace,
|
|
1957
2269
|
WorkspaceFrame,
|
|
1958
2270
|
densityPresets,
|
|
1959
2271
|
densitySize,
|
|
2272
|
+
desktopSlots,
|
|
1960
2273
|
dockTab,
|
|
2274
|
+
hasOpenOverlay,
|
|
1961
2275
|
panes,
|
|
1962
2276
|
reconcileLayout,
|
|
1963
2277
|
restoreLayout,
|
|
1964
2278
|
useFullscreen,
|
|
2279
|
+
useLeaveTab,
|
|
2280
|
+
useShownOnce,
|
|
2281
|
+
useTabActive,
|
|
2282
|
+
useTabHistory,
|
|
2283
|
+
useTabVisible,
|
|
2284
|
+
useTabVisibleRef,
|
|
1965
2285
|
useUnsavedChanges,
|
|
1966
2286
|
useWindowFrame
|
|
1967
2287
|
};
|