@dashfoo/react 0.7.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/README.md +26 -13
- package/dist/index.d.ts +91 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +630 -446
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { canRedo, canUndo, dashfooMachine, decideSnap,
|
|
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";
|
|
@@ -66,11 +66,7 @@ const createDragManager = () => new DragDropManager({
|
|
|
66
66
|
const DragRootContext = createContext(null);
|
|
67
67
|
const DragContext = createContext(null);
|
|
68
68
|
const SharedDragManagerContext = createContext(null);
|
|
69
|
-
/**
|
|
70
|
-
* The actor assigns a freshly built intent on every pointer move, so selecting
|
|
71
|
-
* `context.intent` by reference would re-render every reader of `useDropIntent`
|
|
72
|
-
* for each move that resolved to the same slot.
|
|
73
|
-
*/
|
|
69
|
+
/** The intent is rebuilt every pointer move, so readers compare by value. */
|
|
74
70
|
const sameDropIntent = (a, b) => a === b || a !== null && b !== null && a.index === b.index && a.location === b.location && a.targetId === b.targetId;
|
|
75
71
|
const useDraggableHandle = () => {
|
|
76
72
|
const elementRef = useRef(null);
|
|
@@ -182,7 +178,7 @@ const useExternalTabSource = ({ createTab, disabled = false, label = "" }) => {
|
|
|
182
178
|
const createTabRef = useRef(createTab);
|
|
183
179
|
useEffect(() => {
|
|
184
180
|
createTabRef.current = createTab;
|
|
185
|
-
});
|
|
181
|
+
}, [createTab]);
|
|
186
182
|
const data = useMemo(() => ({
|
|
187
183
|
createTab: () => createTabRef.current(),
|
|
188
184
|
type: "external"
|
|
@@ -193,19 +189,23 @@ const useExternalTabSource = ({ createTab, disabled = false, label = "" }) => {
|
|
|
193
189
|
}, disabled, label);
|
|
194
190
|
};
|
|
195
191
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* actor to read and they report the empty state instead of throwing: a component
|
|
200
|
-
* that renders both inside and outside a layout should not crash in one of them.
|
|
192
|
+
* Reads the root's actor, so it works from an overlay mounted beside the layout
|
|
193
|
+
* rather than inside a drag layer. Outside a provider it reports the empty state
|
|
194
|
+
* instead of throwing.
|
|
201
195
|
*/
|
|
202
196
|
const useDragSubject = () => {
|
|
203
197
|
const actorRef = useContext(DragRootContext)?.actorRef;
|
|
204
|
-
return useSelector(actorRef, (snapshot) =>
|
|
198
|
+
return useSelector(actorRef, (snapshot) => {
|
|
199
|
+
const drag = snapshot?.context.drag;
|
|
200
|
+
return drag?.kind === "dragging" ? drag.subject : null;
|
|
201
|
+
});
|
|
205
202
|
};
|
|
206
203
|
const useDropIntent = () => {
|
|
207
204
|
const actorRef = useContext(DragRootContext)?.actorRef;
|
|
208
|
-
return useSelector(actorRef, (snapshot) =>
|
|
205
|
+
return useSelector(actorRef, (snapshot) => {
|
|
206
|
+
const drag = snapshot?.context.drag;
|
|
207
|
+
return drag?.kind === "dragging" ? drag.drop?.intent ?? null : null;
|
|
208
|
+
}, sameDropIntent);
|
|
209
209
|
};
|
|
210
210
|
|
|
211
211
|
//#endregion
|
|
@@ -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,33 +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 [initialModel] = useState(
|
|
255
|
-
|
|
256
|
-
const raw = config.storage.getItem(config.key);
|
|
257
|
-
if (raw === null) return defaultModel;
|
|
258
|
-
try {
|
|
259
|
-
return fromJSON(raw);
|
|
260
|
-
} catch {
|
|
261
|
-
return defaultModel;
|
|
262
|
-
}
|
|
263
|
-
});
|
|
263
|
+
}, [config]);
|
|
264
|
+
const [initialModel, setInitialModel] = useState(defaultModel);
|
|
265
|
+
const loaded = useRef(false);
|
|
264
266
|
useEffect(() => {
|
|
267
|
+
if (loaded.current) return;
|
|
268
|
+
loaded.current = true;
|
|
265
269
|
const current = configRef.current;
|
|
266
|
-
if (current === null) return;
|
|
267
|
-
const raw = current.storage.getItem(current.key);
|
|
268
|
-
if (raw === null) return;
|
|
270
|
+
if (current === null || defaultModel === void 0) return;
|
|
269
271
|
try {
|
|
270
|
-
|
|
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
|
+
}
|
|
271
281
|
} catch (error) {
|
|
272
|
-
console.warn("[dashfoo]
|
|
273
|
-
current.storage.removeItem(current.key);
|
|
282
|
+
console.warn("[dashfoo] failed to load persisted layout", error);
|
|
274
283
|
}
|
|
275
|
-
}, []);
|
|
284
|
+
}, [defaultModel]);
|
|
276
285
|
const loadedKey = useRef(config?.key);
|
|
277
286
|
useEffect(() => {
|
|
278
287
|
if (config !== null && config.key !== loadedKey.current) {
|
|
@@ -288,8 +297,13 @@ const usePersistence = (config, defaultModel) => {
|
|
|
288
297
|
timer.current = null;
|
|
289
298
|
}
|
|
290
299
|
if (pending.current !== null) {
|
|
291
|
-
|
|
300
|
+
const write = pending.current;
|
|
292
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
|
+
}
|
|
293
307
|
}
|
|
294
308
|
}, []);
|
|
295
309
|
const save = useCallback((next) => {
|
|
@@ -325,7 +339,7 @@ const usePersistence = (config, defaultModel) => {
|
|
|
325
339
|
}
|
|
326
340
|
pending.current = null;
|
|
327
341
|
const current = configRef.current;
|
|
328
|
-
if (current !== null)
|
|
342
|
+
if (current !== null) removeStoredLayout(current);
|
|
329
343
|
}, []);
|
|
330
344
|
return useMemo(() => ({
|
|
331
345
|
clear,
|
|
@@ -352,23 +366,30 @@ const activeBreakpoint = (breakpoints, width) => {
|
|
|
352
366
|
if (!fallback) throw new Error("useResponsiveModel requires at least one breakpoint.");
|
|
353
367
|
return breakpoints.find((breakpoint) => matchBreakpoint(breakpoint, width)) ?? fallback;
|
|
354
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
|
+
};
|
|
355
385
|
const useContainerWidth = () => {
|
|
356
386
|
const [width, setWidth] = useState(Number.POSITIVE_INFINITY);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (entry) setWidth(entry.contentRect.width);
|
|
364
|
-
});
|
|
365
|
-
observer.observe(element);
|
|
366
|
-
observerRef.current = observer;
|
|
367
|
-
}, []);
|
|
368
|
-
useEffect(() => () => {
|
|
369
|
-
observerRef.current?.disconnect();
|
|
370
|
-
}, []);
|
|
371
|
-
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];
|
|
372
393
|
};
|
|
373
394
|
const useResponsiveModel = ({ breakpoints }) => {
|
|
374
395
|
const [containerRef, width] = useContainerWidth();
|
|
@@ -380,9 +401,9 @@ const useResponsiveModel = ({ breakpoints }) => {
|
|
|
380
401
|
const handleChange = () => {
|
|
381
402
|
setMediaTick((tick) => tick + 1);
|
|
382
403
|
};
|
|
383
|
-
|
|
404
|
+
const cleanups = lists.map((list) => listenToMedia(list, handleChange));
|
|
384
405
|
return () => {
|
|
385
|
-
for (const
|
|
406
|
+
for (const cleanup of cleanups) cleanup();
|
|
386
407
|
};
|
|
387
408
|
}, [breakpoints]);
|
|
388
409
|
const active = activeBreakpoint(breakpoints, width);
|
|
@@ -412,19 +433,21 @@ const warnOnce = (key, message) => {
|
|
|
412
433
|
//#endregion
|
|
413
434
|
//#region src/hooks/store.ts
|
|
414
435
|
const CONTROLLED_HISTORY_MESSAGE = "undo/redo are unavailable while `model` is controlled; keep the history in the state you own and drive it through onModelChange";
|
|
436
|
+
const CONTROLLED_SET_MODEL_MESSAGE = "setModel (and resetLayout) do nothing while `model` is controlled; set the model you own instead";
|
|
437
|
+
const CONTROLLED_MODE_SWITCH_MESSAGE = "`model` was added or removed after mount; a layout is controlled or uncontrolled for its whole life, so the first render wins. Remount (key=…) to switch";
|
|
438
|
+
const CONTROLLED_WITH_DEFAULT_MESSAGE = "`model` and `defaultModel` were both passed; `model` wins and `defaultModel` (and `persist`) are ignored";
|
|
439
|
+
/** The mode is latched on the first render; running both sources leaves the
|
|
440
|
+
* unread one describing a document nobody rendered. */
|
|
415
441
|
const useDashfooStore = (options) => {
|
|
416
442
|
const { defaultModel, model: controlledModel, onAction, onActiveTabsetChange, onMaximizedTabsetChange, onModelChange } = options;
|
|
417
443
|
const initialModel = controlledModel ?? defaultModel;
|
|
418
444
|
if (initialModel === void 0) throw new Error("useDashfooStore requires either a `model` or a `defaultModel`.");
|
|
445
|
+
const [controlled] = useState(() => controlledModel !== void 0);
|
|
446
|
+
if (controlled !== (controlledModel !== void 0)) warnOnce("controlled-mode-switch", CONTROLLED_MODE_SWITCH_MESSAGE);
|
|
447
|
+
if (controlled && defaultModel !== void 0) warnOnce("controlled-with-default", CONTROLLED_WITH_DEFAULT_MESSAGE);
|
|
419
448
|
const actorRef = useActorRef(dashfooMachine, { input: { model: normalize(initialModel) } });
|
|
420
449
|
const history = useSelector(actorRef, (snapshot) => snapshot.context.history);
|
|
421
|
-
|
|
422
|
-
if (controlledModel !== void 0) actorRef.send({
|
|
423
|
-
model: normalize(controlledModel),
|
|
424
|
-
type: "SET_MODEL"
|
|
425
|
-
});
|
|
426
|
-
}, [actorRef, controlledModel]);
|
|
427
|
-
const model = controlledModel ?? history.present;
|
|
450
|
+
const model = useMemo(() => controlledModel === void 0 ? history.present : normalize(controlledModel), [controlledModel, history.present]);
|
|
428
451
|
const notify = useCallback((before, after, action) => {
|
|
429
452
|
if (Object.is(before, after)) return;
|
|
430
453
|
onModelChange?.(after, action);
|
|
@@ -438,6 +461,10 @@ const useDashfooStore = (options) => {
|
|
|
438
461
|
const dispatch = useCallback((action) => {
|
|
439
462
|
const resolved = onAction ? onAction(action) : action;
|
|
440
463
|
if (!resolved) return;
|
|
464
|
+
if (controlled) {
|
|
465
|
+
notify(model, reducer(model, resolved), resolved);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
441
468
|
const before = actorRef.getSnapshot().context.history.present;
|
|
442
469
|
actorRef.send({
|
|
443
470
|
action: resolved,
|
|
@@ -446,46 +473,50 @@ const useDashfooStore = (options) => {
|
|
|
446
473
|
notify(before, actorRef.getSnapshot().context.history.present, resolved);
|
|
447
474
|
}, [
|
|
448
475
|
actorRef,
|
|
476
|
+
controlled,
|
|
477
|
+
model,
|
|
449
478
|
notify,
|
|
450
479
|
onAction
|
|
451
480
|
]);
|
|
452
481
|
const undo = useCallback(() => {
|
|
453
|
-
if (
|
|
482
|
+
if (controlled) {
|
|
454
483
|
warnOnce("controlled-undo", CONTROLLED_HISTORY_MESSAGE);
|
|
455
484
|
return;
|
|
456
485
|
}
|
|
457
486
|
const before = actorRef.getSnapshot().context.history.present;
|
|
458
487
|
actorRef.send({ type: "UNDO" });
|
|
459
|
-
|
|
460
|
-
notify(before, after);
|
|
488
|
+
notify(before, actorRef.getSnapshot().context.history.present);
|
|
461
489
|
}, [
|
|
462
490
|
actorRef,
|
|
463
|
-
|
|
491
|
+
controlled,
|
|
464
492
|
notify
|
|
465
493
|
]);
|
|
466
494
|
const redo = useCallback(() => {
|
|
467
|
-
if (
|
|
495
|
+
if (controlled) {
|
|
468
496
|
warnOnce("controlled-redo", CONTROLLED_HISTORY_MESSAGE);
|
|
469
497
|
return;
|
|
470
498
|
}
|
|
471
499
|
const before = actorRef.getSnapshot().context.history.present;
|
|
472
500
|
actorRef.send({ type: "REDO" });
|
|
473
|
-
|
|
474
|
-
notify(before, after);
|
|
501
|
+
notify(before, actorRef.getSnapshot().context.history.present);
|
|
475
502
|
}, [
|
|
476
503
|
actorRef,
|
|
477
|
-
|
|
504
|
+
controlled,
|
|
478
505
|
notify
|
|
479
506
|
]);
|
|
480
507
|
const setModel = useCallback((next) => {
|
|
508
|
+
if (controlled) {
|
|
509
|
+
warnOnce("controlled-set-model", CONTROLLED_SET_MODEL_MESSAGE);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
481
512
|
actorRef.send({
|
|
482
513
|
model: normalize(next),
|
|
483
514
|
type: "SET_MODEL"
|
|
484
515
|
});
|
|
485
|
-
}, [actorRef]);
|
|
516
|
+
}, [actorRef, controlled]);
|
|
486
517
|
return useMemo(() => ({
|
|
487
|
-
canRedo: () =>
|
|
488
|
-
canUndo: () =>
|
|
518
|
+
canRedo: () => !controlled && canRedo(actorRef.getSnapshot().context.history),
|
|
519
|
+
canUndo: () => !controlled && canUndo(actorRef.getSnapshot().context.history),
|
|
489
520
|
dispatch,
|
|
490
521
|
model,
|
|
491
522
|
redo,
|
|
@@ -493,7 +524,7 @@ const useDashfooStore = (options) => {
|
|
|
493
524
|
undo
|
|
494
525
|
}), [
|
|
495
526
|
actorRef,
|
|
496
|
-
|
|
527
|
+
controlled,
|
|
497
528
|
dispatch,
|
|
498
529
|
model,
|
|
499
530
|
redo,
|
|
@@ -502,21 +533,38 @@ const useDashfooStore = (options) => {
|
|
|
502
533
|
]);
|
|
503
534
|
};
|
|
504
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
|
+
|
|
505
556
|
//#endregion
|
|
506
557
|
//#region src/lib/drag-subject.ts
|
|
507
558
|
const isTabFactory = (value) => typeof value === "function";
|
|
508
|
-
/**
|
|
509
|
-
*
|
|
510
|
-
* rather than in the layer adapter because `dragstart` fires once per gesture on
|
|
511
|
-
* the shared manager, above any one layer.
|
|
512
|
-
*/
|
|
559
|
+
/** Beside the manager wiring, not the layer adapter: `dragstart` fires once per
|
|
560
|
+
* gesture on the shared manager, above any one layer. */
|
|
513
561
|
const subjectFor = (source) => {
|
|
514
562
|
const data = source.data;
|
|
515
|
-
if (data
|
|
563
|
+
if (data.type === "tabset") return {
|
|
516
564
|
id: String(data.tabsetId),
|
|
517
565
|
kind: "tabset"
|
|
518
566
|
};
|
|
519
|
-
if (data
|
|
567
|
+
if (data.type === "external") {
|
|
520
568
|
if (!isTabFactory(data.createTab)) {
|
|
521
569
|
console.warn("[dashfoo] external drag source is missing its createTab function");
|
|
522
570
|
return null;
|
|
@@ -571,7 +619,7 @@ const shouldAllowDrop = (draggedId, targetId, tabIds) => {
|
|
|
571
619
|
};
|
|
572
620
|
|
|
573
621
|
//#endregion
|
|
574
|
-
//#region src/components/drag-
|
|
622
|
+
//#region src/components/drag-preview-overlay.tsx
|
|
575
623
|
const overlayBase = {
|
|
576
624
|
boxSizing: "border-box",
|
|
577
625
|
pointerEvents: "none",
|
|
@@ -595,9 +643,10 @@ const lineStyle = (zone) => ({
|
|
|
595
643
|
});
|
|
596
644
|
const tabItemRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab-item\"]")].flatMap((item) => item.querySelector("[data-dashfoo=\"tab\"]")?.dataset.tabId === excludeId ? [] : [item.getBoundingClientRect()]);
|
|
597
645
|
const DockIndicator = ({ actorRef, getTabsetElement }) => {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
646
|
+
const drag = useSelector(actorRef, (snapshot) => snapshot.context.drag);
|
|
647
|
+
if (drag.kind !== "dragging" || drag.drop === null) return null;
|
|
648
|
+
const { intent } = drag.drop;
|
|
649
|
+
const draggedId = drag.subject.id;
|
|
601
650
|
const element = getTabsetElement(intent.targetId);
|
|
602
651
|
if (!element) return null;
|
|
603
652
|
const zIndex = element.closest("[data-dashfoo=\"float\"]") ? overlayBase.zIndex : "auto";
|
|
@@ -628,13 +677,13 @@ const PREVIEW_OFFSET = {
|
|
|
628
677
|
y: 8
|
|
629
678
|
};
|
|
630
679
|
const labelOf = (source) => {
|
|
631
|
-
|
|
632
|
-
return
|
|
680
|
+
if (typeof source?.data.label !== "string") return "";
|
|
681
|
+
return source.data.label;
|
|
633
682
|
};
|
|
634
683
|
const DragPreviewOverlay = ({ manager }) => {
|
|
635
684
|
const [chip, setChip] = useState(null);
|
|
636
685
|
useEffect(() => {
|
|
637
|
-
const
|
|
686
|
+
const handleStart = (event) => {
|
|
638
687
|
const source = event.operation.source;
|
|
639
688
|
if (!source) return;
|
|
640
689
|
const rect = source.element?.getBoundingClientRect();
|
|
@@ -644,21 +693,24 @@ const DragPreviewOverlay = ({ manager }) => {
|
|
|
644
693
|
x: PREVIEW_OFFSET.x + (rect ? point.x - rect.left : 0),
|
|
645
694
|
y: PREVIEW_OFFSET.y + (rect ? point.y - rect.top : 0)
|
|
646
695
|
});
|
|
647
|
-
}
|
|
648
|
-
const
|
|
696
|
+
};
|
|
697
|
+
const handleEnd = () => {
|
|
649
698
|
setChip(null);
|
|
650
|
-
}
|
|
699
|
+
};
|
|
700
|
+
manager.monitor.addEventListener("dragstart", handleStart);
|
|
701
|
+
manager.monitor.addEventListener("dragend", handleEnd);
|
|
651
702
|
return () => {
|
|
652
|
-
|
|
653
|
-
|
|
703
|
+
manager.monitor.removeEventListener("dragstart", handleStart);
|
|
704
|
+
manager.monitor.removeEventListener("dragend", handleEnd);
|
|
654
705
|
};
|
|
655
706
|
}, [manager]);
|
|
707
|
+
const attachOverlay = useCallback((element) => {
|
|
708
|
+
const feedback = manager.registry.plugins.get(Feedback);
|
|
709
|
+
if (feedback) feedback.overlay = element ?? void 0;
|
|
710
|
+
}, [manager]);
|
|
656
711
|
return /* @__PURE__ */ jsx("div", {
|
|
657
712
|
"data-dnd-overlay": "",
|
|
658
|
-
ref:
|
|
659
|
-
const feedback = manager.registry.plugins.get(Feedback);
|
|
660
|
-
if (feedback) feedback.overlay = element ?? void 0;
|
|
661
|
-
}, [manager]),
|
|
713
|
+
ref: attachOverlay,
|
|
662
714
|
children: chip === null ? null : /* @__PURE__ */ jsx("div", {
|
|
663
715
|
"data-dashfoo": "drag-preview",
|
|
664
716
|
style: {
|
|
@@ -674,7 +726,8 @@ const DragPreviewOverlay = ({ manager }) => {
|
|
|
674
726
|
};
|
|
675
727
|
|
|
676
728
|
//#endregion
|
|
677
|
-
//#region src/components/drag-
|
|
729
|
+
//#region src/components/dashfoo-drag-provider.tsx
|
|
730
|
+
const isScopedTargetData = (data) => typeof data.layerId === "string" && typeof data.tabsetId === "string";
|
|
678
731
|
const DashfooDragProvider = ({ children }) => {
|
|
679
732
|
const [manager] = useState(createDragManager);
|
|
680
733
|
useInsertionEffect(() => () => {
|
|
@@ -688,33 +741,29 @@ const DashfooDragProvider = ({ children }) => {
|
|
|
688
741
|
scopes.delete(scope.layerId);
|
|
689
742
|
};
|
|
690
743
|
}, [scopes]);
|
|
691
|
-
/**
|
|
692
|
-
* One listener set and one actor for the whole tree. Attached per layer, a
|
|
693
|
-
* single pointer move fanned out to every float's actor, and each one that did
|
|
694
|
-
* not own the target resolved `intent: null` and then swallowed the drop.
|
|
695
|
-
*/
|
|
696
744
|
useEffect(() => {
|
|
697
745
|
const scopedTarget = () => {
|
|
698
746
|
const target = manager.dragOperation.target;
|
|
699
747
|
const data = target?.data;
|
|
700
|
-
const scope = data
|
|
748
|
+
const scope = data && isScopedTargetData(data) ? scopes.get(data.layerId) : void 0;
|
|
701
749
|
const element = target?.element;
|
|
702
|
-
if (!scope || data
|
|
750
|
+
if (!scope || !data || !isScopedTargetData(data) || !(element instanceof HTMLElement)) return null;
|
|
703
751
|
return {
|
|
704
752
|
element,
|
|
705
753
|
scope,
|
|
706
754
|
tabsetId: data.tabsetId
|
|
707
755
|
};
|
|
708
756
|
};
|
|
709
|
-
let intentLayerId = null;
|
|
710
757
|
const syncIntent = () => {
|
|
711
758
|
const operation = manager.dragOperation;
|
|
712
759
|
const found = scopedTarget();
|
|
713
760
|
const draggedId = operation.source ? String(operation.source.id) : void 0;
|
|
714
761
|
const intent = found === null ? null : found.scope.resolveIntent(found.tabsetId, found.element, operation.position.current, draggedId);
|
|
715
|
-
intentLayerId = intent === null || found === null ? null : found.scope.layerId;
|
|
716
762
|
actorRef.send({
|
|
717
|
-
intent
|
|
763
|
+
drop: intent === null || found === null ? null : {
|
|
764
|
+
intent,
|
|
765
|
+
scope: found.scope.layerId
|
|
766
|
+
},
|
|
718
767
|
type: "OVER"
|
|
719
768
|
});
|
|
720
769
|
};
|
|
@@ -726,19 +775,27 @@ const DashfooDragProvider = ({ children }) => {
|
|
|
726
775
|
manager.actions.stop({ canceled: true });
|
|
727
776
|
return;
|
|
728
777
|
}
|
|
729
|
-
intentLayerId = null;
|
|
730
778
|
actorRef.send({
|
|
731
779
|
subject,
|
|
732
780
|
type: "START"
|
|
733
781
|
});
|
|
734
782
|
};
|
|
735
|
-
|
|
736
|
-
|
|
783
|
+
let frame = null;
|
|
784
|
+
const scheduleResolve = () => {
|
|
785
|
+
if (frame !== null) return;
|
|
786
|
+
frame = requestAnimationFrame(() => {
|
|
787
|
+
frame = null;
|
|
788
|
+
syncIntent();
|
|
789
|
+
});
|
|
737
790
|
};
|
|
738
|
-
const
|
|
739
|
-
|
|
791
|
+
const cancelPendingResolve = () => {
|
|
792
|
+
if (frame !== null) {
|
|
793
|
+
cancelAnimationFrame(frame);
|
|
794
|
+
frame = null;
|
|
795
|
+
}
|
|
740
796
|
};
|
|
741
797
|
const handleEnd = (event) => {
|
|
798
|
+
cancelPendingResolve();
|
|
742
799
|
if (event.canceled) {
|
|
743
800
|
actorRef.send({ type: "CANCEL" });
|
|
744
801
|
return;
|
|
@@ -747,18 +804,19 @@ const DashfooDragProvider = ({ children }) => {
|
|
|
747
804
|
actorRef.send({ type: "DROP" });
|
|
748
805
|
};
|
|
749
806
|
const commitSubscription = actorRef.on("COMMIT", (emitted) => {
|
|
750
|
-
|
|
807
|
+
scopes.get(emitted.scope)?.commit(emitted.action);
|
|
751
808
|
});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
809
|
+
manager.monitor.addEventListener("dragstart", handleStart);
|
|
810
|
+
manager.monitor.addEventListener("dragmove", scheduleResolve);
|
|
811
|
+
manager.monitor.addEventListener("collision", scheduleResolve);
|
|
812
|
+
manager.monitor.addEventListener("dragend", handleEnd);
|
|
756
813
|
return () => {
|
|
814
|
+
cancelPendingResolve();
|
|
757
815
|
commitSubscription.unsubscribe();
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
816
|
+
manager.monitor.removeEventListener("dragstart", handleStart);
|
|
817
|
+
manager.monitor.removeEventListener("dragmove", scheduleResolve);
|
|
818
|
+
manager.monitor.removeEventListener("collision", scheduleResolve);
|
|
819
|
+
manager.monitor.removeEventListener("dragend", handleEnd);
|
|
762
820
|
};
|
|
763
821
|
}, [
|
|
764
822
|
actorRef,
|
|
@@ -794,19 +852,34 @@ const useLayout = (selector) => {
|
|
|
794
852
|
};
|
|
795
853
|
|
|
796
854
|
//#endregion
|
|
797
|
-
//#region src/components/drag-
|
|
855
|
+
//#region src/components/drag-provider.tsx
|
|
798
856
|
const MISSING_ROOT = "[dashfoo] DashfooDragProvider unmounted while its layout is still mounted; keep the provider above the layout or remount the layout";
|
|
799
|
-
|
|
800
|
-
const
|
|
857
|
+
/** One DOM pass per pointer move, where resolving a drop used to run three. */
|
|
858
|
+
const measureStrip = (element, excludeId) => {
|
|
801
859
|
const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
|
|
802
|
-
if (strip
|
|
803
|
-
|
|
860
|
+
if (!strip) return null;
|
|
861
|
+
const tabIds = [];
|
|
862
|
+
const tabRects = [];
|
|
863
|
+
for (const tab of strip.querySelectorAll("[data-dashfoo=\"tab\"]")) {
|
|
864
|
+
const tabId = tab.dataset.tabId ?? "";
|
|
865
|
+
tabIds.push(tabId);
|
|
866
|
+
if (tabId !== excludeId) tabRects.push(tab.getBoundingClientRect());
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
rect: strip.getBoundingClientRect(),
|
|
870
|
+
tabIds,
|
|
871
|
+
tabRects
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
const intentForTabset = (id, element, point, strip) => {
|
|
875
|
+
if (strip && pointInRect(point, strip.rect)) return {
|
|
876
|
+
index: insertionIndex(strip.tabRects, point.x),
|
|
804
877
|
location: "center",
|
|
805
878
|
targetId: id
|
|
806
879
|
};
|
|
807
|
-
const location =
|
|
880
|
+
const location = resolveDockTarget(point, element.getBoundingClientRect());
|
|
808
881
|
if (location === "center" && strip) return {
|
|
809
|
-
index:
|
|
882
|
+
index: strip.tabRects.length,
|
|
810
883
|
location,
|
|
811
884
|
targetId: id
|
|
812
885
|
};
|
|
@@ -815,11 +888,7 @@ const intentForTabset = (id, element, point, draggedId) => {
|
|
|
815
888
|
targetId: id
|
|
816
889
|
};
|
|
817
890
|
};
|
|
818
|
-
/**
|
|
819
|
-
* One drag layer: the main tree, or one float. It holds no drag state of its
|
|
820
|
-
* own. It registers how its own tabsets turn a pointer into an intent and where
|
|
821
|
-
* a committed action goes, then draws the indicator over its own droppables.
|
|
822
|
-
*/
|
|
891
|
+
/** One drag layer (the main tree, or one float). Holds no drag state of its own. */
|
|
823
892
|
const DragScope = ({ children, onCommit, splitDock }) => {
|
|
824
893
|
const root = useContext(DragRootContext);
|
|
825
894
|
const layoutStore = useContext(LayoutStoreContext);
|
|
@@ -842,8 +911,9 @@ const DragScope = ({ children, onCommit, splitDock }) => {
|
|
|
842
911
|
layerId,
|
|
843
912
|
resolveIntent: (targetId, element, point, draggedId) => {
|
|
844
913
|
if (layoutStore?.getState().editable === false) return null;
|
|
845
|
-
|
|
846
|
-
|
|
914
|
+
const strip = measureStrip(element, draggedId);
|
|
915
|
+
if (!shouldAllowDrop(draggedId, targetId, strip?.tabIds ?? [])) return null;
|
|
916
|
+
const intent = intentForTabset(targetId, element, point, strip);
|
|
847
917
|
if (!(splitDock ?? layoutStore?.getState().splitDock ?? true) && splitEdge(intent.location) !== void 0) return {
|
|
848
918
|
location: "center",
|
|
849
919
|
targetId
|
|
@@ -873,10 +943,8 @@ const DragScope = ({ children, onCommit, splitDock }) => {
|
|
|
873
943
|
});
|
|
874
944
|
};
|
|
875
945
|
/**
|
|
876
|
-
* Mounts its own root when there is none above it
|
|
877
|
-
*
|
|
878
|
-
* render, as it was for the manager this used to create itself: a root that
|
|
879
|
-
* disappears later means a torn-down tree, which `DragScope` reports.
|
|
946
|
+
* Mounts its own root when there is none above it. Decided on the first render:
|
|
947
|
+
* a root that disappears later means a torn-down tree, which `DragScope` reports.
|
|
880
948
|
*/
|
|
881
949
|
const DragProvider = (props) => {
|
|
882
950
|
const hasRoot = useContext(DragRootContext) !== null;
|
|
@@ -1062,16 +1130,11 @@ const CHIP_SIZE = {
|
|
|
1062
1130
|
width: 168
|
|
1063
1131
|
};
|
|
1064
1132
|
const TAP_SLOP = 4;
|
|
1065
|
-
/**
|
|
1066
|
-
*
|
|
1067
|
-
* dispatches on pointer-up: routing every pointermove through the model would
|
|
1068
|
-
* re-render the whole layout tree behind the float.
|
|
1069
|
-
*/
|
|
1133
|
+
/** Writes inline style and dispatches only on pointer-up: routing every move
|
|
1134
|
+
* through the model would re-render the whole tree behind the float. */
|
|
1070
1135
|
const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) => {
|
|
1071
1136
|
const panelRef = useRef(null);
|
|
1072
1137
|
const gestureRef = useRef(null);
|
|
1073
|
-
const latestRef = useRef(geometry);
|
|
1074
|
-
const movedRef = useRef(false);
|
|
1075
1138
|
const setPanel = (element) => {
|
|
1076
1139
|
panelRef.current = element;
|
|
1077
1140
|
};
|
|
@@ -1080,22 +1143,35 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
|
|
|
1080
1143
|
if (!panel || !editable) return;
|
|
1081
1144
|
const prior = gestureRef.current;
|
|
1082
1145
|
if (prior && panel.hasPointerCapture?.(prior.pointerId)) panel.releasePointerCapture(prior.pointerId);
|
|
1083
|
-
movedRef.current = false;
|
|
1084
1146
|
const edgeKey = event.currentTarget.dataset.edge;
|
|
1085
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
|
+
};
|
|
1086
1155
|
const gesture = {
|
|
1087
1156
|
bounds: {
|
|
1088
1157
|
height: parent?.clientHeight ?? 0,
|
|
1089
1158
|
width: parent?.clientWidth ?? 0
|
|
1090
1159
|
},
|
|
1091
1160
|
edges: edgeKey === void 0 ? null : EDGE_BY_KEY.get(edgeKey) ?? null,
|
|
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,
|
|
1168
|
+
moved: false,
|
|
1092
1169
|
pointerId: event.pointerId,
|
|
1093
|
-
start:
|
|
1170
|
+
start: visibleGeometry,
|
|
1094
1171
|
startX: event.clientX,
|
|
1095
1172
|
startY: event.clientY
|
|
1096
1173
|
};
|
|
1097
1174
|
gestureRef.current = gesture;
|
|
1098
|
-
latestRef.current = geometry;
|
|
1099
1175
|
if (gesture.edges) panel.setPointerCapture(event.pointerId);
|
|
1100
1176
|
};
|
|
1101
1177
|
const handlePointerUp = (event) => {
|
|
@@ -1104,11 +1180,11 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
|
|
|
1104
1180
|
gestureRef.current = null;
|
|
1105
1181
|
const panel = panelRef.current;
|
|
1106
1182
|
if (panel?.hasPointerCapture?.(event.pointerId) === true) panel.releasePointerCapture(event.pointerId);
|
|
1107
|
-
if (!
|
|
1183
|
+
if (!gesture.moved) {
|
|
1108
1184
|
onTap();
|
|
1109
1185
|
return;
|
|
1110
1186
|
}
|
|
1111
|
-
onCommit(
|
|
1187
|
+
onCommit(gesture.latest);
|
|
1112
1188
|
};
|
|
1113
1189
|
const handlePointerMove = (event) => {
|
|
1114
1190
|
const gesture = gestureRef.current;
|
|
@@ -1120,9 +1196,9 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
|
|
|
1120
1196
|
}
|
|
1121
1197
|
const dx = event.clientX - gesture.startX;
|
|
1122
1198
|
const dy = event.clientY - gesture.startY;
|
|
1123
|
-
if (!
|
|
1199
|
+
if (!gesture.moved) {
|
|
1124
1200
|
if (Math.hypot(dx, dy) <= TAP_SLOP) return;
|
|
1125
|
-
|
|
1201
|
+
gesture.moved = true;
|
|
1126
1202
|
if (!gesture.edges) panel.setPointerCapture(gesture.pointerId);
|
|
1127
1203
|
}
|
|
1128
1204
|
const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
|
|
@@ -1130,7 +1206,7 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
|
|
|
1130
1206
|
left: gesture.start.left + dx,
|
|
1131
1207
|
top: gesture.start.top + dy
|
|
1132
1208
|
}, gesture.bounds, minimized ? CHIP_SIZE : void 0);
|
|
1133
|
-
|
|
1209
|
+
gesture.latest = next;
|
|
1134
1210
|
panel.style.left = `${next.left}px`;
|
|
1135
1211
|
panel.style.top = `${next.top}px`;
|
|
1136
1212
|
if (!minimized) {
|
|
@@ -1144,13 +1220,8 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
|
|
|
1144
1220
|
if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
|
|
1145
1221
|
gestureRef.current = null;
|
|
1146
1222
|
if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
|
|
1147
|
-
if (!
|
|
1148
|
-
panel.style
|
|
1149
|
-
panel.style.top = `${gesture.start.top}px`;
|
|
1150
|
-
if (!minimized) {
|
|
1151
|
-
panel.style.width = `${gesture.start.width}px`;
|
|
1152
|
-
panel.style.height = `${gesture.start.height}px`;
|
|
1153
|
-
}
|
|
1223
|
+
if (!gesture.moved) return;
|
|
1224
|
+
Object.assign(panel.style, gesture.initialStyle);
|
|
1154
1225
|
};
|
|
1155
1226
|
return {
|
|
1156
1227
|
handlers: {
|
|
@@ -1160,14 +1231,11 @@ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) =>
|
|
|
1160
1231
|
onPointerUp: handlePointerUp
|
|
1161
1232
|
},
|
|
1162
1233
|
ref: setPanel,
|
|
1163
|
-
style:
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
left: geometry.left,
|
|
1169
|
-
top: geometry.top,
|
|
1170
|
-
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)`
|
|
1171
1239
|
}
|
|
1172
1240
|
};
|
|
1173
1241
|
};
|
|
@@ -1205,6 +1273,95 @@ const useInlineRename = ({ currentName, inputRef, onCommit, onDone }) => {
|
|
|
1205
1273
|
};
|
|
1206
1274
|
};
|
|
1207
1275
|
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region src/components/close-icon.tsx
|
|
1278
|
+
const iconStyle = { pointerEvents: "none" };
|
|
1279
|
+
const Icon = ({ children }) => /* @__PURE__ */ jsx("svg", {
|
|
1280
|
+
"aria-hidden": "true",
|
|
1281
|
+
fill: "none",
|
|
1282
|
+
height: "12",
|
|
1283
|
+
stroke: "currentColor",
|
|
1284
|
+
strokeLinecap: "round",
|
|
1285
|
+
strokeLinejoin: "round",
|
|
1286
|
+
strokeWidth: "2",
|
|
1287
|
+
style: iconStyle,
|
|
1288
|
+
viewBox: "0 0 24 24",
|
|
1289
|
+
width: "12",
|
|
1290
|
+
children
|
|
1291
|
+
});
|
|
1292
|
+
const CloseIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] });
|
|
1293
|
+
const GripIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1294
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1295
|
+
cx: "12",
|
|
1296
|
+
cy: "5",
|
|
1297
|
+
r: "1"
|
|
1298
|
+
}),
|
|
1299
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1300
|
+
cx: "19",
|
|
1301
|
+
cy: "5",
|
|
1302
|
+
r: "1"
|
|
1303
|
+
}),
|
|
1304
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1305
|
+
cx: "5",
|
|
1306
|
+
cy: "5",
|
|
1307
|
+
r: "1"
|
|
1308
|
+
}),
|
|
1309
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1310
|
+
cx: "12",
|
|
1311
|
+
cy: "12",
|
|
1312
|
+
r: "1"
|
|
1313
|
+
}),
|
|
1314
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1315
|
+
cx: "19",
|
|
1316
|
+
cy: "12",
|
|
1317
|
+
r: "1"
|
|
1318
|
+
}),
|
|
1319
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1320
|
+
cx: "5",
|
|
1321
|
+
cy: "12",
|
|
1322
|
+
r: "1"
|
|
1323
|
+
}),
|
|
1324
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1325
|
+
cx: "12",
|
|
1326
|
+
cy: "19",
|
|
1327
|
+
r: "1"
|
|
1328
|
+
}),
|
|
1329
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1330
|
+
cx: "19",
|
|
1331
|
+
cy: "19",
|
|
1332
|
+
r: "1"
|
|
1333
|
+
}),
|
|
1334
|
+
/* @__PURE__ */ jsx("circle", {
|
|
1335
|
+
cx: "5",
|
|
1336
|
+
cy: "19",
|
|
1337
|
+
r: "1"
|
|
1338
|
+
})
|
|
1339
|
+
] });
|
|
1340
|
+
const MaximizeIcon = ({ maximized }) => maximized ? /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1341
|
+
/* @__PURE__ */ jsx("path", { d: "m14 10 7-7" }),
|
|
1342
|
+
/* @__PURE__ */ jsx("path", { d: "M20 10h-6V4" }),
|
|
1343
|
+
/* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
|
|
1344
|
+
/* @__PURE__ */ jsx("path", { d: "M4 14h6v6" })
|
|
1345
|
+
] }) : /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1346
|
+
/* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
|
|
1347
|
+
/* @__PURE__ */ jsx("path", { d: "m21 3-7 7" }),
|
|
1348
|
+
/* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
|
|
1349
|
+
/* @__PURE__ */ jsx("path", { d: "M9 21H3v-6" })
|
|
1350
|
+
] });
|
|
1351
|
+
const FloatIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("rect", {
|
|
1352
|
+
height: "14",
|
|
1353
|
+
rx: "2",
|
|
1354
|
+
width: "18",
|
|
1355
|
+
x: "3",
|
|
1356
|
+
y: "6"
|
|
1357
|
+
}), /* @__PURE__ */ jsx("path", { d: "M3 10h18" })] });
|
|
1358
|
+
const MinimizeIcon = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
|
|
1359
|
+
const DockIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1360
|
+
/* @__PURE__ */ jsx("path", { d: "M3 7V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2" }),
|
|
1361
|
+
/* @__PURE__ */ jsx("path", { d: "M11 8 7 12l4 4" }),
|
|
1362
|
+
/* @__PURE__ */ jsx("path", { d: "M7 12h10" })
|
|
1363
|
+
] });
|
|
1364
|
+
|
|
1208
1365
|
//#endregion
|
|
1209
1366
|
//#region src/components/layout-root.tsx
|
|
1210
1367
|
const DEFAULT_TABSET_MIN_SIZE = 320;
|
|
@@ -1214,23 +1371,39 @@ const rootStyle = {
|
|
|
1214
1371
|
position: "relative",
|
|
1215
1372
|
width: "100%"
|
|
1216
1373
|
};
|
|
1217
|
-
const
|
|
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 }) => {
|
|
1218
1387
|
const global = model.global;
|
|
1219
1388
|
const snapshot = {
|
|
1220
|
-
|
|
1389
|
+
...resolveCapabilities(global, {
|
|
1390
|
+
closableTabs,
|
|
1391
|
+
draggableTabs,
|
|
1392
|
+
draggableTabsets,
|
|
1393
|
+
editable,
|
|
1394
|
+
floatable,
|
|
1395
|
+
maximizable,
|
|
1396
|
+
renamableTabs,
|
|
1397
|
+
resizableSplits,
|
|
1398
|
+
restructurable
|
|
1399
|
+
}),
|
|
1221
1400
|
dispatch,
|
|
1222
|
-
draggableTabs: editable && restructurable && draggableTabs && global.tabEnableDrag !== false,
|
|
1223
|
-
draggableTabsets: editable && restructurable && draggableTabsets,
|
|
1224
1401
|
editable,
|
|
1225
|
-
floatable: editable && restructurable && floatable,
|
|
1226
1402
|
keepMounted,
|
|
1227
|
-
maximizable: maximizable && global.tabSetEnableMaximize !== false,
|
|
1228
1403
|
maximizedTabsetId: model.maximizedTabsetId,
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
renderTabsetToolbar,
|
|
1233
|
-
resizableSplits: editable && restructurable && resizableSplits && global.enableSplitResize !== false,
|
|
1404
|
+
renderTab: renderers.tab,
|
|
1405
|
+
renderTabLabel: renderers.tabLabel,
|
|
1406
|
+
renderTabsetToolbar: renderers.tabsetToolbar,
|
|
1234
1407
|
snap: snap ?? global.snap ?? null,
|
|
1235
1408
|
splitDock: global.enableSplitDock !== false,
|
|
1236
1409
|
tabLocation: global.tabLocation ?? "top",
|
|
@@ -1241,14 +1414,11 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
|
|
|
1241
1414
|
useLayoutEffect(() => {
|
|
1242
1415
|
store.setState(snapshot);
|
|
1243
1416
|
});
|
|
1244
|
-
const layoutStyle =
|
|
1417
|
+
const layoutStyle = {
|
|
1245
1418
|
...rootStyle,
|
|
1246
1419
|
...style
|
|
1247
|
-
} : {
|
|
1248
|
-
...rootStyle,
|
|
1249
|
-
"--dashfoo-splitter-size": `${global.splitterSize}px`,
|
|
1250
|
-
...style
|
|
1251
1420
|
};
|
|
1421
|
+
if (global.splitterSize !== void 0) layoutStyle["--dashfoo-splitter-size"] = `${global.splitterSize}px`;
|
|
1252
1422
|
return /* @__PURE__ */ jsx(LayoutStoreContext.Provider, {
|
|
1253
1423
|
value: store,
|
|
1254
1424
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -1261,10 +1431,8 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
|
|
|
1261
1431
|
});
|
|
1262
1432
|
};
|
|
1263
1433
|
/**
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
* overrides applied instead of re-deriving every flag from a synthetic model,
|
|
1267
|
-
* so anything the parent changes keeps flowing into the subtree.
|
|
1434
|
+
* Republishes the parent store with overrides applied rather than re-deriving
|
|
1435
|
+
* flags, so anything the parent changes keeps flowing into the subtree.
|
|
1268
1436
|
*/
|
|
1269
1437
|
const LayoutOverrides = ({ children, overrides }) => {
|
|
1270
1438
|
const snapshot = {
|
|
@@ -1402,7 +1570,7 @@ const createTabsetStore = (initial) => createStore((set, get) => ({
|
|
|
1402
1570
|
set({ editingTabId: null });
|
|
1403
1571
|
},
|
|
1404
1572
|
closeTab: (tabId) => {
|
|
1405
|
-
set({
|
|
1573
|
+
set({ pendingCloseTabId: tabId });
|
|
1406
1574
|
get().dispatch({
|
|
1407
1575
|
tabId,
|
|
1408
1576
|
type: "deleteTab"
|
|
@@ -1417,6 +1585,7 @@ const createTabsetStore = (initial) => createStore((set, get) => ({
|
|
|
1417
1585
|
},
|
|
1418
1586
|
editingTabId: null,
|
|
1419
1587
|
overflowItems: [],
|
|
1588
|
+
pendingCloseTabId: null,
|
|
1420
1589
|
registerRenameInput: () => {
|
|
1421
1590
|
set((state) => ({ renameInputCount: state.renameInputCount + 1 }));
|
|
1422
1591
|
return () => {
|
|
@@ -1440,7 +1609,6 @@ const createTabsetStore = (initial) => createStore((set, get) => ({
|
|
|
1440
1609
|
type: "renameTab"
|
|
1441
1610
|
});
|
|
1442
1611
|
},
|
|
1443
|
-
restoreFocus: false,
|
|
1444
1612
|
selectOverflowTab: (tabId) => {
|
|
1445
1613
|
const { dispatch, node, tablistElement } = get();
|
|
1446
1614
|
const index = node.children.findIndex((tab) => tab.id === tabId);
|
|
@@ -1510,8 +1678,9 @@ const TabPanel = ({ render, tab }) => {
|
|
|
1510
1678
|
const renderTab = useLayout((state) => state.renderTab);
|
|
1511
1679
|
return render ? render(tab) : renderTab(tab);
|
|
1512
1680
|
};
|
|
1513
|
-
const TabsetContent = ({ children, style, ...props }) => {
|
|
1681
|
+
const TabsetContent = forwardRef(({ children, style, ...props }, ref) => {
|
|
1514
1682
|
const node = useTabset((state) => state.node);
|
|
1683
|
+
const editingTabId = useTabset((state) => state.editingTabId);
|
|
1515
1684
|
const visualSelected = useTabset((state) => state.visualSelected);
|
|
1516
1685
|
const keepMounted = useLayout((state) => state.keepMounted);
|
|
1517
1686
|
const mergedStyle = {
|
|
@@ -1520,11 +1689,13 @@ const TabsetContent = ({ children, style, ...props }) => {
|
|
|
1520
1689
|
};
|
|
1521
1690
|
if (keepMounted && node.children.length > 0) return node.children.map((tab, index) => /* @__PURE__ */ createElement("div", {
|
|
1522
1691
|
...props,
|
|
1523
|
-
"aria-
|
|
1692
|
+
"aria-label": editingTabId === tab.id ? tab.name : void 0,
|
|
1693
|
+
"aria-labelledby": editingTabId === tab.id ? void 0 : tabDomId(node.id, tab.id),
|
|
1524
1694
|
"data-dashfoo": "tabcontent",
|
|
1525
1695
|
hidden: index !== visualSelected || void 0,
|
|
1526
1696
|
id: index === visualSelected ? panelDomId(node.id) : void 0,
|
|
1527
1697
|
key: tab.id,
|
|
1698
|
+
ref: index === visualSelected ? ref : void 0,
|
|
1528
1699
|
role: index === visualSelected ? "tabpanel" : void 0,
|
|
1529
1700
|
style: mergedStyle,
|
|
1530
1701
|
tabIndex: index === visualSelected ? 0 : void 0
|
|
@@ -1535,9 +1706,11 @@ const TabsetContent = ({ children, style, ...props }) => {
|
|
|
1535
1706
|
const active = node.children[visualSelected];
|
|
1536
1707
|
if (active) return /* @__PURE__ */ jsx("div", {
|
|
1537
1708
|
...props,
|
|
1538
|
-
"aria-
|
|
1709
|
+
"aria-label": editingTabId === active.id ? active.name : void 0,
|
|
1710
|
+
"aria-labelledby": editingTabId === active.id ? void 0 : tabDomId(node.id, active.id),
|
|
1539
1711
|
"data-dashfoo": "tabcontent",
|
|
1540
1712
|
id: panelDomId(node.id),
|
|
1713
|
+
ref,
|
|
1541
1714
|
role: "tabpanel",
|
|
1542
1715
|
style: mergedStyle,
|
|
1543
1716
|
tabIndex: 0,
|
|
@@ -1549,12 +1722,14 @@ const TabsetContent = ({ children, style, ...props }) => {
|
|
|
1549
1722
|
return /* @__PURE__ */ jsx("div", {
|
|
1550
1723
|
...props,
|
|
1551
1724
|
"data-dashfoo": "tabcontent",
|
|
1725
|
+
ref,
|
|
1552
1726
|
style: mergedStyle
|
|
1553
1727
|
});
|
|
1554
|
-
};
|
|
1728
|
+
});
|
|
1729
|
+
TabsetContent.displayName = "TabsetContent";
|
|
1555
1730
|
|
|
1556
1731
|
//#endregion
|
|
1557
|
-
//#region src/components/tab-overflow.tsx
|
|
1732
|
+
//#region src/components/tab-overflow-menu.tsx
|
|
1558
1733
|
const OverflowIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
1559
1734
|
"aria-hidden": "true",
|
|
1560
1735
|
fill: "currentColor",
|
|
@@ -1642,8 +1817,6 @@ const TabOverflowMenu = ({ items, onSelect }) => {
|
|
|
1642
1817
|
case "Escape":
|
|
1643
1818
|
event.preventDefault();
|
|
1644
1819
|
closeAndReturnFocus();
|
|
1645
|
-
break;
|
|
1646
|
-
default: break;
|
|
1647
1820
|
}
|
|
1648
1821
|
};
|
|
1649
1822
|
const handleFocusOut = (event) => {
|
|
@@ -1689,7 +1862,7 @@ const TabOverflowMenu = ({ items, onSelect }) => {
|
|
|
1689
1862
|
};
|
|
1690
1863
|
|
|
1691
1864
|
//#endregion
|
|
1692
|
-
//#region src/components/tabset/tabset-overflow.tsx
|
|
1865
|
+
//#region src/components/tabset/tabset-overflow-menu.tsx
|
|
1693
1866
|
const TabsetOverflowMenu = () => {
|
|
1694
1867
|
const overflowItems = useTabset((state) => state.overflowItems);
|
|
1695
1868
|
const selectOverflowTab = useTabset((state) => state.selectOverflowTab);
|
|
@@ -1700,6 +1873,90 @@ const TabsetOverflowMenu = () => {
|
|
|
1700
1873
|
});
|
|
1701
1874
|
};
|
|
1702
1875
|
|
|
1876
|
+
//#endregion
|
|
1877
|
+
//#region src/lib/merge-refs.ts
|
|
1878
|
+
const applyRef = (ref, value) => {
|
|
1879
|
+
if (typeof ref === "function") ref(value);
|
|
1880
|
+
else if (ref) ref.current = value;
|
|
1881
|
+
};
|
|
1882
|
+
const mergeRefs = (...refs) => (value) => {
|
|
1883
|
+
for (const ref of refs) applyRef(ref, value);
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
//#endregion
|
|
1887
|
+
//#region src/components/tabset/tabset-rename-input.tsx
|
|
1888
|
+
const RenameEditor = forwardRef(({ defaultValue, onBlur, onKeyDown, ...props }, userRef) => {
|
|
1889
|
+
const { tab } = useTab();
|
|
1890
|
+
const cancelRename = useTabset((state) => state.cancelRename);
|
|
1891
|
+
const commitRename = useTabset((state) => state.commitRename);
|
|
1892
|
+
const inputRef = useRef(null);
|
|
1893
|
+
const rename = useInlineRename({
|
|
1894
|
+
currentName: tab.name,
|
|
1895
|
+
inputRef,
|
|
1896
|
+
onCommit: (name) => {
|
|
1897
|
+
commitRename(tab.id, name);
|
|
1898
|
+
},
|
|
1899
|
+
onDone: cancelRename
|
|
1900
|
+
});
|
|
1901
|
+
const refCallback = useMemo(() => mergeRefs(inputRef, userRef), [userRef]);
|
|
1902
|
+
const handleKeyDown = (event) => {
|
|
1903
|
+
onKeyDown?.(event);
|
|
1904
|
+
rename.handleKeyDown(event);
|
|
1905
|
+
};
|
|
1906
|
+
const handleCommitOnBlur = (event) => {
|
|
1907
|
+
onBlur?.(event);
|
|
1908
|
+
rename.handleBlur(event);
|
|
1909
|
+
};
|
|
1910
|
+
return /* @__PURE__ */ jsx("input", {
|
|
1911
|
+
"aria-label": `Rename ${tab.name}`,
|
|
1912
|
+
...props,
|
|
1913
|
+
"data-dashfoo": "tab-rename",
|
|
1914
|
+
defaultValue: defaultValue ?? tab.name,
|
|
1915
|
+
onBlur: handleCommitOnBlur,
|
|
1916
|
+
onKeyDown: handleKeyDown,
|
|
1917
|
+
ref: refCallback,
|
|
1918
|
+
type: "text"
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
RenameEditor.displayName = "RenameEditor";
|
|
1922
|
+
const TabsetRenameInput = forwardRef((props, ref) => {
|
|
1923
|
+
const { tab } = useTab();
|
|
1924
|
+
const editing = useTabset((state) => state.editingTabId === tab.id);
|
|
1925
|
+
const registerRenameInput = useTabset((state) => state.registerRenameInput);
|
|
1926
|
+
useEffect(() => registerRenameInput(), [registerRenameInput]);
|
|
1927
|
+
if (!editing) return null;
|
|
1928
|
+
return /* @__PURE__ */ jsx(RenameEditor, {
|
|
1929
|
+
...props,
|
|
1930
|
+
ref
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
TabsetRenameInput.displayName = "TabsetRenameInput";
|
|
1934
|
+
const TabsetCloseButton = forwardRef(({ children, onClick, ...props }, ref) => {
|
|
1935
|
+
const { index, tab } = useTab();
|
|
1936
|
+
const closeTab = useTabset((state) => state.closeTab);
|
|
1937
|
+
const editing = useTabset((state) => state.editingTabId === tab.id);
|
|
1938
|
+
const tabsClosable = useTabset((state) => state.tabsClosable);
|
|
1939
|
+
const visualSelected = useTabset((state) => state.visualSelected);
|
|
1940
|
+
if (!(tabsClosable && tab.enableClose !== false) || editing) return null;
|
|
1941
|
+
const handleClick = (event) => {
|
|
1942
|
+
onClick?.(event);
|
|
1943
|
+
event.stopPropagation();
|
|
1944
|
+
closeTab(tab.id);
|
|
1945
|
+
};
|
|
1946
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1947
|
+
"aria-label": `Close ${tab.name}`,
|
|
1948
|
+
title: `Close ${tab.name}`,
|
|
1949
|
+
...props,
|
|
1950
|
+
"data-dashfoo": "tab-close",
|
|
1951
|
+
onClick: handleClick,
|
|
1952
|
+
ref,
|
|
1953
|
+
tabIndex: index === visualSelected ? 0 : -1,
|
|
1954
|
+
type: "button",
|
|
1955
|
+
children: children ?? /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1956
|
+
});
|
|
1957
|
+
});
|
|
1958
|
+
TabsetCloseButton.displayName = "TabsetCloseButton";
|
|
1959
|
+
|
|
1703
1960
|
//#endregion
|
|
1704
1961
|
//#region src/hooks/use-tab-overflow.ts
|
|
1705
1962
|
const overflowingIds = (tablist) => {
|
|
@@ -1711,13 +1968,14 @@ const overflowingIds = (tablist) => {
|
|
|
1711
1968
|
return clipped && tabId !== void 0 && tabId !== "" ? [tabId] : [];
|
|
1712
1969
|
});
|
|
1713
1970
|
};
|
|
1971
|
+
const sameIds = (a, b) => a.length === b.length && a.every((id, index) => id === b[index]);
|
|
1714
1972
|
const useTabOverflow = (tablist, signature) => {
|
|
1715
1973
|
const [overflow, setOverflow] = useState([]);
|
|
1716
1974
|
useEffect(() => {
|
|
1717
1975
|
const recompute = () => {
|
|
1718
1976
|
setOverflow((previous) => {
|
|
1719
1977
|
const next = tablist ? overflowingIds(tablist) : [];
|
|
1720
|
-
return
|
|
1978
|
+
return sameIds(previous, next) ? previous : next;
|
|
1721
1979
|
});
|
|
1722
1980
|
};
|
|
1723
1981
|
recompute();
|
|
@@ -1747,7 +2005,7 @@ const tabsetStyle = {
|
|
|
1747
2005
|
minWidth: 0,
|
|
1748
2006
|
width: "100%"
|
|
1749
2007
|
};
|
|
1750
|
-
const TabsetRoot = ({ children, node,
|
|
2008
|
+
const TabsetRoot = forwardRef(({ children, node, style, ...props }, userRef) => {
|
|
1751
2009
|
const closableTabs = useLayout((state) => state.closableTabs);
|
|
1752
2010
|
const dispatch = useLayout((state) => state.dispatch);
|
|
1753
2011
|
const maximizable = useLayout((state) => state.maximizable);
|
|
@@ -1779,7 +2037,9 @@ const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
|
|
|
1779
2037
|
useLayoutEffect(() => {
|
|
1780
2038
|
store.setState(snapshot);
|
|
1781
2039
|
});
|
|
1782
|
-
const
|
|
2040
|
+
const tablistElement = useStore(store, (state) => state.tablistElement);
|
|
2041
|
+
const overflowSignature = node.children.map((tab) => `${tab.id}:${tab.name}`).join("|");
|
|
2042
|
+
const overflow = useTabOverflow(tablistElement, overflowSignature);
|
|
1783
2043
|
const overflowItems = useMemo(() => {
|
|
1784
2044
|
const names = new Map(node.children.map((tab) => [tab.id, tab.name]));
|
|
1785
2045
|
return overflow.map((id) => ({
|
|
@@ -1791,8 +2051,10 @@ const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
|
|
|
1791
2051
|
store.setState({ overflowItems });
|
|
1792
2052
|
}, [overflowItems, store]);
|
|
1793
2053
|
useEffect(() => {
|
|
1794
|
-
|
|
1795
|
-
|
|
2054
|
+
const { pendingCloseTabId } = store.getState();
|
|
2055
|
+
if (pendingCloseTabId === null) return;
|
|
2056
|
+
store.setState({ pendingCloseTabId: null });
|
|
2057
|
+
if (node.children.some((tab) => tab.id === pendingCloseTabId)) return;
|
|
1796
2058
|
const activeTabId = node.children[node.selected]?.id;
|
|
1797
2059
|
if (activeTabId) tabsetRef.current?.querySelector(`#${CSS.escape(tabDomId(node.id, activeTabId))}`)?.focus();
|
|
1798
2060
|
else tabsetRef.current?.focus();
|
|
@@ -1823,21 +2085,12 @@ const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
|
|
|
1823
2085
|
children
|
|
1824
2086
|
})
|
|
1825
2087
|
});
|
|
1826
|
-
};
|
|
1827
|
-
|
|
1828
|
-
//#endregion
|
|
1829
|
-
//#region src/lib/merge-refs.ts
|
|
1830
|
-
const applyRef = (ref, value) => {
|
|
1831
|
-
if (typeof ref === "function") ref(value);
|
|
1832
|
-
else if (ref) ref.current = value;
|
|
1833
|
-
};
|
|
1834
|
-
const mergeRefs = (...refs) => (value) => {
|
|
1835
|
-
for (const ref of refs) applyRef(ref, value);
|
|
1836
|
-
};
|
|
2088
|
+
});
|
|
2089
|
+
TabsetRoot.displayName = "TabsetRoot";
|
|
1837
2090
|
|
|
1838
2091
|
//#endregion
|
|
1839
2092
|
//#region src/components/tabset/tabset-tab.tsx
|
|
1840
|
-
const TabsetTab = ({ children,
|
|
2093
|
+
const TabsetTab = forwardRef(({ children, tab, ...props }, userRef) => {
|
|
1841
2094
|
const store = useContext(TabsetStoreContext);
|
|
1842
2095
|
const node = useTabset((state) => state.node);
|
|
1843
2096
|
const editing = useTabset((state) => state.editingTabId === tab.id);
|
|
@@ -1872,8 +2125,9 @@ const TabsetTab = ({ children, ref: userRef, tab, ...props }) => {
|
|
|
1872
2125
|
children
|
|
1873
2126
|
})
|
|
1874
2127
|
});
|
|
1875
|
-
};
|
|
1876
|
-
|
|
2128
|
+
});
|
|
2129
|
+
TabsetTab.displayName = "TabsetTab";
|
|
2130
|
+
const TabsetTrigger = forwardRef(({ children, onClick, onDoubleClick, ...props }, userRef) => {
|
|
1877
2131
|
const { index, tab } = useTab();
|
|
1878
2132
|
const node = useTabset((state) => state.node);
|
|
1879
2133
|
const editing = useTabset((state) => state.editingTabId === tab.id);
|
|
@@ -1912,163 +2166,8 @@ const TabsetTrigger = ({ children, onClick, onDoubleClick, ref: userRef, ...prop
|
|
|
1912
2166
|
type: "button",
|
|
1913
2167
|
children: children ?? (renderTabLabel ? renderTabLabel(tab) : tab.name)
|
|
1914
2168
|
});
|
|
1915
|
-
};
|
|
1916
|
-
|
|
1917
|
-
//#endregion
|
|
1918
|
-
//#region src/components/tabset-icons.tsx
|
|
1919
|
-
const iconStyle = { pointerEvents: "none" };
|
|
1920
|
-
const Icon = ({ children }) => /* @__PURE__ */ jsx("svg", {
|
|
1921
|
-
"aria-hidden": "true",
|
|
1922
|
-
fill: "none",
|
|
1923
|
-
height: "12",
|
|
1924
|
-
stroke: "currentColor",
|
|
1925
|
-
strokeLinecap: "round",
|
|
1926
|
-
strokeLinejoin: "round",
|
|
1927
|
-
strokeWidth: "2",
|
|
1928
|
-
style: iconStyle,
|
|
1929
|
-
viewBox: "0 0 24 24",
|
|
1930
|
-
width: "12",
|
|
1931
|
-
children
|
|
1932
2169
|
});
|
|
1933
|
-
|
|
1934
|
-
const GripIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1935
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1936
|
-
cx: "12",
|
|
1937
|
-
cy: "5",
|
|
1938
|
-
r: "1"
|
|
1939
|
-
}),
|
|
1940
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1941
|
-
cx: "19",
|
|
1942
|
-
cy: "5",
|
|
1943
|
-
r: "1"
|
|
1944
|
-
}),
|
|
1945
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1946
|
-
cx: "5",
|
|
1947
|
-
cy: "5",
|
|
1948
|
-
r: "1"
|
|
1949
|
-
}),
|
|
1950
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1951
|
-
cx: "12",
|
|
1952
|
-
cy: "12",
|
|
1953
|
-
r: "1"
|
|
1954
|
-
}),
|
|
1955
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1956
|
-
cx: "19",
|
|
1957
|
-
cy: "12",
|
|
1958
|
-
r: "1"
|
|
1959
|
-
}),
|
|
1960
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1961
|
-
cx: "5",
|
|
1962
|
-
cy: "12",
|
|
1963
|
-
r: "1"
|
|
1964
|
-
}),
|
|
1965
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1966
|
-
cx: "12",
|
|
1967
|
-
cy: "19",
|
|
1968
|
-
r: "1"
|
|
1969
|
-
}),
|
|
1970
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1971
|
-
cx: "19",
|
|
1972
|
-
cy: "19",
|
|
1973
|
-
r: "1"
|
|
1974
|
-
}),
|
|
1975
|
-
/* @__PURE__ */ jsx("circle", {
|
|
1976
|
-
cx: "5",
|
|
1977
|
-
cy: "19",
|
|
1978
|
-
r: "1"
|
|
1979
|
-
})
|
|
1980
|
-
] });
|
|
1981
|
-
const MaximizeIcon = ({ maximized }) => maximized ? /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1982
|
-
/* @__PURE__ */ jsx("path", { d: "m14 10 7-7" }),
|
|
1983
|
-
/* @__PURE__ */ jsx("path", { d: "M20 10h-6V4" }),
|
|
1984
|
-
/* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
|
|
1985
|
-
/* @__PURE__ */ jsx("path", { d: "M4 14h6v6" })
|
|
1986
|
-
] }) : /* @__PURE__ */ jsxs(Icon, { children: [
|
|
1987
|
-
/* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
|
|
1988
|
-
/* @__PURE__ */ jsx("path", { d: "m21 3-7 7" }),
|
|
1989
|
-
/* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
|
|
1990
|
-
/* @__PURE__ */ jsx("path", { d: "M9 21H3v-6" })
|
|
1991
|
-
] });
|
|
1992
|
-
const FloatIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("rect", {
|
|
1993
|
-
height: "14",
|
|
1994
|
-
rx: "2",
|
|
1995
|
-
width: "18",
|
|
1996
|
-
x: "3",
|
|
1997
|
-
y: "6"
|
|
1998
|
-
}), /* @__PURE__ */ jsx("path", { d: "M3 10h18" })] });
|
|
1999
|
-
const MinimizeIcon = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
|
|
2000
|
-
const DockIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
|
|
2001
|
-
/* @__PURE__ */ jsx("path", { d: "M3 7V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2" }),
|
|
2002
|
-
/* @__PURE__ */ jsx("path", { d: "M11 8 7 12l4 4" }),
|
|
2003
|
-
/* @__PURE__ */ jsx("path", { d: "M7 12h10" })
|
|
2004
|
-
] });
|
|
2005
|
-
|
|
2006
|
-
//#endregion
|
|
2007
|
-
//#region src/components/tabset/tabset-tab-controls.tsx
|
|
2008
|
-
const RenameEditor = ({ defaultValue, onBlur, onKeyDown, ref: userRef, ...props }) => {
|
|
2009
|
-
const { tab } = useTab();
|
|
2010
|
-
const cancelRename = useTabset((state) => state.cancelRename);
|
|
2011
|
-
const commitRename = useTabset((state) => state.commitRename);
|
|
2012
|
-
const inputRef = useRef(null);
|
|
2013
|
-
const rename = useInlineRename({
|
|
2014
|
-
currentName: tab.name,
|
|
2015
|
-
inputRef,
|
|
2016
|
-
onCommit: (name) => {
|
|
2017
|
-
commitRename(tab.id, name);
|
|
2018
|
-
},
|
|
2019
|
-
onDone: cancelRename
|
|
2020
|
-
});
|
|
2021
|
-
const refCallback = useMemo(() => mergeRefs(inputRef, userRef), [userRef]);
|
|
2022
|
-
const handleKeyDown = (event) => {
|
|
2023
|
-
onKeyDown?.(event);
|
|
2024
|
-
rename.handleKeyDown(event);
|
|
2025
|
-
};
|
|
2026
|
-
const handleCommitOnBlur = (event) => {
|
|
2027
|
-
onBlur?.(event);
|
|
2028
|
-
rename.handleBlur(event);
|
|
2029
|
-
};
|
|
2030
|
-
return /* @__PURE__ */ jsx("input", {
|
|
2031
|
-
"aria-label": `Rename ${tab.name}`,
|
|
2032
|
-
...props,
|
|
2033
|
-
"data-dashfoo": "tab-rename",
|
|
2034
|
-
defaultValue: defaultValue ?? tab.name,
|
|
2035
|
-
onBlur: handleCommitOnBlur,
|
|
2036
|
-
onKeyDown: handleKeyDown,
|
|
2037
|
-
ref: refCallback,
|
|
2038
|
-
type: "text"
|
|
2039
|
-
});
|
|
2040
|
-
};
|
|
2041
|
-
const TabsetRenameInput = (props) => {
|
|
2042
|
-
const { tab } = useTab();
|
|
2043
|
-
const editing = useTabset((state) => state.editingTabId === tab.id);
|
|
2044
|
-
const registerRenameInput = useTabset((state) => state.registerRenameInput);
|
|
2045
|
-
useEffect(() => registerRenameInput(), [registerRenameInput]);
|
|
2046
|
-
if (!editing) return null;
|
|
2047
|
-
return /* @__PURE__ */ jsx(RenameEditor, { ...props });
|
|
2048
|
-
};
|
|
2049
|
-
const TabsetCloseButton = ({ children, onClick, ...props }) => {
|
|
2050
|
-
const { index, tab } = useTab();
|
|
2051
|
-
const closeTab = useTabset((state) => state.closeTab);
|
|
2052
|
-
const editing = useTabset((state) => state.editingTabId === tab.id);
|
|
2053
|
-
const tabsClosable = useTabset((state) => state.tabsClosable);
|
|
2054
|
-
const visualSelected = useTabset((state) => state.visualSelected);
|
|
2055
|
-
if (!(tabsClosable && tab.enableClose !== false) || editing) return null;
|
|
2056
|
-
const handleClick = (event) => {
|
|
2057
|
-
onClick?.(event);
|
|
2058
|
-
event.stopPropagation();
|
|
2059
|
-
closeTab(tab.id);
|
|
2060
|
-
};
|
|
2061
|
-
return /* @__PURE__ */ jsx("button", {
|
|
2062
|
-
"aria-label": `Close ${tab.name}`,
|
|
2063
|
-
title: `Close ${tab.name}`,
|
|
2064
|
-
...props,
|
|
2065
|
-
"data-dashfoo": "tab-close",
|
|
2066
|
-
onClick: handleClick,
|
|
2067
|
-
tabIndex: index === visualSelected ? 0 : -1,
|
|
2068
|
-
type: "button",
|
|
2069
|
-
children: children ?? /* @__PURE__ */ jsx(CloseIcon, {})
|
|
2070
|
-
});
|
|
2071
|
-
};
|
|
2170
|
+
TabsetTrigger.displayName = "TabsetTrigger";
|
|
2072
2171
|
|
|
2073
2172
|
//#endregion
|
|
2074
2173
|
//#region src/components/tabset/tabset-tablist.tsx
|
|
@@ -2081,50 +2180,60 @@ const tablistStyle = {
|
|
|
2081
2180
|
minWidth: 0,
|
|
2082
2181
|
overflowX: "auto"
|
|
2083
2182
|
};
|
|
2084
|
-
const TabsetTabStrip = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
2183
|
+
const TabsetTabStrip = forwardRef(({ style, ...props }, ref) => /* @__PURE__ */ jsx("div", {
|
|
2085
2184
|
...props,
|
|
2086
2185
|
"data-dashfoo": "tabstrip",
|
|
2186
|
+
ref,
|
|
2087
2187
|
style: {
|
|
2088
2188
|
...stripStyle,
|
|
2089
2189
|
...style
|
|
2090
2190
|
}
|
|
2091
|
-
});
|
|
2092
|
-
|
|
2191
|
+
}));
|
|
2192
|
+
TabsetTabStrip.displayName = "TabsetTabStrip";
|
|
2193
|
+
const TabsetTablist = forwardRef(({ "aria-label": ariaLabel, children, onKeyDown, style, ...props }, userRef) => {
|
|
2093
2194
|
const node = useTabset((state) => state.node);
|
|
2094
2195
|
const registerTablist = useTabset((state) => state.registerTablist);
|
|
2095
2196
|
const selectTab = useTabset((state) => state.selectTab);
|
|
2197
|
+
const editingTabId = useTabset((state) => state.editingTabId);
|
|
2198
|
+
const visualSelected = useTabset((state) => state.visualSelected);
|
|
2096
2199
|
const handleKeyDown = (event) => {
|
|
2097
2200
|
onKeyDown?.(event);
|
|
2098
2201
|
if (event.defaultPrevented) return;
|
|
2099
2202
|
if (!(event.target instanceof HTMLElement) || !event.target.closest("[role=\"tab\"]")) return;
|
|
2100
2203
|
const count = node.children.length;
|
|
2101
2204
|
if (count === 0) return;
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2205
|
+
const from = Math.max(visualSelected, 0);
|
|
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);
|
|
2108
2212
|
if (next === void 0) return;
|
|
2109
2213
|
event.preventDefault();
|
|
2110
2214
|
selectTab(next, { focus: true });
|
|
2111
2215
|
};
|
|
2112
2216
|
const refCallback = useMemo(() => mergeRefs(registerTablist, userRef), [registerTablist, userRef]);
|
|
2113
|
-
|
|
2114
|
-
|
|
2217
|
+
const ownedTabs = node.children.filter((tab) => tab.id !== editingTabId).map((tab) => tabDomId(node.id, tab.id)).join(" ");
|
|
2218
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2115
2219
|
...props,
|
|
2116
|
-
"aria-orientation": "horizontal",
|
|
2117
2220
|
"data-dashfoo": "tablist",
|
|
2118
2221
|
onKeyDown: handleKeyDown,
|
|
2119
2222
|
ref: refCallback,
|
|
2120
|
-
role: "tablist",
|
|
2121
2223
|
style: {
|
|
2122
2224
|
...tablistStyle,
|
|
2123
2225
|
...style
|
|
2124
2226
|
},
|
|
2125
|
-
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]
|
|
2126
2234
|
});
|
|
2127
|
-
};
|
|
2235
|
+
});
|
|
2236
|
+
TabsetTablist.displayName = "TabsetTablist";
|
|
2128
2237
|
|
|
2129
2238
|
//#endregion
|
|
2130
2239
|
//#region src/components/tabset/tabset-toolbar.tsx
|
|
@@ -2134,22 +2243,42 @@ const toolbarStyle = {
|
|
|
2134
2243
|
flexShrink: 0,
|
|
2135
2244
|
marginInlineStart: "auto"
|
|
2136
2245
|
};
|
|
2137
|
-
const TabsetToolbar = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
2246
|
+
const TabsetToolbar = forwardRef(({ style, ...props }, ref) => /* @__PURE__ */ jsx("div", {
|
|
2138
2247
|
...props,
|
|
2139
2248
|
"data-dashfoo": "tabset-toolbar",
|
|
2249
|
+
ref,
|
|
2140
2250
|
style: {
|
|
2141
2251
|
...toolbarStyle,
|
|
2142
2252
|
...style
|
|
2143
2253
|
}
|
|
2144
|
-
});
|
|
2145
|
-
|
|
2146
|
-
|
|
2254
|
+
}));
|
|
2255
|
+
TabsetToolbar.displayName = "TabsetToolbar";
|
|
2256
|
+
/**
|
|
2257
|
+
* One answer per control, shared by the controls and by the layout deciding
|
|
2258
|
+
* whether to render a toolbar at all, so the two cannot disagree.
|
|
2259
|
+
*/
|
|
2260
|
+
const useTabsetChrome = () => {
|
|
2261
|
+
const draggableTabsets = useLayout((state) => state.draggableTabsets);
|
|
2262
|
+
const floatable = useLayout((state) => state.floatable);
|
|
2147
2263
|
const isMaximized = useTabset((state) => state.isMaximized);
|
|
2264
|
+
const overflowCount = useTabset((state) => state.overflowItems.length);
|
|
2265
|
+
const showMaximize = useTabset((state) => state.showMaximize);
|
|
2266
|
+
const hasFloatLayer = useHasFloatLayer();
|
|
2267
|
+
if (floatable && !isMaximized && !hasFloatLayer) warnOnce("float-no-layer", "Tabset.FloatButton needs a <Layout.FloatLayer> around the layout (DashfooLayout adds one); the float control is hidden");
|
|
2268
|
+
return {
|
|
2269
|
+
float: floatable && !isMaximized && hasFloatLayer,
|
|
2270
|
+
grip: draggableTabsets && !isMaximized,
|
|
2271
|
+
maximize: showMaximize,
|
|
2272
|
+
overflow: overflowCount > 0
|
|
2273
|
+
};
|
|
2274
|
+
};
|
|
2275
|
+
const TabsetGrip = forwardRef(({ children, ...props }, userRef) => {
|
|
2276
|
+
const activeTabName = useTabset((state) => state.activeTab?.name ?? "");
|
|
2148
2277
|
const node = useTabset((state) => state.node);
|
|
2149
|
-
const
|
|
2150
|
-
const { ref } = useTabsetDraggable(node.id,
|
|
2278
|
+
const { grip } = useTabsetChrome();
|
|
2279
|
+
const { ref } = useTabsetDraggable(node.id, !grip, activeTabName);
|
|
2151
2280
|
const refCallback = useMemo(() => mergeRefs(ref, userRef), [ref, userRef]);
|
|
2152
|
-
if (
|
|
2281
|
+
if (!grip) return null;
|
|
2153
2282
|
return /* @__PURE__ */ jsx("button", {
|
|
2154
2283
|
"aria-label": "Move tabset",
|
|
2155
2284
|
title: "Move tabset",
|
|
@@ -2159,12 +2288,13 @@ const TabsetGrip = ({ children, ref: userRef, ...props }) => {
|
|
|
2159
2288
|
type: "button",
|
|
2160
2289
|
children: children ?? /* @__PURE__ */ jsx(GripIcon, {})
|
|
2161
2290
|
});
|
|
2162
|
-
};
|
|
2163
|
-
|
|
2291
|
+
});
|
|
2292
|
+
TabsetGrip.displayName = "TabsetGrip";
|
|
2293
|
+
const TabsetMaximizeButton = forwardRef(({ children, onClick, ...props }, ref) => {
|
|
2164
2294
|
const isMaximized = useTabset((state) => state.isMaximized);
|
|
2165
|
-
const showMaximize = useTabset((state) => state.showMaximize);
|
|
2166
2295
|
const toggleMaximize = useTabset((state) => state.toggleMaximize);
|
|
2167
|
-
|
|
2296
|
+
const { maximize } = useTabsetChrome();
|
|
2297
|
+
if (!maximize) return null;
|
|
2168
2298
|
const handleClick = (event) => {
|
|
2169
2299
|
onClick?.(event);
|
|
2170
2300
|
toggleMaximize();
|
|
@@ -2176,21 +2306,17 @@ const TabsetMaximizeButton = ({ children, onClick, ...props }) => {
|
|
|
2176
2306
|
"aria-pressed": isMaximized,
|
|
2177
2307
|
"data-dashfoo": "tabset-maximize",
|
|
2178
2308
|
onClick: handleClick,
|
|
2309
|
+
ref,
|
|
2179
2310
|
type: "button",
|
|
2180
2311
|
children: children ?? /* @__PURE__ */ jsx(MaximizeIcon, { maximized: isMaximized })
|
|
2181
2312
|
});
|
|
2182
|
-
};
|
|
2183
|
-
|
|
2184
|
-
|
|
2313
|
+
});
|
|
2314
|
+
TabsetMaximizeButton.displayName = "TabsetMaximizeButton";
|
|
2315
|
+
const TabsetFloatButton = forwardRef(({ children, onClick, ...props }, ref) => {
|
|
2185
2316
|
const dispatch = useLayout((state) => state.dispatch);
|
|
2186
|
-
const isMaximized = useTabset((state) => state.isMaximized);
|
|
2187
2317
|
const node = useTabset((state) => state.node);
|
|
2188
|
-
const
|
|
2189
|
-
if (!
|
|
2190
|
-
if (!hasFloatLayer) {
|
|
2191
|
-
warnOnce("float-no-layer", "Tabset.FloatButton needs a <Layout.FloatLayer> around the layout (DashfooLayout adds one); the float control is hidden");
|
|
2192
|
-
return null;
|
|
2193
|
-
}
|
|
2318
|
+
const { float } = useTabsetChrome();
|
|
2319
|
+
if (!float) return null;
|
|
2194
2320
|
const handleClick = (event) => {
|
|
2195
2321
|
onClick?.(event);
|
|
2196
2322
|
const geometry = measureFloatRect(event.currentTarget);
|
|
@@ -2206,24 +2332,23 @@ const TabsetFloatButton = ({ children, onClick, ...props }) => {
|
|
|
2206
2332
|
...props,
|
|
2207
2333
|
"data-dashfoo": "tabset-float",
|
|
2208
2334
|
onClick: handleClick,
|
|
2335
|
+
ref,
|
|
2209
2336
|
type: "button",
|
|
2210
2337
|
children: children ?? /* @__PURE__ */ jsx(FloatIcon, {})
|
|
2211
2338
|
});
|
|
2212
|
-
};
|
|
2339
|
+
});
|
|
2340
|
+
TabsetFloatButton.displayName = "TabsetFloatButton";
|
|
2213
2341
|
|
|
2214
2342
|
//#endregion
|
|
2215
2343
|
//#region src/components/tabset/tabset-view.tsx
|
|
2216
2344
|
const tabsetContent = /* @__PURE__ */ jsx(TabsetContent, {});
|
|
2217
2345
|
const DefaultTabsetLayout = () => {
|
|
2218
|
-
const draggableTabsets = useLayout((state) => state.draggableTabsets);
|
|
2219
|
-
const floatable = useLayout((state) => state.floatable);
|
|
2220
2346
|
const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
|
|
2221
2347
|
const tabLocation = useLayout((state) => state.tabLocation);
|
|
2222
2348
|
const tabStripEnabled = useLayout((state) => state.tabStripEnabled);
|
|
2223
|
-
const isMaximized = useTabset((state) => state.isMaximized);
|
|
2224
2349
|
const node = useTabset((state) => state.node);
|
|
2225
|
-
const
|
|
2226
|
-
const showToolbar =
|
|
2350
|
+
const chrome = useTabsetChrome();
|
|
2351
|
+
const showToolbar = renderTabsetToolbar !== void 0 || Object.values(chrome).some(Boolean);
|
|
2227
2352
|
const strip = tabStripEnabled ? /* @__PURE__ */ jsxs(TabsetTabStrip, { children: [/* @__PURE__ */ jsx(TabsetTablist, { children: node.children.map((tab) => /* @__PURE__ */ jsxs(TabsetTab, {
|
|
2228
2353
|
tab,
|
|
2229
2354
|
children: [
|
|
@@ -2275,7 +2400,8 @@ const RowView = ({ node, renderTabset }) => {
|
|
|
2275
2400
|
const tabsetMinSize = useLayout((state) => state.tabsetMinSize);
|
|
2276
2401
|
const globalSnap = useLayout((state) => state.snap);
|
|
2277
2402
|
const effectiveSnap = node.snap ?? globalSnap;
|
|
2278
|
-
const
|
|
2403
|
+
const snapGrid = useMemo(() => resolveSnapGrid(effectiveSnap, node.children.length), [effectiveSnap, node.children.length]);
|
|
2404
|
+
const snapActive = snapEnabled(snapGrid);
|
|
2279
2405
|
const orientation = node.orientation === "row" ? "horizontal" : "vertical";
|
|
2280
2406
|
const total = node.children.reduce((sum, child) => sum + child.weight, 0);
|
|
2281
2407
|
const { beginBoundaryDrag, groupRef, onLayoutChange, onLayoutChanged, snappedBoundary } = useSnapResize({
|
|
@@ -2288,7 +2414,7 @@ const RowView = ({ node, renderTabset }) => {
|
|
|
2288
2414
|
});
|
|
2289
2415
|
},
|
|
2290
2416
|
resizableSplits,
|
|
2291
|
-
snap:
|
|
2417
|
+
snap: snapGrid
|
|
2292
2418
|
});
|
|
2293
2419
|
return /* @__PURE__ */ jsx(Group, {
|
|
2294
2420
|
"data-dashfoo": "row",
|
|
@@ -2589,8 +2715,8 @@ const Layout = {
|
|
|
2589
2715
|
//#endregion
|
|
2590
2716
|
//#region src/components/dashfoo-layout.tsx
|
|
2591
2717
|
const DEFAULT_PERSIST_DEBOUNCE_MS = 300;
|
|
2592
|
-
const resolvePersist = (persist
|
|
2593
|
-
if (persist === void 0
|
|
2718
|
+
const resolvePersist = (persist) => {
|
|
2719
|
+
if (persist === void 0) return null;
|
|
2594
2720
|
if (typeof persist === "string") return {
|
|
2595
2721
|
debounceMs: DEFAULT_PERSIST_DEBOUNCE_MS,
|
|
2596
2722
|
key: persist,
|
|
@@ -2605,7 +2731,8 @@ const resolvePersist = (persist, hasDefaultModel) => {
|
|
|
2605
2731
|
const warnedComponents = /* @__PURE__ */ new Set();
|
|
2606
2732
|
const DashfooLayout = forwardRef((props, ref) => {
|
|
2607
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;
|
|
2608
|
-
const
|
|
2734
|
+
const persistConfig = useMemo(() => model === void 0 ? resolvePersist(persist) : null, [model, persist]);
|
|
2735
|
+
const persistence = usePersistence(persistConfig, defaultModel);
|
|
2609
2736
|
const handleModelChange = useCallback((next, action) => {
|
|
2610
2737
|
persistence.save(next);
|
|
2611
2738
|
onModelChange?.(next, action);
|
|
@@ -2618,6 +2745,14 @@ const DashfooLayout = forwardRef((props, ref) => {
|
|
|
2618
2745
|
onMaximizedTabsetChange,
|
|
2619
2746
|
onModelChange: handleModelChange
|
|
2620
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]);
|
|
2621
2756
|
const hasSharedManager = useContext(SharedDragManagerContext) !== null;
|
|
2622
2757
|
useImperativeHandle(ref, () => ({
|
|
2623
2758
|
addTab: (tab, target) => {
|
|
@@ -2694,16 +2829,36 @@ const DashfooLayout = forwardRef((props, ref) => {
|
|
|
2694
2829
|
}
|
|
2695
2830
|
return /* @__PURE__ */ jsx(Component, { node: tab });
|
|
2696
2831
|
}, [components, factory]);
|
|
2832
|
+
const renderers = useMemo(() => ({
|
|
2833
|
+
tab: renderTab,
|
|
2834
|
+
tabLabel: renderTabLabel,
|
|
2835
|
+
tabsetToolbar: renderTabsetToolbar
|
|
2836
|
+
}), [
|
|
2837
|
+
renderTab,
|
|
2838
|
+
renderTabLabel,
|
|
2839
|
+
renderTabsetToolbar
|
|
2840
|
+
]);
|
|
2697
2841
|
const [containerRef, width] = useContainerWidth();
|
|
2698
2842
|
const isCompact = responsive !== void 0 && width <= responsive.maxWidth;
|
|
2699
|
-
|
|
2843
|
+
/** Everything the compact breakpoint changes, decided once. */
|
|
2844
|
+
const presentation = useMemo(() => isCompact ? {
|
|
2845
|
+
maximizable: false,
|
|
2846
|
+
model: compactModel(store.model, responsive?.orientation),
|
|
2847
|
+
restructurable: false
|
|
2848
|
+
} : {
|
|
2849
|
+
maximizable,
|
|
2850
|
+
model: store.model,
|
|
2851
|
+
restructurable: true
|
|
2852
|
+
}, [
|
|
2700
2853
|
isCompact,
|
|
2854
|
+
maximizable,
|
|
2701
2855
|
responsive?.orientation,
|
|
2702
2856
|
store.model
|
|
2703
2857
|
]);
|
|
2858
|
+
const view = presentation.model;
|
|
2704
2859
|
const maximized = view.maximizedTabsetId === void 0 ? void 0 : findTabset(view, view.maximizedTabsetId);
|
|
2705
2860
|
const tree = /* @__PURE__ */ jsx(Layout.FloatLayer, {
|
|
2706
|
-
floats:
|
|
2861
|
+
floats: view.floats,
|
|
2707
2862
|
children: /* @__PURE__ */ jsx(Layout.DragLayer, { children: maximized ? /* @__PURE__ */ jsx(Layout.Tabset, { node: maximized }) : /* @__PURE__ */ jsx(Layout.Rows, { node: view.layout }) })
|
|
2708
2863
|
});
|
|
2709
2864
|
return /* @__PURE__ */ jsx(Layout.Root, {
|
|
@@ -2714,14 +2869,12 @@ const DashfooLayout = forwardRef((props, ref) => {
|
|
|
2714
2869
|
editable,
|
|
2715
2870
|
floatable,
|
|
2716
2871
|
keepMounted,
|
|
2717
|
-
maximizable,
|
|
2872
|
+
maximizable: presentation.maximizable,
|
|
2718
2873
|
model: view,
|
|
2719
2874
|
renamableTabs,
|
|
2720
|
-
|
|
2721
|
-
renderTabLabel,
|
|
2722
|
-
renderTabsetToolbar,
|
|
2875
|
+
renderers,
|
|
2723
2876
|
resizableSplits,
|
|
2724
|
-
restructurable:
|
|
2877
|
+
restructurable: presentation.restructurable,
|
|
2725
2878
|
rootRef: containerRef,
|
|
2726
2879
|
snap,
|
|
2727
2880
|
children: hasSharedManager ? tree : /* @__PURE__ */ jsx(DashfooDragProvider, { children: tree })
|
|
@@ -2730,31 +2883,62 @@ const DashfooLayout = forwardRef((props, ref) => {
|
|
|
2730
2883
|
DashfooLayout.displayName = "DashfooLayout";
|
|
2731
2884
|
|
|
2732
2885
|
//#endregion
|
|
2733
|
-
//#region src/components/panel.tsx
|
|
2734
|
-
const
|
|
2886
|
+
//#region src/components/panel-badge.tsx
|
|
2887
|
+
const PanelBadge = forwardRef((props, ref) => /* @__PURE__ */ jsx("span", {
|
|
2735
2888
|
...props,
|
|
2736
|
-
"data-dashfoo": "panel"
|
|
2737
|
-
|
|
2738
|
-
|
|
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", {
|
|
2739
2897
|
...props,
|
|
2740
|
-
"data-dashfoo": "panel-
|
|
2741
|
-
|
|
2742
|
-
|
|
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", {
|
|
2743
2907
|
...props,
|
|
2744
|
-
"data-dashfoo": "panel-
|
|
2745
|
-
|
|
2746
|
-
|
|
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", {
|
|
2747
2916
|
...props,
|
|
2748
|
-
"data-dashfoo": "panel-
|
|
2749
|
-
|
|
2750
|
-
|
|
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", {
|
|
2751
2925
|
...props,
|
|
2752
|
-
"data-dashfoo": "panel
|
|
2753
|
-
|
|
2754
|
-
|
|
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", {
|
|
2755
2934
|
...props,
|
|
2756
|
-
"data-dashfoo": "panel-
|
|
2757
|
-
|
|
2935
|
+
"data-dashfoo": "panel-title",
|
|
2936
|
+
ref
|
|
2937
|
+
}));
|
|
2938
|
+
PanelTitle.displayName = "PanelTitle";
|
|
2939
|
+
|
|
2940
|
+
//#endregion
|
|
2941
|
+
//#region src/components/panel.tsx
|
|
2758
2942
|
const Panel = {
|
|
2759
2943
|
Badge: PanelBadge,
|
|
2760
2944
|
Body: PanelBody,
|