@dashfoo/core 0.5.1 → 0.5.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 +20 -20
- package/dist/index.d.ts +93 -94
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -85
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -95,11 +95,11 @@ const globalAttributesSchema = z.object({
|
|
|
95
95
|
});
|
|
96
96
|
const dashfooSchema = z.object({
|
|
97
97
|
activeTabsetId: z.string().optional(),
|
|
98
|
+
floats: z.array(floatNodeSchema).optional(),
|
|
98
99
|
global: globalAttributesSchema,
|
|
99
100
|
layout: rowNodeSchema,
|
|
100
101
|
maximizedTabsetId: z.string().optional(),
|
|
101
|
-
version: z.literal(1)
|
|
102
|
-
floats: z.array(floatNodeSchema).optional()
|
|
102
|
+
version: z.literal(1)
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
//#endregion
|
|
@@ -138,6 +138,7 @@ const dockLocationSchema = z.enum([
|
|
|
138
138
|
"split-right",
|
|
139
139
|
"split-top"
|
|
140
140
|
]);
|
|
141
|
+
const splitWeightsSchema = z.array(z.number());
|
|
141
142
|
const actionSchema = z.discriminatedUnion("type", [
|
|
142
143
|
z.object({
|
|
143
144
|
index: z.number().int().optional(),
|
|
@@ -189,7 +190,7 @@ const actionSchema = z.discriminatedUnion("type", [
|
|
|
189
190
|
z.object({
|
|
190
191
|
rowId: z.string(),
|
|
191
192
|
type: z.literal("adjustSplit"),
|
|
192
|
-
weights:
|
|
193
|
+
weights: splitWeightsSchema
|
|
193
194
|
}),
|
|
194
195
|
z.object({
|
|
195
196
|
attrs: mutableNodeAttrsSchema,
|
|
@@ -244,9 +245,52 @@ const createTabId = () => createNodeId("tab");
|
|
|
244
245
|
//#region src/model/tree.ts
|
|
245
246
|
const collectRoots = (model) => [model.layout, ...(model.floats ?? []).map((float) => float.layout)];
|
|
246
247
|
const findFloat = (model, floatId) => (model.floats ?? []).find((float) => float.id === floatId);
|
|
248
|
+
/**
|
|
249
|
+
* Depth-first pre-order over one root: the row itself, then each child in
|
|
250
|
+
* order, descending into nested rows and into a tabset's tabs. Returning a
|
|
251
|
+
* defined value from `visit` stops the walk and becomes the result, so one
|
|
252
|
+
* traversal serves both find-first and collect-all.
|
|
253
|
+
*
|
|
254
|
+
* Every query below used to carry its own copy of this recursion, and they had
|
|
255
|
+
* drifted in child order: some scanned a row's own tabsets before descending,
|
|
256
|
+
* others interleaved. With unique ids all orders agree, so they now share this
|
|
257
|
+
* one; a model with duplicate ids (which the builder and the parser both warn
|
|
258
|
+
* about) can see a different duplicate win than it did before.
|
|
259
|
+
*/
|
|
260
|
+
const walk = (row, visit, parent, index = -1) => {
|
|
261
|
+
const atRoot = visit({
|
|
262
|
+
index,
|
|
263
|
+
node: row,
|
|
264
|
+
parent
|
|
265
|
+
});
|
|
266
|
+
if (atRoot !== void 0) return atRoot;
|
|
267
|
+
for (const [childIndex, child] of row.children.entries()) {
|
|
268
|
+
if (child.type === "row") {
|
|
269
|
+
const inRow = walk(child, visit, row, childIndex);
|
|
270
|
+
if (inRow !== void 0) return inRow;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const atTabset = visit({
|
|
274
|
+
index: childIndex,
|
|
275
|
+
node: child,
|
|
276
|
+
parent: row
|
|
277
|
+
});
|
|
278
|
+
if (atTabset !== void 0) return atTabset;
|
|
279
|
+
for (const [tabIndex, tab] of child.children.entries()) {
|
|
280
|
+
const atTab = visit({
|
|
281
|
+
index: tabIndex,
|
|
282
|
+
node: tab,
|
|
283
|
+
parent: child
|
|
284
|
+
});
|
|
285
|
+
if (atTab !== void 0) return atTab;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const locate = (row, id) => walk(row, (found) => found.node.id === id ? found : void 0);
|
|
247
290
|
const collectTabsetsInRow = (row, acc) => {
|
|
248
|
-
|
|
249
|
-
|
|
291
|
+
walk(row, ({ node }) => {
|
|
292
|
+
if (node.type === "tabset") acc.push(node);
|
|
293
|
+
});
|
|
250
294
|
};
|
|
251
295
|
const collectTabsets = (model) => {
|
|
252
296
|
const acc = [];
|
|
@@ -258,7 +302,7 @@ const findTabset = (model, tabsetId) => collectTabsets(model).find((tabset) => t
|
|
|
258
302
|
const findTabInContainer = (container, tabId) => {
|
|
259
303
|
const index = container.children.findIndex((tab) => tab.id === tabId);
|
|
260
304
|
if (index === -1) return;
|
|
261
|
-
const tab = container.children
|
|
305
|
+
const tab = container.children.at(index);
|
|
262
306
|
if (!tab) return;
|
|
263
307
|
return {
|
|
264
308
|
container,
|
|
@@ -273,25 +317,10 @@ const findTab = (model, tabId) => {
|
|
|
273
317
|
}
|
|
274
318
|
};
|
|
275
319
|
const findRow = (row, id) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const found = findRow(child, id);
|
|
279
|
-
if (found) return found;
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
const findAttributedNodeInRow = (row, id) => {
|
|
283
|
-
if (row.id === id) return row;
|
|
284
|
-
for (const child of row.children) {
|
|
285
|
-
if (child.type === "row") {
|
|
286
|
-
const found = findAttributedNodeInRow(child, id);
|
|
287
|
-
if (found) return found;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (child.id === id) return child;
|
|
291
|
-
const tab = child.children.find((candidate) => candidate.id === id);
|
|
292
|
-
if (tab) return tab;
|
|
293
|
-
}
|
|
320
|
+
const found = locate(row, id)?.node;
|
|
321
|
+
return found?.type === "row" ? found : void 0;
|
|
294
322
|
};
|
|
323
|
+
const findAttributedNodeInRow = (row, id) => locate(row, id)?.node;
|
|
295
324
|
const findAttributedNode = (model, id) => {
|
|
296
325
|
for (const root of collectRoots(model)) {
|
|
297
326
|
const found = findAttributedNodeInRow(root, id);
|
|
@@ -302,23 +331,16 @@ const findRootContaining = (model, nodeId) => {
|
|
|
302
331
|
for (const root of collectRoots(model)) if (findAttributedNodeInRow(root, nodeId)) return root;
|
|
303
332
|
};
|
|
304
333
|
const findTabsetParent = (row, tabsetId) => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
index,
|
|
308
|
-
parent:
|
|
309
|
-
};
|
|
310
|
-
for (const child of row.children) if (child.type === "row") {
|
|
311
|
-
const found = findTabsetParent(child, tabsetId);
|
|
312
|
-
if (found) return found;
|
|
313
|
-
}
|
|
334
|
+
const found = locate(row, tabsetId);
|
|
335
|
+
return found?.node.type === "tabset" && found.parent?.type === "row" ? {
|
|
336
|
+
index: found.index,
|
|
337
|
+
parent: found.parent
|
|
338
|
+
} : void 0;
|
|
314
339
|
};
|
|
315
340
|
const collectIdsInRow = (row, acc) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
acc.push(child.id);
|
|
320
|
-
for (const tab of child.children) acc.push(tab.id);
|
|
321
|
-
}
|
|
341
|
+
walk(row, ({ node }) => {
|
|
342
|
+
acc.push(node.id);
|
|
343
|
+
});
|
|
322
344
|
};
|
|
323
345
|
const findDuplicateIds = (model) => {
|
|
324
346
|
const ids = [];
|
|
@@ -385,7 +407,7 @@ const model = (layout, options = {}) => {
|
|
|
385
407
|
global: options.global ?? {}
|
|
386
408
|
};
|
|
387
409
|
const duplicates = findDuplicateIds(built);
|
|
388
|
-
if (duplicates.length > 0) console.warn(`[dashfoo] builder produced duplicate node ids: ${duplicates.join(", ")}
|
|
410
|
+
if (duplicates.length > 0) console.warn(`[dashfoo] builder produced duplicate node ids: ${duplicates.join(", ")}. Pass explicit ids when reusing a component`);
|
|
389
411
|
return built;
|
|
390
412
|
};
|
|
391
413
|
|
|
@@ -541,6 +563,7 @@ const zoneRect = (rect, location) => {
|
|
|
541
563
|
x: rect.x,
|
|
542
564
|
y: rect.y
|
|
543
565
|
};
|
|
566
|
+
case "center": return rect;
|
|
544
567
|
default: return rect;
|
|
545
568
|
}
|
|
546
569
|
};
|
|
@@ -617,6 +640,27 @@ const normalizeTabset = (tabset) => ({
|
|
|
617
640
|
...tabset,
|
|
618
641
|
selected: clampSelected(tabset.children.length, tabset.selected)
|
|
619
642
|
});
|
|
643
|
+
/**
|
|
644
|
+
* A row that collapses to a single child hands its slot in the parent to
|
|
645
|
+
* that child, so the slot's sizing constraints have to travel with it.
|
|
646
|
+
* Dropping them silently widens a pinned sidebar, and `normalize` runs on
|
|
647
|
+
* load and on every persist, so the loss is permanent.
|
|
648
|
+
*
|
|
649
|
+
* `snap` only exists on rows, so it is carried only to a row survivor.
|
|
650
|
+
*/
|
|
651
|
+
const inheritSlot = (row, survivor) => {
|
|
652
|
+
const sized = {
|
|
653
|
+
...survivor,
|
|
654
|
+
...row.weight === void 0 ? {} : { weight: row.weight },
|
|
655
|
+
...row.min === void 0 ? {} : { min: row.min },
|
|
656
|
+
...row.max === void 0 ? {} : { max: row.max }
|
|
657
|
+
};
|
|
658
|
+
if (sized.type === "row" && row.snap !== void 0) return {
|
|
659
|
+
...sized,
|
|
660
|
+
snap: row.snap
|
|
661
|
+
};
|
|
662
|
+
return sized;
|
|
663
|
+
};
|
|
620
664
|
const normalizeRowChildren = (children) => {
|
|
621
665
|
const out = [];
|
|
622
666
|
for (const child of children) {
|
|
@@ -628,10 +672,7 @@ const normalizeRowChildren = (children) => {
|
|
|
628
672
|
if (grandchildren.length === 0) continue;
|
|
629
673
|
if (grandchildren.length === 1) {
|
|
630
674
|
const only = grandchildren[0];
|
|
631
|
-
if (only !== void 0) out.push(child
|
|
632
|
-
...only,
|
|
633
|
-
weight: child.weight
|
|
634
|
-
});
|
|
675
|
+
if (only !== void 0) out.push(inheritSlot(child, only));
|
|
635
676
|
continue;
|
|
636
677
|
}
|
|
637
678
|
out.push({
|
|
@@ -686,26 +727,13 @@ const normalize = (model) => {
|
|
|
686
727
|
|
|
687
728
|
//#endregion
|
|
688
729
|
//#region src/state/surgery.ts
|
|
689
|
-
const removeTabset = (row, tabsetId) => {
|
|
690
|
-
const index = row.children.findIndex((child) => child.type === "tabset" && child.id === tabsetId);
|
|
691
|
-
if (index !== -1) {
|
|
692
|
-
row.children.splice(index, 1);
|
|
693
|
-
return true;
|
|
694
|
-
}
|
|
695
|
-
for (const child of row.children) if (child.type === "row" && removeTabset(child, tabsetId)) return true;
|
|
696
|
-
return false;
|
|
697
|
-
};
|
|
698
730
|
const removeTabsetReturning = (row, tabsetId) => {
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
for (const child of row.children) if (child.type === "row") {
|
|
705
|
-
const found = removeTabsetReturning(child, tabsetId);
|
|
706
|
-
if (found) return found;
|
|
707
|
-
}
|
|
731
|
+
const found = findTabsetParent(row, tabsetId);
|
|
732
|
+
if (!found) return;
|
|
733
|
+
const [removed] = found.parent.children.splice(found.index, 1);
|
|
734
|
+
return removed?.type === "tabset" ? removed : void 0;
|
|
708
735
|
};
|
|
736
|
+
const removeTabset = (row, tabsetId) => removeTabsetReturning(row, tabsetId) !== void 0;
|
|
709
737
|
const splitOrientation = (location) => location === "split-left" || location === "split-right" ? "row" : "column";
|
|
710
738
|
const splitsBefore = (location) => location === "split-left" || location === "split-top";
|
|
711
739
|
const placeBesideTarget = (draft, placed, targetId, location) => {
|
|
@@ -744,17 +772,19 @@ const insertTab = (draft, tab, target) => {
|
|
|
744
772
|
targetTabset.selected = at;
|
|
745
773
|
return;
|
|
746
774
|
}
|
|
747
|
-
|
|
775
|
+
const newTabset = {
|
|
748
776
|
children: [tab],
|
|
749
777
|
id: createNodeId("tabset"),
|
|
750
778
|
selected: 0,
|
|
751
779
|
type: "tabset",
|
|
752
780
|
weight: 50
|
|
753
|
-
}
|
|
781
|
+
};
|
|
782
|
+
placeBesideTarget(draft, newTabset, targetId, location);
|
|
754
783
|
};
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
784
|
+
const mergeTabsInto = (target, tabs, selectedOffset) => {
|
|
785
|
+
const mergeStart = target.children.length;
|
|
786
|
+
target.children.push(...tabs);
|
|
787
|
+
target.selected = mergeStart + selectedOffset;
|
|
758
788
|
};
|
|
759
789
|
const wrapTabInLayout = (tab) => ({
|
|
760
790
|
children: [{
|
|
@@ -799,8 +829,8 @@ const pushFloat = (draft, layout, geometry, floatId) => {
|
|
|
799
829
|
};
|
|
800
830
|
draft.floats = [...draft.floats ?? [], float];
|
|
801
831
|
const tabsets = [];
|
|
802
|
-
|
|
803
|
-
const first = tabsets
|
|
832
|
+
collectTabsetsInRow(layout, tabsets);
|
|
833
|
+
const first = tabsets.at(0);
|
|
804
834
|
if (first) draft.activeTabsetId = first.id;
|
|
805
835
|
};
|
|
806
836
|
const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
|
|
@@ -811,8 +841,8 @@ const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
|
|
|
811
841
|
};
|
|
812
842
|
const resolveMainTarget = (draft, targetId) => {
|
|
813
843
|
const mainTabsets = [];
|
|
814
|
-
|
|
815
|
-
if (targetId) {
|
|
844
|
+
collectTabsetsInRow(draft.layout, mainTabsets);
|
|
845
|
+
if (targetId !== void 0) {
|
|
816
846
|
const explicit = mainTabsets.find((tabset) => tabset.id === targetId);
|
|
817
847
|
if (explicit) return explicit;
|
|
818
848
|
}
|
|
@@ -826,15 +856,15 @@ const dockFloat = (draft, floatId, targetId, location) => {
|
|
|
826
856
|
const floats = draft.floats ?? [];
|
|
827
857
|
const index = floats.findIndex((float) => float.id === floatId);
|
|
828
858
|
if (index === -1) return;
|
|
829
|
-
const
|
|
859
|
+
const float = floats.splice(index, 1).at(0);
|
|
830
860
|
draft.floats = floats;
|
|
831
861
|
if (!float) return;
|
|
832
862
|
const tabsets = [];
|
|
833
|
-
|
|
863
|
+
collectTabsetsInRow(float.layout, tabsets);
|
|
834
864
|
const tabs = tabsets.flatMap((tabset) => tabset.children);
|
|
835
865
|
if (tabs.length === 0) return;
|
|
836
|
-
const leadTabset = tabsets
|
|
837
|
-
const selectedOffset = leadTabset ?
|
|
866
|
+
const leadTabset = tabsets.at(0);
|
|
867
|
+
const selectedOffset = leadTabset ? clampSelected(leadTabset.children.length, leadTabset.selected) : 0;
|
|
838
868
|
const target = resolveMainTarget(draft, targetId);
|
|
839
869
|
if (!target) {
|
|
840
870
|
draft.layout = float.layout;
|
|
@@ -842,9 +872,7 @@ const dockFloat = (draft, floatId, targetId, location) => {
|
|
|
842
872
|
return;
|
|
843
873
|
}
|
|
844
874
|
if (location === "center") {
|
|
845
|
-
|
|
846
|
-
target.children.push(...tabs);
|
|
847
|
-
target.selected = mergeStart + selectedOffset;
|
|
875
|
+
mergeTabsInto(target, tabs, selectedOffset);
|
|
848
876
|
draft.activeTabsetId = target.id;
|
|
849
877
|
return;
|
|
850
878
|
}
|
|
@@ -877,7 +905,7 @@ const applyAction = (draft, action) => {
|
|
|
877
905
|
const row = findRow(findRootContaining(draft, action.rowId) ?? draft.layout, action.rowId);
|
|
878
906
|
if (!row) return;
|
|
879
907
|
for (let index = 0; index < action.weights.length; index++) {
|
|
880
|
-
const child = row.children
|
|
908
|
+
const child = row.children.at(index);
|
|
881
909
|
const weight = action.weights[index];
|
|
882
910
|
if (!child || weight === void 0) continue;
|
|
883
911
|
child.weight = weight;
|
|
@@ -900,7 +928,7 @@ const applyAction = (draft, action) => {
|
|
|
900
928
|
case "floatTab": {
|
|
901
929
|
const location = findTab(draft, action.tabId);
|
|
902
930
|
if (!location) return;
|
|
903
|
-
const
|
|
931
|
+
const removed = location.container.children.splice(location.index, 1).at(0);
|
|
904
932
|
if (removed) pushFloat(draft, wrapTabInLayout(removed), action.geometry, action.floatId);
|
|
905
933
|
return;
|
|
906
934
|
}
|
|
@@ -915,7 +943,7 @@ const applyAction = (draft, action) => {
|
|
|
915
943
|
case "moveNode": {
|
|
916
944
|
const source = findTab(draft, action.sourceId);
|
|
917
945
|
if (!source) return;
|
|
918
|
-
const
|
|
946
|
+
const removed = source.container.children.splice(source.index, 1).at(0);
|
|
919
947
|
if (!removed) return;
|
|
920
948
|
insertTab(draft, removed, {
|
|
921
949
|
id: action.targetId,
|
|
@@ -931,10 +959,7 @@ const applyAction = (draft, action) => {
|
|
|
931
959
|
if (action.location === "center") {
|
|
932
960
|
const target = findTabset(draft, action.targetId);
|
|
933
961
|
if (target) {
|
|
934
|
-
|
|
935
|
-
const sourceSelected = Math.min(Math.max(source.selected, 0), source.children.length - 1);
|
|
936
|
-
target.children.push(...source.children);
|
|
937
|
-
target.selected = mergeStart + sourceSelected;
|
|
962
|
+
mergeTabsInto(target, source.children, clampSelected(source.children.length, source.selected));
|
|
938
963
|
const sourceRoot = findRootContaining(draft, action.sourceId);
|
|
939
964
|
if (sourceRoot) removeTabset(sourceRoot, action.sourceId);
|
|
940
965
|
}
|