@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/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: z.array(z.number())
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
- for (const child of row.children) if (child.type === "tabset") acc.push(child);
249
- else collectTabsetsInRow(child, acc);
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[index];
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
- if (row.id === id) return row;
277
- for (const child of row.children) if (child.type === "row") {
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 index = row.children.findIndex((child) => child.type === "tabset" && child.id === tabsetId);
306
- if (index !== -1) return {
307
- index,
308
- parent: row
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
- acc.push(row.id);
317
- for (const child of row.children) if (child.type === "row") collectIdsInRow(child, acc);
318
- else {
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(", ")} — pass explicit ids when reusing a component`);
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.weight === void 0 ? only : {
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 index = row.children.findIndex((child) => child.type === "tabset" && child.id === tabsetId);
700
- if (index !== -1) {
701
- const [removed] = row.children.splice(index, 1);
702
- return removed?.type === "tabset" ? removed : void 0;
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
- placeBesideTarget(draft, {
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
- }, targetId, location);
781
+ };
782
+ placeBesideTarget(draft, newTabset, targetId, location);
754
783
  };
755
- const tabsetsInRow = (row, acc) => {
756
- for (const child of row.children) if (child.type === "tabset") acc.push(child);
757
- else tabsetsInRow(child, acc);
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
- tabsetsInRow(layout, tabsets);
803
- const first = tabsets[0];
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
- tabsetsInRow(draft.layout, mainTabsets);
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 [float] = floats.splice(index, 1);
859
+ const float = floats.splice(index, 1).at(0);
830
860
  draft.floats = floats;
831
861
  if (!float) return;
832
862
  const tabsets = [];
833
- tabsetsInRow(float.layout, tabsets);
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[0];
837
- const selectedOffset = leadTabset ? Math.min(Math.max(leadTabset.selected, 0), leadTabset.children.length - 1) : 0;
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
- const mergeStart = target.children.length;
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[index];
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 [removed] = location.container.children.splice(location.index, 1);
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 [removed] = source.container.children.splice(source.index, 1);
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
- const mergeStart = target.children.length;
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
  }