@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/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // workspace.tsx
2
2
  import {
3
- useEffect as useEffect2,
3
+ useEffect as useEffect4,
4
4
  useId as useId2,
5
- useLayoutEffect as useLayoutEffect4,
6
- useRef as useRef5,
7
- useState as useState4
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((key) => [
56
- key,
57
- from[key] + (to[key] - from[key]) * amount
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: (key) => `${prefix}-tab-${encodeURIComponent(key)}`,
71
- panel: (key) => `${prefix}-panel-${encodeURIComponent(key)}`
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
- (key) => keys.includes(key) && !seen.has(key) && !!seen.add(key)
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((key) => !seen.has(key));
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 read(value2, depth = 0) {
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: read(n.first, depth + 1),
138
- second: read(n.second, depth + 1)
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((key) => typeof key !== "string"))
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(read(JSON.parse(value ?? "null")), keys);
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, key, target, edge, index) {
160
- const leaves = panes(layout), source = leaves.find((p) => p.tabs.includes(key)), destination = leaves.find((p) => p.id === target);
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(key), tabs = pane.tabs.filter((k) => k !== key);
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 === key ? tabs[Math.min(index2, tabs.length - 1)] : 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, key);
176
- return { ...pane, tabs, active: key };
188
+ tabs.splice(index ?? tabs.length, 0, key2);
189
+ return { ...pane, tabs, active: key2 };
177
190
  }
178
- const added = { id: newId(), tabs: [key], active: key };
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
- key
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: (key) => {
315
- const index = leaves.findIndex((p) => p.pane.tabs.includes(key));
334
+ next: (key2) => {
335
+ const index = leaves.findIndex((p) => p.pane.tabs.includes(key2));
316
336
  change(
317
- dockTab(layout, key, leaves[(index + 1) % leaves.length].pane.id)
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(key));
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(key) || !!edge && target.pane.tabs.length > 1);
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: (key, edge) => {
387
- const target = leaves.find((p) => p.pane.tabs.includes(key));
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, key, target.pane.id, edge));
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((key) => key !== lifted);
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
- (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])`
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 }) => [key, label]);
794
- const tabOrder = JSON.stringify(items.map(({ key }) => 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 = (key) => navRef.current?.querySelector(
826
- `a[data-tab="${CSS.escape(key)}"]`
848
+ const tabElement = (key2) => navRef.current?.querySelector(
849
+ `a[data-tab="${CSS.escape(key2)}"]`
827
850
  )?.parentElement;
828
- const reveal = (key = active) => {
829
- const nav = navRef.current, tab = tabElement(key);
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 = (key) => requestAnimationFrame(() => {
934
+ const settle = (key2) => requestAnimationFrame(() => {
912
935
  if (drag.current) return;
913
- reveal(key);
914
- const ghost2 = ghostRef.current, slot = tabElement(key);
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(([key]) => key), index = order.indexOf(to);
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 focusTab = (key) => requestAnimationFrame(() => document.getElementById(ids.tab(key))?.focus());
955
- const split = (key, edge) => {
977
+ const split = (key2, edge) => {
956
978
  announce(
957
- docking.split(key, edge) ? `${labels.get(key)} split ${edge}` : "Open another tab or make this pane larger to split it"
979
+ docking.split(key2, edge) ? `${labels.get(key2)} split ${edge}` : "Open another tab or make this pane larger to split it"
958
980
  );
959
- focusTab(key);
981
+ tabLinks.focus(key2);
960
982
  };
961
- const keyDown = (event, key, index) => {
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(key, edge);
993
+ if (event.shiftKey) split(key2, edge);
971
994
  else if (edge !== "bottom")
972
- move(key, items[index + (edge === "left" ? -1 : 1)]?.key ?? key);
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
- document.getElementById(ids.tab(items[target].key))?.focus();
985
- else if (event.key === "Delete") onClose(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 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)
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(key, left);
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(([key, text], index) => {
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(key);
1199
+ menuFocus.current = onDuplicate(key2);
1178
1200
  }
1179
1201
  }
1180
1202
  ] : [],
1181
1203
  {
1182
1204
  label: "Move left",
1183
- onSelect: () => move(key, items[index - 1].key),
1205
+ onSelect: () => move(key2, items[index - 1].key),
1184
1206
  disabled: !index
1185
1207
  },
1186
1208
  {
1187
1209
  label: "Move right",
1188
- onSelect: () => move(key, items[index + 1].key),
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(key),
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(key, edge),
1221
+ onSelect: () => split(key2, edge),
1200
1222
  disabled: items.length < 2
1201
1223
  })),
1202
1224
  null,
1203
- { label: "Close tab", onSelect: () => onClose(key) }
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 === key || void 0,
1232
+ "data-dragging": dragging === key2 || void 0,
1211
1233
  style: {
1212
1234
  paddingTop: height - contentHeight,
1213
- marginRight: dragging === key && drag.current?.detached ? -tabWidth : 0,
1235
+ marginRight: dragging === key2 && drag.current?.detached ? -tabWidth : 0,
1214
1236
  marginLeft: docking.insertion === index ? tabWidth : 0,
1215
- zIndex: active === key ? links.length + 1 : links.length - index
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 === key,
1244
+ selected: active === key2,
1223
1245
  keyboardFocus: !pointerFocus
1224
1246
  }
1225
1247
  ),
1226
1248
  /* @__PURE__ */ jsxs3(
1227
1249
  "a",
1228
1250
  {
1229
- id: ids.tab(key),
1251
+ ref: tabLinks.register(key2),
1252
+ id: ids.tab(key2),
1230
1253
  role: "tab",
1231
- tabIndex: active === key ? 0 : -1,
1232
- "aria-selected": active === key,
1233
- "aria-controls": ids.panel(key),
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": key,
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(key);
1264
+ onSelect(key2);
1242
1265
  },
1243
- onFocus: () => onSelect(key),
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, key, index),
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(key) })
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
- focusTab(menuFocus.current ?? key);
1305
+ tabLinks.focus(menuFocus.current ?? key2);
1283
1306
  menuFocus.current = void 0;
1284
1307
  },
1285
- key
1286
- ) : target }, key);
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
- return /* @__PURE__ */ jsx6(WorkspaceId.Provider, { value: id, children: /* @__PURE__ */ jsx6(WorkspaceDensity.Provider, { value: props.density ?? 100, children: /* @__PURE__ */ jsx6(WorkspaceContent, { ...props }) }) });
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 ids = useWorkspaceIds();
1448
- const { height, logoSize } = useTabSize();
1449
- const corner2 = useRef5(null);
1450
- const [reserve, setReserve] = useState4(0);
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
- useLayoutEffect4(() => {
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 = useRef5(null);
1460
- const preferredPane = useRef5(void 0);
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
- useLayoutEffect4(() => {
1715
+ useLayoutEffect8(() => {
1468
1716
  if (visibleLayout !== layout) change(visibleLayout);
1469
1717
  }, [visibleLayout, layout]);
1470
- const dirty = useRef5(/* @__PURE__ */ new Map());
1471
- useEffect2(() => {
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(key) {
1479
- const pane = panes(visibleLayout).find((p) => p.tabs.includes(key));
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 !== 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);
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
- () => (document.getElementById(
1487
- ids.tab(key === pane.active ? neighbor ?? "" : pane.active ?? "")
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
- const focus = (key) => requestAnimationFrame(() => document.getElementById(ids.tab(key))?.focus());
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-logo-size": `${logoSize}px`
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((key) => tabs.find((t) => t.key === key)),
1821
+ items: pane.tabs.map((key2) => tabs.find((t) => t.key === key2)),
1549
1822
  active: pane.active,
1550
1823
  onSelect,
1551
- onDuplicate: onDuplicate ? (key) => {
1552
- const copy = onDuplicate(key), index = pane.tabs.indexOf(key) + 1;
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: (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
- },
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: tab.key !== region.pane.active && tab.key !== lifted,
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
- UnsavedChanges.Provider,
1867
+ TabScope,
1605
1868
  {
1606
- value: (source, changed) => changed ? dirty.current.set(source, tab.key) : dirty.current.delete(source),
1607
- children: children(tab, paneOf(region.pane))
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 = useRef5(null);
1643
- useLayoutEffect4(() => {
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 useLayoutEffect5,
1687
- useRef as useRef6,
1688
- useState as useState5
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 = useRef6(null);
1697
- const drag = useRef6(void 0);
1698
- const [available, setAvailable] = useState5(0);
1699
- const [draft, setDraft] = useState5();
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
- useLayoutEffect5(() => {
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 useEffect3,
1794
- useLayoutEffect as useLayoutEffect6,
1795
- useRef as useRef7,
1796
- useState as useState6
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] = useState6(() => isFullscreen(host));
1811
- useEffect3(() => {
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
- useEffect3(() => {
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 key = (event) => {
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", key);
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", key);
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] = useState6(maximized);
1897
- const [top, setTop] = useState6(0);
1898
- const spacer = useRef7(null);
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
- useLayoutEffect6(measure);
1905
- useEffect3(() => {
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
  };