@dashfoo/react 0.8.0 → 0.8.2

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,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { canRedo, canUndo, dashfooMachine, decideSnap, dragDockMachine, findTabset, fromJSON, normalize, reducer, resolveDockTarget, resolveSnapGrid, settleSnap, snapEnabled, splitEdge, stackModel, tabNodeSchema, toJSON, zoneRect } from "@dashfoo/core";
3
+ import { canRedo, canUndo, dashfooMachine, decideSnap, dragDockMachine, findTabset, fromJSON, normalize, reducer, resolveDockTarget, resolveSnapGrid, settleSnap, snapEnabled, splitEdge, tabNodeSchema, toJSON, zoneRect } from "@dashfoo/core";
4
4
  import { Fragment, createContext, createElement, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
5
  import { Accessibility, DragDropManager, Draggable, Droppable, Feedback, PointerSensor } from "@dnd-kit/dom";
6
6
  import { useActorRef, useSelector } from "@xstate/react";
@@ -178,7 +178,7 @@ const useExternalTabSource = ({ createTab, disabled = false, label = "" }) => {
178
178
  const createTabRef = useRef(createTab);
179
179
  useEffect(() => {
180
180
  createTabRef.current = createTab;
181
- });
181
+ }, [createTab]);
182
182
  const data = useMemo(() => ({
183
183
  createTab: () => createTabRef.current(),
184
184
  type: "external"
@@ -227,7 +227,8 @@ const localStorageAdapter = {
227
227
  if (typeof window === "undefined") return null;
228
228
  try {
229
229
  return window.localStorage.getItem(key);
230
- } catch {
230
+ } catch (error) {
231
+ console.warn("[dashfoo] failed to load persisted layout", error);
231
232
  return null;
232
233
  }
233
234
  },
@@ -235,7 +236,9 @@ const localStorageAdapter = {
235
236
  if (typeof window === "undefined") return;
236
237
  try {
237
238
  window.localStorage.removeItem(key);
238
- } catch {}
239
+ } catch (error) {
240
+ console.warn("[dashfoo] failed to clear persisted layout", error);
241
+ }
239
242
  },
240
243
  setItem: (key, value) => {
241
244
  if (typeof window === "undefined") return;
@@ -246,34 +249,39 @@ const localStorageAdapter = {
246
249
  }
247
250
  }
248
251
  };
252
+ const removeStoredLayout = ({ key, storage }) => {
253
+ try {
254
+ storage.removeItem(key);
255
+ } catch (error) {
256
+ console.warn("[dashfoo] failed to clear persisted layout", error);
257
+ }
258
+ };
249
259
  const usePersistence = (config, defaultModel) => {
250
260
  const configRef = useRef(config);
251
261
  useEffect(() => {
252
262
  configRef.current = config;
253
- });
254
- const [load] = useState(() => {
255
- if (config === null) return { kind: "absent" };
256
- const raw = config.storage.getItem(config.key);
257
- if (raw === null) return { kind: "absent" };
263
+ }, [config]);
264
+ const [initialModel, setInitialModel] = useState(defaultModel);
265
+ const loaded = useRef(false);
266
+ useEffect(() => {
267
+ if (loaded.current) return;
268
+ loaded.current = true;
269
+ const current = configRef.current;
270
+ if (current === null || defaultModel === void 0) return;
258
271
  try {
259
- return {
260
- kind: "loaded",
261
- model: fromJSON(raw)
262
- };
272
+ const raw = current.storage.getItem(current.key);
273
+ if (raw === null) return;
274
+ try {
275
+ const restored = fromJSON(raw);
276
+ setInitialModel(restored);
277
+ } catch (error) {
278
+ console.warn("[dashfoo] discarding unreadable persisted layout", error);
279
+ removeStoredLayout(current);
280
+ }
263
281
  } catch (error) {
264
- return {
265
- error,
266
- kind: "corrupt"
267
- };
282
+ console.warn("[dashfoo] failed to load persisted layout", error);
268
283
  }
269
- });
270
- const initialModel = load.kind === "loaded" && defaultModel !== void 0 ? load.model : defaultModel;
271
- useEffect(() => {
272
- const current = configRef.current;
273
- if (current === null || load.kind !== "corrupt") return;
274
- console.warn("[dashfoo] discarding unreadable persisted layout", load.error);
275
- current.storage.removeItem(current.key);
276
- }, [load]);
284
+ }, [defaultModel]);
277
285
  const loadedKey = useRef(config?.key);
278
286
  useEffect(() => {
279
287
  if (config !== null && config.key !== loadedKey.current) {
@@ -289,8 +297,13 @@ const usePersistence = (config, defaultModel) => {
289
297
  timer.current = null;
290
298
  }
291
299
  if (pending.current !== null) {
292
- pending.current.storage.setItem(pending.current.key, pending.current.value);
300
+ const write = pending.current;
293
301
  pending.current = null;
302
+ try {
303
+ write.storage.setItem(write.key, write.value);
304
+ } catch (error) {
305
+ console.warn("[dashfoo] failed to persist layout", error);
306
+ }
294
307
  }
295
308
  }, []);
296
309
  const save = useCallback((next) => {
@@ -326,7 +339,7 @@ const usePersistence = (config, defaultModel) => {
326
339
  }
327
340
  pending.current = null;
328
341
  const current = configRef.current;
329
- if (current !== null) current.storage.removeItem(current.key);
342
+ if (current !== null) removeStoredLayout(current);
330
343
  }, []);
331
344
  return useMemo(() => ({
332
345
  clear,
@@ -353,23 +366,30 @@ const activeBreakpoint = (breakpoints, width) => {
353
366
  if (!fallback) throw new Error("useResponsiveModel requires at least one breakpoint.");
354
367
  return breakpoints.find((breakpoint) => matchBreakpoint(breakpoint, width)) ?? fallback;
355
368
  };
369
+ const observeWidth = (element, onWidth) => {
370
+ const observer = new ResizeObserver((entries) => {
371
+ const entry = entries.at(0);
372
+ if (entry) onWidth(entry.contentRect.width);
373
+ });
374
+ observer.observe(element);
375
+ return () => {
376
+ observer.disconnect();
377
+ };
378
+ };
379
+ const listenToMedia = (list, handleChange) => {
380
+ list.addEventListener("change", handleChange);
381
+ return () => {
382
+ list.removeEventListener("change", handleChange);
383
+ };
384
+ };
356
385
  const useContainerWidth = () => {
357
386
  const [width, setWidth] = useState(Number.POSITIVE_INFINITY);
358
- const observerRef = useRef(null);
359
- const containerRef = useCallback((element) => {
360
- observerRef.current?.disconnect();
361
- if (!element) return;
362
- const observer = new ResizeObserver((entries) => {
363
- const entry = entries.at(0);
364
- if (entry) setWidth(entry.contentRect.width);
365
- });
366
- observer.observe(element);
367
- observerRef.current = observer;
368
- }, []);
369
- useEffect(() => () => {
370
- observerRef.current?.disconnect();
371
- }, []);
372
- return [containerRef, width];
387
+ const [container, setContainer] = useState(null);
388
+ useEffect(() => {
389
+ if (!container) return;
390
+ return observeWidth(container, setWidth);
391
+ }, [container]);
392
+ return [setContainer, width];
373
393
  };
374
394
  const useResponsiveModel = ({ breakpoints }) => {
375
395
  const [containerRef, width] = useContainerWidth();
@@ -381,9 +401,9 @@ const useResponsiveModel = ({ breakpoints }) => {
381
401
  const handleChange = () => {
382
402
  setMediaTick((tick) => tick + 1);
383
403
  };
384
- for (const list of lists) list.addEventListener("change", handleChange);
404
+ const cleanups = lists.map((list) => listenToMedia(list, handleChange));
385
405
  return () => {
386
- for (const list of lists) list.removeEventListener("change", handleChange);
406
+ for (const cleanup of cleanups) cleanup();
387
407
  };
388
408
  }, [breakpoints]);
389
409
  const active = activeBreakpoint(breakpoints, width);
@@ -513,6 +533,26 @@ const useDashfooStore = (options) => {
513
533
  ]);
514
534
  };
515
535
 
536
+ //#endregion
537
+ //#region src/lib/compact-model.ts
538
+ const compactRow = (node, orientation) => {
539
+ const children = node.children.map((child) => child.type === "row" ? compactRow(child, orientation) : {
540
+ ...child,
541
+ weight: 1
542
+ });
543
+ return {
544
+ ...node,
545
+ children,
546
+ orientation,
547
+ weight: children.reduce((sum, child) => sum + child.weight, 0)
548
+ };
549
+ };
550
+ const compactModel = (model, orientation = "column") => ({
551
+ ...model,
552
+ layout: compactRow(model.layout, orientation),
553
+ maximizedTabsetId: void 0
554
+ });
555
+
516
556
  //#endregion
517
557
  //#region src/lib/drag-subject.ts
518
558
  const isTabFactory = (value) => typeof value === "function";
@@ -520,11 +560,11 @@ const isTabFactory = (value) => typeof value === "function";
520
560
  * gesture on the shared manager, above any one layer. */
521
561
  const subjectFor = (source) => {
522
562
  const data = source.data;
523
- if (data?.type === "tabset") return {
563
+ if (data.type === "tabset") return {
524
564
  id: String(data.tabsetId),
525
565
  kind: "tabset"
526
566
  };
527
- if (data?.type === "external") {
567
+ if (data.type === "external") {
528
568
  if (!isTabFactory(data.createTab)) {
529
569
  console.warn("[dashfoo] external drag source is missing its createTab function");
530
570
  return null;
@@ -637,13 +677,13 @@ const PREVIEW_OFFSET = {
637
677
  y: 8
638
678
  };
639
679
  const labelOf = (source) => {
640
- const raw = source?.data?.label;
641
- return typeof raw === "string" ? raw : "";
680
+ if (typeof source?.data.label !== "string") return "";
681
+ return source.data.label;
642
682
  };
643
683
  const DragPreviewOverlay = ({ manager }) => {
644
684
  const [chip, setChip] = useState(null);
645
685
  useEffect(() => {
646
- const offStart = manager.monitor.addEventListener("dragstart", (event) => {
686
+ const handleStart = (event) => {
647
687
  const source = event.operation.source;
648
688
  if (!source) return;
649
689
  const rect = source.element?.getBoundingClientRect();
@@ -653,13 +693,15 @@ const DragPreviewOverlay = ({ manager }) => {
653
693
  x: PREVIEW_OFFSET.x + (rect ? point.x - rect.left : 0),
654
694
  y: PREVIEW_OFFSET.y + (rect ? point.y - rect.top : 0)
655
695
  });
656
- });
657
- const offEnd = manager.monitor.addEventListener("dragend", () => {
696
+ };
697
+ const handleEnd = () => {
658
698
  setChip(null);
659
- });
699
+ };
700
+ manager.monitor.addEventListener("dragstart", handleStart);
701
+ manager.monitor.addEventListener("dragend", handleEnd);
660
702
  return () => {
661
- offStart();
662
- offEnd();
703
+ manager.monitor.removeEventListener("dragstart", handleStart);
704
+ manager.monitor.removeEventListener("dragend", handleEnd);
663
705
  };
664
706
  }, [manager]);
665
707
  const attachOverlay = useCallback((element) => {
@@ -685,6 +727,7 @@ const DragPreviewOverlay = ({ manager }) => {
685
727
 
686
728
  //#endregion
687
729
  //#region src/components/dashfoo-drag-provider.tsx
730
+ const isScopedTargetData = (data) => typeof data.layerId === "string" && typeof data.tabsetId === "string";
688
731
  const DashfooDragProvider = ({ children }) => {
689
732
  const [manager] = useState(createDragManager);
690
733
  useInsertionEffect(() => () => {
@@ -702,9 +745,9 @@ const DashfooDragProvider = ({ children }) => {
702
745
  const scopedTarget = () => {
703
746
  const target = manager.dragOperation.target;
704
747
  const data = target?.data;
705
- const scope = data?.layerId === void 0 ? void 0 : scopes.get(data.layerId);
748
+ const scope = data && isScopedTargetData(data) ? scopes.get(data.layerId) : void 0;
706
749
  const element = target?.element;
707
- if (!scope || data?.tabsetId === void 0 || !(element instanceof HTMLElement)) return null;
750
+ if (!scope || !data || !isScopedTargetData(data) || !(element instanceof HTMLElement)) return null;
708
751
  return {
709
752
  element,
710
753
  scope,
@@ -763,17 +806,17 @@ const DashfooDragProvider = ({ children }) => {
763
806
  const commitSubscription = actorRef.on("COMMIT", (emitted) => {
764
807
  scopes.get(emitted.scope)?.commit(emitted.action);
765
808
  });
766
- const offStart = manager.monitor.addEventListener("dragstart", handleStart);
767
- const offMove = manager.monitor.addEventListener("dragmove", scheduleResolve);
768
- const offCollision = manager.monitor.addEventListener("collision", scheduleResolve);
769
- const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
809
+ manager.monitor.addEventListener("dragstart", handleStart);
810
+ manager.monitor.addEventListener("dragmove", scheduleResolve);
811
+ manager.monitor.addEventListener("collision", scheduleResolve);
812
+ manager.monitor.addEventListener("dragend", handleEnd);
770
813
  return () => {
771
814
  cancelPendingResolve();
772
815
  commitSubscription.unsubscribe();
773
- offStart();
774
- offMove();
775
- offCollision();
776
- offEnd();
816
+ manager.monitor.removeEventListener("dragstart", handleStart);
817
+ manager.monitor.removeEventListener("dragmove", scheduleResolve);
818
+ manager.monitor.removeEventListener("collision", scheduleResolve);
819
+ manager.monitor.removeEventListener("dragend", handleEnd);
777
820
  };
778
821
  }, [
779
822
  actorRef,
@@ -1102,16 +1145,29 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
1102
1145
  if (prior && panel.hasPointerCapture?.(prior.pointerId)) panel.releasePointerCapture(prior.pointerId);
1103
1146
  const edgeKey = event.currentTarget.dataset.edge;
1104
1147
  const parent = panel.offsetParent instanceof HTMLElement ? panel.offsetParent : null;
1148
+ const rect = panel.getBoundingClientRect();
1149
+ const visibleGeometry = {
1150
+ height: minimized ? geometry.height : rect.height,
1151
+ left: rect.left,
1152
+ top: rect.top,
1153
+ width: minimized ? geometry.width : rect.width
1154
+ };
1105
1155
  const gesture = {
1106
1156
  bounds: {
1107
1157
  height: parent?.clientHeight ?? 0,
1108
1158
  width: parent?.clientWidth ?? 0
1109
1159
  },
1110
1160
  edges: edgeKey === void 0 ? null : EDGE_BY_KEY.get(edgeKey) ?? null,
1111
- latest: geometry,
1161
+ initialStyle: {
1162
+ height: panel.style.height,
1163
+ left: panel.style.left,
1164
+ top: panel.style.top,
1165
+ width: panel.style.width
1166
+ },
1167
+ latest: visibleGeometry,
1112
1168
  moved: false,
1113
1169
  pointerId: event.pointerId,
1114
- start: geometry,
1170
+ start: visibleGeometry,
1115
1171
  startX: event.clientX,
1116
1172
  startY: event.clientY
1117
1173
  };
@@ -1165,12 +1221,7 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
1165
1221
  gestureRef.current = null;
1166
1222
  if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
1167
1223
  if (!gesture.moved) return;
1168
- panel.style.left = `${gesture.start.left}px`;
1169
- panel.style.top = `${gesture.start.top}px`;
1170
- if (!minimized) {
1171
- panel.style.width = `${gesture.start.width}px`;
1172
- panel.style.height = `${gesture.start.height}px`;
1173
- }
1224
+ Object.assign(panel.style, gesture.initialStyle);
1174
1225
  };
1175
1226
  return {
1176
1227
  handlers: {
@@ -1180,14 +1231,11 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
1180
1231
  onPointerUp: handlePointerUp
1181
1232
  },
1182
1233
  ref: setPanel,
1183
- style: minimized ? {
1184
- left: geometry.left,
1185
- top: geometry.top
1186
- } : {
1187
- height: geometry.height,
1188
- left: geometry.left,
1189
- top: geometry.top,
1190
- width: geometry.width
1234
+ style: {
1235
+ height: minimized ? void 0 : `min(${geometry.height}px, 100dvh)`,
1236
+ left: `clamp(0px, ${geometry.left}px, calc(100vw - min(${minimized ? CHIP_SIZE.width : geometry.width}px, 100vw)))`,
1237
+ top: `clamp(0px, ${geometry.top}px, calc(100dvh - min(${minimized ? CHIP_SIZE.height : geometry.height}px, 100dvh)))`,
1238
+ width: minimized ? void 0 : `min(${geometry.width}px, 100vw)`
1191
1239
  }
1192
1240
  };
1193
1241
  };
@@ -1323,23 +1371,39 @@ const rootStyle = {
1323
1371
  position: "relative",
1324
1372
  width: "100%"
1325
1373
  };
1326
- const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = true, draggableTabsets = true, editable = true, floatable = false, keepMounted = false, maximizable = true, model, renamableTabs = true, renderTab, renderTabLabel, renderTabsetToolbar, resizableSplits = true, restructurable = true, rootRef, snap, style, ...props }) => {
1374
+ const resolveCapabilities = (global, options) => {
1375
+ const structural = options.editable && options.restructurable;
1376
+ return {
1377
+ closableTabs: options.editable && options.closableTabs && global.tabEnableClose !== false,
1378
+ draggableTabs: structural && options.draggableTabs && global.tabEnableDrag !== false,
1379
+ draggableTabsets: structural && options.draggableTabsets,
1380
+ floatable: structural && options.floatable,
1381
+ maximizable: options.maximizable && global.tabSetEnableMaximize !== false,
1382
+ renamableTabs: options.editable && options.renamableTabs && global.tabEnableRename !== false,
1383
+ resizableSplits: structural && options.resizableSplits && global.enableSplitResize !== false
1384
+ };
1385
+ };
1386
+ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = true, draggableTabsets = true, editable = true, floatable = false, keepMounted = false, maximizable = true, model, renamableTabs = true, renderers, resizableSplits = true, restructurable = true, rootRef, snap, style, ...props }) => {
1327
1387
  const global = model.global;
1328
1388
  const snapshot = {
1329
- closableTabs: editable && closableTabs && global.tabEnableClose !== false,
1389
+ ...resolveCapabilities(global, {
1390
+ closableTabs,
1391
+ draggableTabs,
1392
+ draggableTabsets,
1393
+ editable,
1394
+ floatable,
1395
+ maximizable,
1396
+ renamableTabs,
1397
+ resizableSplits,
1398
+ restructurable
1399
+ }),
1330
1400
  dispatch,
1331
- draggableTabs: editable && restructurable && draggableTabs && global.tabEnableDrag !== false,
1332
- draggableTabsets: editable && restructurable && draggableTabsets,
1333
1401
  editable,
1334
- floatable: editable && restructurable && floatable,
1335
1402
  keepMounted,
1336
- maximizable: maximizable && global.tabSetEnableMaximize !== false,
1337
1403
  maximizedTabsetId: model.maximizedTabsetId,
1338
- renamableTabs: editable && renamableTabs && global.tabEnableRename !== false,
1339
- renderTab,
1340
- renderTabLabel,
1341
- renderTabsetToolbar,
1342
- resizableSplits: editable && restructurable && resizableSplits && global.enableSplitResize !== false,
1404
+ renderTab: renderers.tab,
1405
+ renderTabLabel: renderers.tabLabel,
1406
+ renderTabsetToolbar: renderers.tabsetToolbar,
1343
1407
  snap: snap ?? global.snap ?? null,
1344
1408
  splitDock: global.enableSplitDock !== false,
1345
1409
  tabLocation: global.tabLocation ?? "top",
@@ -1350,14 +1414,11 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
1350
1414
  useLayoutEffect(() => {
1351
1415
  store.setState(snapshot);
1352
1416
  });
1353
- const layoutStyle = global.splitterSize === void 0 ? {
1354
- ...rootStyle,
1355
- ...style
1356
- } : {
1417
+ const layoutStyle = {
1357
1418
  ...rootStyle,
1358
- "--dashfoo-splitter-size": `${global.splitterSize}px`,
1359
1419
  ...style
1360
1420
  };
1421
+ if (global.splitterSize !== void 0) layoutStyle["--dashfoo-splitter-size"] = `${global.splitterSize}px`;
1361
1422
  return /* @__PURE__ */ jsx(LayoutStoreContext.Provider, {
1362
1423
  value: store,
1363
1424
  children: /* @__PURE__ */ jsx("div", {
@@ -1617,8 +1678,9 @@ const TabPanel = ({ render, tab }) => {
1617
1678
  const renderTab = useLayout((state) => state.renderTab);
1618
1679
  return render ? render(tab) : renderTab(tab);
1619
1680
  };
1620
- const TabsetContent = ({ children, style, ...props }) => {
1681
+ const TabsetContent = forwardRef(({ children, style, ...props }, ref) => {
1621
1682
  const node = useTabset((state) => state.node);
1683
+ const editingTabId = useTabset((state) => state.editingTabId);
1622
1684
  const visualSelected = useTabset((state) => state.visualSelected);
1623
1685
  const keepMounted = useLayout((state) => state.keepMounted);
1624
1686
  const mergedStyle = {
@@ -1627,11 +1689,13 @@ const TabsetContent = ({ children, style, ...props }) => {
1627
1689
  };
1628
1690
  if (keepMounted && node.children.length > 0) return node.children.map((tab, index) => /* @__PURE__ */ createElement("div", {
1629
1691
  ...props,
1630
- "aria-labelledby": tabDomId(node.id, tab.id),
1692
+ "aria-label": editingTabId === tab.id ? tab.name : void 0,
1693
+ "aria-labelledby": editingTabId === tab.id ? void 0 : tabDomId(node.id, tab.id),
1631
1694
  "data-dashfoo": "tabcontent",
1632
1695
  hidden: index !== visualSelected || void 0,
1633
1696
  id: index === visualSelected ? panelDomId(node.id) : void 0,
1634
1697
  key: tab.id,
1698
+ ref: index === visualSelected ? ref : void 0,
1635
1699
  role: index === visualSelected ? "tabpanel" : void 0,
1636
1700
  style: mergedStyle,
1637
1701
  tabIndex: index === visualSelected ? 0 : void 0
@@ -1642,9 +1706,11 @@ const TabsetContent = ({ children, style, ...props }) => {
1642
1706
  const active = node.children[visualSelected];
1643
1707
  if (active) return /* @__PURE__ */ jsx("div", {
1644
1708
  ...props,
1645
- "aria-labelledby": tabDomId(node.id, active.id),
1709
+ "aria-label": editingTabId === active.id ? active.name : void 0,
1710
+ "aria-labelledby": editingTabId === active.id ? void 0 : tabDomId(node.id, active.id),
1646
1711
  "data-dashfoo": "tabcontent",
1647
1712
  id: panelDomId(node.id),
1713
+ ref,
1648
1714
  role: "tabpanel",
1649
1715
  style: mergedStyle,
1650
1716
  tabIndex: 0,
@@ -1656,9 +1722,11 @@ const TabsetContent = ({ children, style, ...props }) => {
1656
1722
  return /* @__PURE__ */ jsx("div", {
1657
1723
  ...props,
1658
1724
  "data-dashfoo": "tabcontent",
1725
+ ref,
1659
1726
  style: mergedStyle
1660
1727
  });
1661
- };
1728
+ });
1729
+ TabsetContent.displayName = "TabsetContent";
1662
1730
 
1663
1731
  //#endregion
1664
1732
  //#region src/components/tab-overflow-menu.tsx
@@ -1817,7 +1885,7 @@ const mergeRefs = (...refs) => (value) => {
1817
1885
 
1818
1886
  //#endregion
1819
1887
  //#region src/components/tabset/tabset-rename-input.tsx
1820
- const RenameEditor = ({ defaultValue, onBlur, onKeyDown, ref: userRef, ...props }) => {
1888
+ const RenameEditor = forwardRef(({ defaultValue, onBlur, onKeyDown, ...props }, userRef) => {
1821
1889
  const { tab } = useTab();
1822
1890
  const cancelRename = useTabset((state) => state.cancelRename);
1823
1891
  const commitRename = useTabset((state) => state.commitRename);
@@ -1849,16 +1917,21 @@ const RenameEditor = ({ defaultValue, onBlur, onKeyDown, ref: userRef, ...props
1849
1917
  ref: refCallback,
1850
1918
  type: "text"
1851
1919
  });
1852
- };
1853
- const TabsetRenameInput = (props) => {
1920
+ });
1921
+ RenameEditor.displayName = "RenameEditor";
1922
+ const TabsetRenameInput = forwardRef((props, ref) => {
1854
1923
  const { tab } = useTab();
1855
1924
  const editing = useTabset((state) => state.editingTabId === tab.id);
1856
1925
  const registerRenameInput = useTabset((state) => state.registerRenameInput);
1857
1926
  useEffect(() => registerRenameInput(), [registerRenameInput]);
1858
1927
  if (!editing) return null;
1859
- return /* @__PURE__ */ jsx(RenameEditor, { ...props });
1860
- };
1861
- const TabsetCloseButton = ({ children, onClick, ...props }) => {
1928
+ return /* @__PURE__ */ jsx(RenameEditor, {
1929
+ ...props,
1930
+ ref
1931
+ });
1932
+ });
1933
+ TabsetRenameInput.displayName = "TabsetRenameInput";
1934
+ const TabsetCloseButton = forwardRef(({ children, onClick, ...props }, ref) => {
1862
1935
  const { index, tab } = useTab();
1863
1936
  const closeTab = useTabset((state) => state.closeTab);
1864
1937
  const editing = useTabset((state) => state.editingTabId === tab.id);
@@ -1876,11 +1949,13 @@ const TabsetCloseButton = ({ children, onClick, ...props }) => {
1876
1949
  ...props,
1877
1950
  "data-dashfoo": "tab-close",
1878
1951
  onClick: handleClick,
1952
+ ref,
1879
1953
  tabIndex: index === visualSelected ? 0 : -1,
1880
1954
  type: "button",
1881
1955
  children: children ?? /* @__PURE__ */ jsx(CloseIcon, {})
1882
1956
  });
1883
- };
1957
+ });
1958
+ TabsetCloseButton.displayName = "TabsetCloseButton";
1884
1959
 
1885
1960
  //#endregion
1886
1961
  //#region src/hooks/use-tab-overflow.ts
@@ -1930,7 +2005,7 @@ const tabsetStyle = {
1930
2005
  minWidth: 0,
1931
2006
  width: "100%"
1932
2007
  };
1933
- const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
2008
+ const TabsetRoot = forwardRef(({ children, node, style, ...props }, userRef) => {
1934
2009
  const closableTabs = useLayout((state) => state.closableTabs);
1935
2010
  const dispatch = useLayout((state) => state.dispatch);
1936
2011
  const maximizable = useLayout((state) => state.maximizable);
@@ -2010,11 +2085,12 @@ const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
2010
2085
  children
2011
2086
  })
2012
2087
  });
2013
- };
2088
+ });
2089
+ TabsetRoot.displayName = "TabsetRoot";
2014
2090
 
2015
2091
  //#endregion
2016
2092
  //#region src/components/tabset/tabset-tab.tsx
2017
- const TabsetTab = ({ children, ref: userRef, tab, ...props }) => {
2093
+ const TabsetTab = forwardRef(({ children, tab, ...props }, userRef) => {
2018
2094
  const store = useContext(TabsetStoreContext);
2019
2095
  const node = useTabset((state) => state.node);
2020
2096
  const editing = useTabset((state) => state.editingTabId === tab.id);
@@ -2049,8 +2125,9 @@ const TabsetTab = ({ children, ref: userRef, tab, ...props }) => {
2049
2125
  children
2050
2126
  })
2051
2127
  });
2052
- };
2053
- const TabsetTrigger = ({ children, onClick, onDoubleClick, ref: userRef, ...props }) => {
2128
+ });
2129
+ TabsetTab.displayName = "TabsetTab";
2130
+ const TabsetTrigger = forwardRef(({ children, onClick, onDoubleClick, ...props }, userRef) => {
2054
2131
  const { index, tab } = useTab();
2055
2132
  const node = useTabset((state) => state.node);
2056
2133
  const editing = useTabset((state) => state.editingTabId === tab.id);
@@ -2089,7 +2166,8 @@ const TabsetTrigger = ({ children, onClick, onDoubleClick, ref: userRef, ...prop
2089
2166
  type: "button",
2090
2167
  children: children ?? (renderTabLabel ? renderTabLabel(tab) : tab.name)
2091
2168
  });
2092
- };
2169
+ });
2170
+ TabsetTrigger.displayName = "TabsetTrigger";
2093
2171
 
2094
2172
  //#endregion
2095
2173
  //#region src/components/tabset/tabset-tablist.tsx
@@ -2102,18 +2180,21 @@ const tablistStyle = {
2102
2180
  minWidth: 0,
2103
2181
  overflowX: "auto"
2104
2182
  };
2105
- const TabsetTabStrip = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
2183
+ const TabsetTabStrip = forwardRef(({ style, ...props }, ref) => /* @__PURE__ */ jsx("div", {
2106
2184
  ...props,
2107
2185
  "data-dashfoo": "tabstrip",
2186
+ ref,
2108
2187
  style: {
2109
2188
  ...stripStyle,
2110
2189
  ...style
2111
2190
  }
2112
- });
2113
- const TabsetTablist = ({ onKeyDown, ref: userRef, style, ...props }) => {
2191
+ }));
2192
+ TabsetTabStrip.displayName = "TabsetTabStrip";
2193
+ const TabsetTablist = forwardRef(({ "aria-label": ariaLabel, children, onKeyDown, style, ...props }, userRef) => {
2114
2194
  const node = useTabset((state) => state.node);
2115
2195
  const registerTablist = useTabset((state) => state.registerTablist);
2116
2196
  const selectTab = useTabset((state) => state.selectTab);
2197
+ const editingTabId = useTabset((state) => state.editingTabId);
2117
2198
  const visualSelected = useTabset((state) => state.visualSelected);
2118
2199
  const handleKeyDown = (event) => {
2119
2200
  onKeyDown?.(event);
@@ -2122,32 +2203,37 @@ const TabsetTablist = ({ onKeyDown, ref: userRef, style, ...props }) => {
2122
2203
  const count = node.children.length;
2123
2204
  if (count === 0) return;
2124
2205
  const from = Math.max(visualSelected, 0);
2125
- const next = {
2126
- ArrowLeft: (from - 1 + count) % count,
2127
- ArrowRight: (from + 1) % count,
2128
- End: count - 1,
2129
- Home: 0
2130
- }[event.key];
2206
+ const next = (/* @__PURE__ */ new Map([
2207
+ ["ArrowLeft", (from - 1 + count) % count],
2208
+ ["ArrowRight", (from + 1) % count],
2209
+ ["End", count - 1],
2210
+ ["Home", 0]
2211
+ ])).get(event.key);
2131
2212
  if (next === void 0) return;
2132
2213
  event.preventDefault();
2133
2214
  selectTab(next, { focus: true });
2134
2215
  };
2135
2216
  const refCallback = useMemo(() => mergeRefs(registerTablist, userRef), [registerTablist, userRef]);
2136
- return /* @__PURE__ */ jsx("div", {
2137
- "aria-label": node.name ?? "Tabs",
2217
+ const ownedTabs = node.children.filter((tab) => tab.id !== editingTabId).map((tab) => tabDomId(node.id, tab.id)).join(" ");
2218
+ return /* @__PURE__ */ jsxs("div", {
2138
2219
  ...props,
2139
- "aria-orientation": "horizontal",
2140
2220
  "data-dashfoo": "tablist",
2141
2221
  onKeyDown: handleKeyDown,
2142
2222
  ref: refCallback,
2143
- role: "tablist",
2144
2223
  style: {
2145
2224
  ...tablistStyle,
2146
2225
  ...style
2147
2226
  },
2148
- tabIndex: -1
2227
+ tabIndex: -1,
2228
+ children: [ownedTabs ? /* @__PURE__ */ jsx("span", {
2229
+ "aria-label": ariaLabel ?? node.name ?? "Tabs",
2230
+ "aria-orientation": "horizontal",
2231
+ "aria-owns": ownedTabs,
2232
+ role: "tablist"
2233
+ }) : null, children]
2149
2234
  });
2150
- };
2235
+ });
2236
+ TabsetTablist.displayName = "TabsetTablist";
2151
2237
 
2152
2238
  //#endregion
2153
2239
  //#region src/components/tabset/tabset-toolbar.tsx
@@ -2157,14 +2243,16 @@ const toolbarStyle = {
2157
2243
  flexShrink: 0,
2158
2244
  marginInlineStart: "auto"
2159
2245
  };
2160
- const TabsetToolbar = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
2246
+ const TabsetToolbar = forwardRef(({ style, ...props }, ref) => /* @__PURE__ */ jsx("div", {
2161
2247
  ...props,
2162
2248
  "data-dashfoo": "tabset-toolbar",
2249
+ ref,
2163
2250
  style: {
2164
2251
  ...toolbarStyle,
2165
2252
  ...style
2166
2253
  }
2167
- });
2254
+ }));
2255
+ TabsetToolbar.displayName = "TabsetToolbar";
2168
2256
  /**
2169
2257
  * One answer per control, shared by the controls and by the layout deciding
2170
2258
  * whether to render a toolbar at all, so the two cannot disagree.
@@ -2184,7 +2272,7 @@ const useTabsetChrome = () => {
2184
2272
  overflow: overflowCount > 0
2185
2273
  };
2186
2274
  };
2187
- const TabsetGrip = ({ children, ref: userRef, ...props }) => {
2275
+ const TabsetGrip = forwardRef(({ children, ...props }, userRef) => {
2188
2276
  const activeTabName = useTabset((state) => state.activeTab?.name ?? "");
2189
2277
  const node = useTabset((state) => state.node);
2190
2278
  const { grip } = useTabsetChrome();
@@ -2200,8 +2288,9 @@ const TabsetGrip = ({ children, ref: userRef, ...props }) => {
2200
2288
  type: "button",
2201
2289
  children: children ?? /* @__PURE__ */ jsx(GripIcon, {})
2202
2290
  });
2203
- };
2204
- const TabsetMaximizeButton = ({ children, onClick, ...props }) => {
2291
+ });
2292
+ TabsetGrip.displayName = "TabsetGrip";
2293
+ const TabsetMaximizeButton = forwardRef(({ children, onClick, ...props }, ref) => {
2205
2294
  const isMaximized = useTabset((state) => state.isMaximized);
2206
2295
  const toggleMaximize = useTabset((state) => state.toggleMaximize);
2207
2296
  const { maximize } = useTabsetChrome();
@@ -2217,11 +2306,13 @@ const TabsetMaximizeButton = ({ children, onClick, ...props }) => {
2217
2306
  "aria-pressed": isMaximized,
2218
2307
  "data-dashfoo": "tabset-maximize",
2219
2308
  onClick: handleClick,
2309
+ ref,
2220
2310
  type: "button",
2221
2311
  children: children ?? /* @__PURE__ */ jsx(MaximizeIcon, { maximized: isMaximized })
2222
2312
  });
2223
- };
2224
- const TabsetFloatButton = ({ children, onClick, ...props }) => {
2313
+ });
2314
+ TabsetMaximizeButton.displayName = "TabsetMaximizeButton";
2315
+ const TabsetFloatButton = forwardRef(({ children, onClick, ...props }, ref) => {
2225
2316
  const dispatch = useLayout((state) => state.dispatch);
2226
2317
  const node = useTabset((state) => state.node);
2227
2318
  const { float } = useTabsetChrome();
@@ -2241,10 +2332,12 @@ const TabsetFloatButton = ({ children, onClick, ...props }) => {
2241
2332
  ...props,
2242
2333
  "data-dashfoo": "tabset-float",
2243
2334
  onClick: handleClick,
2335
+ ref,
2244
2336
  type: "button",
2245
2337
  children: children ?? /* @__PURE__ */ jsx(FloatIcon, {})
2246
2338
  });
2247
- };
2339
+ });
2340
+ TabsetFloatButton.displayName = "TabsetFloatButton";
2248
2341
 
2249
2342
  //#endregion
2250
2343
  //#region src/components/tabset/tabset-view.tsx
@@ -2638,7 +2731,7 @@ const resolvePersist = (persist) => {
2638
2731
  const warnedComponents = /* @__PURE__ */ new Set();
2639
2732
  const DashfooLayout = forwardRef((props, ref) => {
2640
2733
  const { closableTabs = true, components, defaultModel, draggableTabs = true, draggableTabsets = true, editable = true, factory, floatable = false, keepMounted = false, maximizable = true, model, onAction, onActiveTabsetChange, onMaximizedTabsetChange, onModelChange, persist, renamableTabs = true, renderTabLabel, renderTabsetToolbar, resizableSplits = true, responsive, snap } = props;
2641
- const persistConfig = useMemo(() => resolvePersist(persist), [persist]);
2734
+ const persistConfig = useMemo(() => model === void 0 ? resolvePersist(persist) : null, [model, persist]);
2642
2735
  const persistence = usePersistence(persistConfig, defaultModel);
2643
2736
  const handleModelChange = useCallback((next, action) => {
2644
2737
  persistence.save(next);
@@ -2652,6 +2745,14 @@ const DashfooLayout = forwardRef((props, ref) => {
2652
2745
  onMaximizedTabsetChange,
2653
2746
  onModelChange: handleModelChange
2654
2747
  });
2748
+ const { setModel } = store;
2749
+ const appliedInitialModel = useRef(persistence.initialModel);
2750
+ useEffect(() => {
2751
+ if (persistence.initialModel !== void 0 && persistence.initialModel !== appliedInitialModel.current) {
2752
+ appliedInitialModel.current = persistence.initialModel;
2753
+ setModel(persistence.initialModel);
2754
+ }
2755
+ }, [persistence.initialModel, setModel]);
2655
2756
  const hasSharedManager = useContext(SharedDragManagerContext) !== null;
2656
2757
  useImperativeHandle(ref, () => ({
2657
2758
  addTab: (tab, target) => {
@@ -2728,12 +2829,21 @@ const DashfooLayout = forwardRef((props, ref) => {
2728
2829
  }
2729
2830
  return /* @__PURE__ */ jsx(Component, { node: tab });
2730
2831
  }, [components, factory]);
2832
+ const renderers = useMemo(() => ({
2833
+ tab: renderTab,
2834
+ tabLabel: renderTabLabel,
2835
+ tabsetToolbar: renderTabsetToolbar
2836
+ }), [
2837
+ renderTab,
2838
+ renderTabLabel,
2839
+ renderTabsetToolbar
2840
+ ]);
2731
2841
  const [containerRef, width] = useContainerWidth();
2732
2842
  const isCompact = responsive !== void 0 && width <= responsive.maxWidth;
2733
2843
  /** Everything the compact breakpoint changes, decided once. */
2734
2844
  const presentation = useMemo(() => isCompact ? {
2735
2845
  maximizable: false,
2736
- model: stackModel(store.model, responsive?.orientation),
2846
+ model: compactModel(store.model, responsive?.orientation),
2737
2847
  restructurable: false
2738
2848
  } : {
2739
2849
  maximizable,
@@ -2762,9 +2872,7 @@ const DashfooLayout = forwardRef((props, ref) => {
2762
2872
  maximizable: presentation.maximizable,
2763
2873
  model: view,
2764
2874
  renamableTabs,
2765
- renderTab,
2766
- renderTabLabel,
2767
- renderTabsetToolbar,
2875
+ renderers,
2768
2876
  resizableSplits,
2769
2877
  restructurable: presentation.restructurable,
2770
2878
  rootRef: containerRef,
@@ -2775,31 +2883,62 @@ const DashfooLayout = forwardRef((props, ref) => {
2775
2883
  DashfooLayout.displayName = "DashfooLayout";
2776
2884
 
2777
2885
  //#endregion
2778
- //#region src/components/panel.tsx
2779
- const PanelRoot = (props) => /* @__PURE__ */ jsx("div", {
2886
+ //#region src/components/panel-badge.tsx
2887
+ const PanelBadge = forwardRef((props, ref) => /* @__PURE__ */ jsx("span", {
2780
2888
  ...props,
2781
- "data-dashfoo": "panel"
2782
- });
2783
- const PanelHeader = (props) => /* @__PURE__ */ jsx("div", {
2889
+ "data-dashfoo": "panel-badge",
2890
+ ref
2891
+ }));
2892
+ PanelBadge.displayName = "PanelBadge";
2893
+
2894
+ //#endregion
2895
+ //#region src/components/panel-body.tsx
2896
+ const PanelBody = forwardRef((props, ref) => /* @__PURE__ */ jsx("div", {
2784
2897
  ...props,
2785
- "data-dashfoo": "panel-header"
2786
- });
2787
- const PanelBody = (props) => /* @__PURE__ */ jsx("div", {
2898
+ "data-dashfoo": "panel-body",
2899
+ ref,
2900
+ tabIndex: props.tabIndex ?? 0
2901
+ }));
2902
+ PanelBody.displayName = "PanelBody";
2903
+
2904
+ //#endregion
2905
+ //#region src/components/panel-header.tsx
2906
+ const PanelHeader = forwardRef((props, ref) => /* @__PURE__ */ jsx("div", {
2788
2907
  ...props,
2789
- "data-dashfoo": "panel-body"
2790
- });
2791
- const PanelTitle = (props) => /* @__PURE__ */ jsx("span", {
2908
+ "data-dashfoo": "panel-header",
2909
+ ref
2910
+ }));
2911
+ PanelHeader.displayName = "PanelHeader";
2912
+
2913
+ //#endregion
2914
+ //#region src/components/panel-icon.tsx
2915
+ const PanelIcon = forwardRef((props, ref) => /* @__PURE__ */ jsx("span", {
2792
2916
  ...props,
2793
- "data-dashfoo": "panel-title"
2794
- });
2795
- const PanelIcon = (props) => /* @__PURE__ */ jsx("span", {
2917
+ "data-dashfoo": "panel-icon",
2918
+ ref
2919
+ }));
2920
+ PanelIcon.displayName = "PanelIcon";
2921
+
2922
+ //#endregion
2923
+ //#region src/components/panel-root.tsx
2924
+ const PanelRoot = forwardRef((props, ref) => /* @__PURE__ */ jsx("div", {
2796
2925
  ...props,
2797
- "data-dashfoo": "panel-icon"
2798
- });
2799
- const PanelBadge = (props) => /* @__PURE__ */ jsx("span", {
2926
+ "data-dashfoo": "panel",
2927
+ ref
2928
+ }));
2929
+ PanelRoot.displayName = "PanelRoot";
2930
+
2931
+ //#endregion
2932
+ //#region src/components/panel-title.tsx
2933
+ const PanelTitle = forwardRef((props, ref) => /* @__PURE__ */ jsx("span", {
2800
2934
  ...props,
2801
- "data-dashfoo": "panel-badge"
2802
- });
2935
+ "data-dashfoo": "panel-title",
2936
+ ref
2937
+ }));
2938
+ PanelTitle.displayName = "PanelTitle";
2939
+
2940
+ //#endregion
2941
+ //#region src/components/panel.tsx
2803
2942
  const Panel = {
2804
2943
  Badge: PanelBadge,
2805
2944
  Body: PanelBody,