@dashfoo/core 0.5.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,31 @@
1
1
  import { z } from "zod";
2
2
  import { assign, emit, setup } from "xstate";
3
3
 
4
+ //#region src/lib/clamp-selected.ts
5
+ /**
6
+ * Lives in `lib/` rather than beside `normalize`, because both the parse
7
+ * boundary (`tabsetNodeSchema`) and the tree walker that `normalize` uses need
8
+ * it, and importing it from either one would close a module cycle.
9
+ */
10
+ const clampSelected = (length, selected) => {
11
+ if (length === 0) return 0;
12
+ const base = Number.isFinite(selected) ? Math.trunc(selected) : 0;
13
+ if (base < 0) return 0;
14
+ if (base > length - 1) return length - 1;
15
+ return base;
16
+ };
17
+
18
+ //#endregion
4
19
  //#region src/model/schema.ts
20
+ /**
21
+ * Weights are relative shares, and every reader that rendered one already
22
+ * treated a missing weight as a single share (`row-view` summed `weight ?? 1`),
23
+ * so filling it in at the boundary changes nothing on screen. The one place that
24
+ * read a missing weight as a percentage was `placeBesideTarget`, which is why
25
+ * docking beside an unweighted target halved it to `50` and left its unweighted
26
+ * siblings rendering as a sliver of the row.
27
+ */
28
+ const DEFAULT_WEIGHT = 1;
5
29
  const jsonValueSchema = z.lazy(() => z.union([
6
30
  z.string(),
7
31
  z.number(),
@@ -44,7 +68,14 @@ const tabNodeSchema = z.object({
44
68
  name: z.string(),
45
69
  type: z.literal("tab")
46
70
  });
47
- const tabsetNodeSchema = z.object({
71
+ /**
72
+ * The pre-transform object, kept separate so `node-attrs` can `.pick()` mutable
73
+ * keys off it. Picking from the transformed schema is impossible (a pipe has no
74
+ * `.pick`), and picking `weight` with its default attached would inject that
75
+ * default: `.partial()` over a defaulted field still fills it in, which would
76
+ * rewrite a tabset's weight on every unrelated attribute update.
77
+ */
78
+ const tabsetNodeObjectSchema = z.object({
48
79
  children: z.array(tabNodeSchema),
49
80
  enableClose: z.boolean().optional(),
50
81
  enableMaximize: z.boolean().optional(),
@@ -54,8 +85,12 @@ const tabsetNodeSchema = z.object({
54
85
  name: z.string().optional(),
55
86
  selected: z.number().int(),
56
87
  type: z.literal("tabset"),
57
- weight: z.number().optional()
88
+ weight: z.number().default(1)
58
89
  });
90
+ const tabsetNodeSchema = tabsetNodeObjectSchema.transform((tabset) => ({
91
+ ...tabset,
92
+ selected: clampSelected(tabset.children.length, tabset.selected)
93
+ }));
59
94
  const rowNodeSchema = z.lazy(() => z.object({
60
95
  children: z.array(z.union([rowNodeSchema, tabsetNodeSchema])),
61
96
  id: z.string(),
@@ -64,7 +99,7 @@ const rowNodeSchema = z.lazy(() => z.object({
64
99
  orientation: orientationSchema,
65
100
  snap: snapSchema.optional(),
66
101
  type: z.literal("row"),
67
- weight: z.number().optional()
102
+ weight: z.number().default(1)
68
103
  }));
69
104
  const geometrySchema = z.object({
70
105
  height: z.number(),
@@ -95,7 +130,7 @@ const globalAttributesSchema = z.object({
95
130
  });
96
131
  const dashfooSchema = z.object({
97
132
  activeTabsetId: z.string().optional(),
98
- floats: z.array(floatNodeSchema).optional(),
133
+ floats: z.array(floatNodeSchema).default(() => []),
99
134
  global: globalAttributesSchema,
100
135
  layout: rowNodeSchema,
101
136
  maximizedTabsetId: z.string().optional(),
@@ -103,7 +138,7 @@ const dashfooSchema = z.object({
103
138
  });
104
139
 
105
140
  //#endregion
106
- //#region src/state/actions.ts
141
+ //#region src/state/node-attrs.ts
107
142
  const mutableTabAttrsSchema = tabNodeSchema.pick({
108
143
  config: true,
109
144
  enableClose: true,
@@ -111,14 +146,18 @@ const mutableTabAttrsSchema = tabNodeSchema.pick({
111
146
  enableRename: true,
112
147
  name: true
113
148
  }).partial();
114
- const mutableTabsetAttrsSchema = tabsetNodeSchema.pick({
149
+ /**
150
+ * `weight` is re-declared instead of picked: `.partial()` over the schema's
151
+ * defaulted `weight` still injects the default, so `{ name: "x" }` would arrive
152
+ * at the reducer carrying a weight and silently resize the tabset.
153
+ */
154
+ const mutableTabsetAttrsSchema = tabsetNodeObjectSchema.pick({
115
155
  enableClose: true,
116
156
  enableMaximize: true,
117
157
  max: true,
118
158
  min: true,
119
- selected: true,
120
- weight: true
121
- }).partial();
159
+ selected: true
160
+ }).extend({ weight: z.number() }).partial();
122
161
  const mutableRowAttrsSchema = z.object({
123
162
  max: dimensionSchema.optional(),
124
163
  min: dimensionSchema.optional(),
@@ -126,11 +165,19 @@ const mutableRowAttrsSchema = z.object({
126
165
  snap: snapSchema.optional(),
127
166
  weight: z.number()
128
167
  }).partial();
129
- const mutableNodeAttrsSchema = z.union([
130
- mutableTabAttrsSchema,
131
- mutableTabsetAttrsSchema,
132
- mutableRowAttrsSchema
133
- ]);
168
+ /**
169
+ * One all-optional object carrying every mutable key of every node kind, not a
170
+ * union of the three: a union whose members are all fully optional matches on
171
+ * its first member and strips the keys the others own, which erased every
172
+ * tabset and row attribute at the parse boundary. The four keys that two kinds
173
+ * share (`enableClose`, `max`, `min`, `weight`) declare the same type on both
174
+ * sides, so merging widens nothing. Which keys are legal for a given node is
175
+ * enforced by the reducer against that node's own schema.
176
+ */
177
+ const mutableNodeAttrsSchema = mutableTabAttrsSchema.extend(mutableTabsetAttrsSchema.shape).extend(mutableRowAttrsSchema.shape);
178
+
179
+ //#endregion
180
+ //#region src/state/actions.ts
134
181
  const dockLocationSchema = z.enum([
135
182
  "center",
136
183
  "split-bottom",
@@ -243,8 +290,8 @@ const createTabId = () => createNodeId("tab");
243
290
 
244
291
  //#endregion
245
292
  //#region src/model/tree.ts
246
- const collectRoots = (model) => [model.layout, ...(model.floats ?? []).map((float) => float.layout)];
247
- const findFloat = (model, floatId) => (model.floats ?? []).find((float) => float.id === floatId);
293
+ const collectRoots = (model) => [model.layout, ...model.floats.map((float) => float.layout)];
294
+ const findFloat = (model, floatId) => model.floats.find((float) => float.id === floatId);
248
295
  /**
249
296
  * Depth-first pre-order over one root: the row itself, then each child in
250
297
  * order, descending into nested rows and into a tabset's tabs. Returning a
@@ -344,7 +391,7 @@ const collectIdsInRow = (row, acc) => {
344
391
  };
345
392
  const findDuplicateIds = (model) => {
346
393
  const ids = [];
347
- for (const float of model.floats ?? []) ids.push(float.id);
394
+ for (const float of model.floats) ids.push(float.id);
348
395
  for (const root of collectRoots(model)) collectIdsInRow(root, ids);
349
396
  const seen = /* @__PURE__ */ new Set();
350
397
  const duplicates = /* @__PURE__ */ new Set();
@@ -377,7 +424,8 @@ const tabset = (children, options = {}) => {
377
424
  ...max === void 0 ? {} : { max: toDimension(max) },
378
425
  ...min === void 0 ? {} : { min: toDimension(min) },
379
426
  id: options.id ?? createNodeId("tabset"),
380
- selected: options.selected ?? 0
427
+ selected: clampSelected(children.length, options.selected ?? 0),
428
+ weight: options.weight ?? 1
381
429
  };
382
430
  };
383
431
  const row = (children, options = {}) => {
@@ -389,7 +437,8 @@ const row = (children, options = {}) => {
389
437
  ...max === void 0 ? {} : { max: toDimension(max) },
390
438
  ...min === void 0 ? {} : { min: toDimension(min) },
391
439
  id: options.id ?? createNodeId("row"),
392
- orientation: options.orientation ?? "row"
440
+ orientation: options.orientation ?? "row",
441
+ weight: options.weight ?? 1
393
442
  };
394
443
  };
395
444
  const floatNode = (layout, geometry, options = {}) => ({
@@ -404,6 +453,7 @@ const model = (layout, options = {}) => {
404
453
  layout,
405
454
  version: 1,
406
455
  ...options,
456
+ floats: options.floats ?? [],
407
457
  global: options.global ?? {}
408
458
  };
409
459
  const duplicates = findDuplicateIds(built);
@@ -411,6 +461,12 @@ const model = (layout, options = {}) => {
411
461
  return built;
412
462
  };
413
463
 
464
+ //#endregion
465
+ //#region src/lib/assert-never.ts
466
+ const assertNever = (value) => {
467
+ throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
468
+ };
469
+
414
470
  //#endregion
415
471
  //#region src/geometry/geometry.ts
416
472
  const DEFAULT_BAND_FRACTION = .22;
@@ -431,6 +487,23 @@ const closestEdge = (d) => {
431
487
  if (min === d.top) return "top";
432
488
  return "bottom";
433
489
  };
490
+ const dockLocationFor = (target) => {
491
+ switch (target.kind) {
492
+ case "split": return `split-${target.edge}`;
493
+ case "tab": return "center";
494
+ default: return assertNever(target);
495
+ }
496
+ };
497
+ const splitEdge = (location) => {
498
+ switch (location) {
499
+ case "center": return;
500
+ case "split-bottom": return "bottom";
501
+ case "split-left": return "left";
502
+ case "split-right": return "right";
503
+ case "split-top": return "top";
504
+ default: return assertNever(location);
505
+ }
506
+ };
434
507
  const resolveDockTarget = (pointer, rect, opts) => {
435
508
  const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
436
509
  if (rect.width <= 0 || rect.height <= 0) return { kind: "tab" };
@@ -564,7 +637,7 @@ const zoneRect = (rect, location) => {
564
637
  y: rect.y
565
638
  };
566
639
  case "center": return rect;
567
- default: return rect;
640
+ default: return assertNever(location);
568
641
  }
569
642
  };
570
643
 
@@ -626,20 +699,21 @@ const snapSizes = (sizes, boundaryIndex, config) => {
626
699
  snapped: true
627
700
  };
628
701
  };
702
+ const decideSnap = (sizes, boundaryIndex, config) => {
703
+ if (boundaryIndex === null || config === null || !snapEnabled(config)) return { kind: "inactive" };
704
+ const { sizes: snappedSizes, snapped } = snapSizes(sizes, boundaryIndex, config);
705
+ return snapped ? {
706
+ kind: "engage",
707
+ sizes: snappedSizes
708
+ } : { kind: "clear" };
709
+ };
710
+ const settleSnap = (sizes, boundaryIndex, config) => {
711
+ const decision = decideSnap(sizes, boundaryIndex, config);
712
+ return decision.kind === "engage" ? decision.sizes : sizes;
713
+ };
629
714
 
630
715
  //#endregion
631
716
  //#region src/model/invariants.ts
632
- const clampSelected = (length, selected) => {
633
- if (length === 0) return 0;
634
- const base = Number.isFinite(selected) ? Math.trunc(selected) : 0;
635
- if (base < 0) return 0;
636
- if (base > length - 1) return length - 1;
637
- return base;
638
- };
639
- const normalizeTabset = (tabset) => ({
640
- ...tabset,
641
- selected: clampSelected(tabset.children.length, tabset.selected)
642
- });
643
717
  /**
644
718
  * A row that collapses to a single child hands its slot in the parent to
645
719
  * that child, so the slot's sizing constraints have to travel with it.
@@ -651,7 +725,7 @@ const normalizeTabset = (tabset) => ({
651
725
  const inheritSlot = (row, survivor) => {
652
726
  const sized = {
653
727
  ...survivor,
654
- ...row.weight === void 0 ? {} : { weight: row.weight },
728
+ weight: row.weight,
655
729
  ...row.min === void 0 ? {} : { min: row.min },
656
730
  ...row.max === void 0 ? {} : { max: row.max }
657
731
  };
@@ -665,7 +739,7 @@ const normalizeRowChildren = (children) => {
665
739
  const out = [];
666
740
  for (const child of children) {
667
741
  if (child.type === "tabset") {
668
- if (child.children.length > 0) out.push(normalizeTabset(child));
742
+ if (child.children.length > 0) out.push(child);
669
743
  continue;
670
744
  }
671
745
  const grandchildren = normalizeRowChildren(child.children);
@@ -700,22 +774,22 @@ const normalizeLayout = (root) => {
700
774
  };
701
775
  const normalize = (model) => {
702
776
  const layout = normalizeLayout(model.layout);
703
- const healedFloats = [];
704
- for (const float of model.floats ?? []) {
777
+ const floats = [];
778
+ for (const float of model.floats) {
705
779
  const floatLayout = normalizeLayout(float.layout);
706
- if (rowContainsTabset(floatLayout)) healedFloats.push({
780
+ if (rowContainsTabset(floatLayout)) floats.push({
707
781
  ...float,
708
782
  layout: floatLayout
709
783
  });
710
784
  }
711
- const floats = healedFloats.length > 0 ? healedFloats : void 0;
712
785
  const withRoots = {
713
786
  ...model,
714
787
  floats,
715
788
  layout
716
789
  };
717
- const tabsetIds = new Set(collectTabsets(withRoots).map((tabset) => tabset.id));
718
- const firstTabsetId = collectTabsets(withRoots)[0]?.id;
790
+ const tabsets = collectTabsets(withRoots);
791
+ const tabsetIds = new Set(tabsets.map((tabset) => tabset.id));
792
+ const firstTabsetId = tabsets[0]?.id;
719
793
  const activeTabsetId = model.activeTabsetId !== void 0 && tabsetIds.has(model.activeTabsetId) ? model.activeTabsetId : firstTabsetId;
720
794
  const maximizedTabsetId = model.maximizedTabsetId !== void 0 && tabsetIds.has(model.maximizedTabsetId) ? model.maximizedTabsetId : void 0;
721
795
  return {
@@ -734,30 +808,33 @@ const removeTabsetReturning = (row, tabsetId) => {
734
808
  return removed?.type === "tabset" ? removed : void 0;
735
809
  };
736
810
  const removeTabset = (row, tabsetId) => removeTabsetReturning(row, tabsetId) !== void 0;
737
- const splitOrientation = (location) => location === "split-left" || location === "split-right" ? "row" : "column";
738
- const splitsBefore = (location) => location === "split-left" || location === "split-top";
811
+ const splitPlacement = (location) => {
812
+ const edge = splitEdge(location);
813
+ return {
814
+ before: edge === "left" || edge === "top",
815
+ orientation: edge === "left" || edge === "right" ? "row" : "column"
816
+ };
817
+ };
739
818
  const placeBesideTarget = (draft, placed, targetId, location) => {
740
819
  const targetTabset = findTabset(draft, targetId);
741
820
  const found = findTabsetParent(findRootContaining(draft, targetId) ?? draft.layout, targetId);
742
821
  if (!targetTabset || !found) return;
743
- const orientation = splitOrientation(location);
744
- const before = splitsBefore(location);
822
+ const { before, orientation } = splitPlacement(location);
745
823
  if (found.parent.orientation === orientation) {
746
- const targetWeight = targetTabset.weight ?? 100;
747
- targetTabset.weight = targetWeight / 2;
748
- placed.weight = targetWeight / 2;
824
+ const half = targetTabset.weight / 2;
825
+ targetTabset.weight = half;
826
+ placed.weight = half;
749
827
  found.parent.children.splice(before ? found.index : found.index + 1, 0, placed);
750
828
  } else {
751
- const targetWeight = targetTabset.weight;
752
- targetTabset.weight = 50;
753
- placed.weight = 50;
754
829
  const newRow = {
755
830
  children: before ? [placed, targetTabset] : [targetTabset, placed],
756
831
  id: createNodeId("row"),
757
832
  orientation,
758
- type: "row"
833
+ type: "row",
834
+ weight: targetTabset.weight
759
835
  };
760
- if (targetWeight !== void 0) newRow.weight = targetWeight;
836
+ targetTabset.weight = 50;
837
+ placed.weight = 50;
761
838
  found.parent.children.splice(found.index, 1, newRow);
762
839
  }
763
840
  draft.activeTabsetId = placed.id;
@@ -767,7 +844,7 @@ const insertTab = (draft, tab, target) => {
767
844
  const targetTabset = findTabset(draft, targetId);
768
845
  if (!targetTabset) return;
769
846
  if (location === "center") {
770
- const at = index ?? targetTabset.children.length;
847
+ const at = Math.min(Math.max(index ?? targetTabset.children.length, 0), targetTabset.children.length);
771
848
  targetTabset.children.splice(at, 0, tab);
772
849
  targetTabset.selected = at;
773
850
  return;
@@ -777,10 +854,20 @@ const insertTab = (draft, tab, target) => {
777
854
  id: createNodeId("tabset"),
778
855
  selected: 0,
779
856
  type: "tabset",
780
- weight: 50
857
+ weight: 1
781
858
  };
782
859
  placeBesideTarget(draft, newTabset, targetId, location);
783
860
  };
861
+ /**
862
+ * Removing a tab can leave `selected` past the end of the strip, so every
863
+ * detach re-clamps its container rather than leaving the whole tree to be
864
+ * re-walked afterwards.
865
+ */
866
+ const detachTab = (container, index) => {
867
+ const removed = container.children.splice(index, 1).at(0);
868
+ container.selected = clampSelected(container.children.length, container.selected);
869
+ return removed;
870
+ };
784
871
  const mergeTabsInto = (target, tabs, selectedOffset) => {
785
872
  const mergeStart = target.children.length;
786
873
  target.children.push(...tabs);
@@ -791,17 +878,20 @@ const wrapTabInLayout = (tab) => ({
791
878
  children: [tab],
792
879
  id: createNodeId("tabset"),
793
880
  selected: 0,
794
- type: "tabset"
881
+ type: "tabset",
882
+ weight: 1
795
883
  }],
796
884
  id: createNodeId("row"),
797
885
  orientation: "row",
798
- type: "row"
886
+ type: "row",
887
+ weight: 1
799
888
  });
800
889
  const wrapTabsetInLayout = (tabset) => ({
801
890
  children: [tabset],
802
891
  id: createNodeId("row"),
803
892
  orientation: "row",
804
- type: "row"
893
+ type: "row",
894
+ weight: 1
805
895
  });
806
896
 
807
897
  //#endregion
@@ -824,10 +914,10 @@ const pushFloat = (draft, layout, geometry, floatId) => {
824
914
  geometry: geometry ?? DEFAULT_FLOAT_GEOMETRY,
825
915
  id: floatId ?? createNodeId("float"),
826
916
  layout,
827
- name: uniqueFloatName("Panel", draft.floats ?? []),
917
+ name: uniqueFloatName("Panel", draft.floats),
828
918
  type: "float"
829
919
  };
830
- draft.floats = [...draft.floats ?? [], float];
920
+ draft.floats = [...draft.floats, float];
831
921
  const tabsets = [];
832
922
  collectTabsetsInRow(layout, tabsets);
833
923
  const first = tabsets.at(0);
@@ -853,11 +943,10 @@ const resolveMainTarget = (draft, targetId) => {
853
943
  return mainTabsets[0];
854
944
  };
855
945
  const dockFloat = (draft, floatId, targetId, location) => {
856
- const floats = draft.floats ?? [];
946
+ const floats = draft.floats;
857
947
  const index = floats.findIndex((float) => float.id === floatId);
858
948
  if (index === -1) return;
859
949
  const float = floats.splice(index, 1).at(0);
860
- draft.floats = floats;
861
950
  if (!float) return;
862
951
  const tabsets = [];
863
952
  collectTabsetsInRow(float.layout, tabsets);
@@ -877,20 +966,30 @@ const dockFloat = (draft, floatId, targetId, location) => {
877
966
  return;
878
967
  }
879
968
  const where = location ?? "split-right";
880
- const placed = tabsets.length === 1 && leadTabset ? leadTabset : {
969
+ placeBesideTarget(draft, tabsets.length === 1 && leadTabset ? leadTabset : {
881
970
  children: tabs,
882
971
  id: createNodeId("tabset"),
883
972
  selected: selectedOffset,
884
- type: "tabset"
885
- };
886
- placed.weight = 50;
887
- placeBesideTarget(draft, placed, target.id, where);
973
+ type: "tabset",
974
+ weight: 1
975
+ }, target.id, where);
888
976
  };
889
977
 
890
978
  //#endregion
891
979
  //#region src/state/reducer.ts
892
- const assertNever = (value) => {
893
- throw new Error(`Unhandled action: ${JSON.stringify(value)}`);
980
+ const parseNodeAttrs = (node, attrs) => {
981
+ switch (node.type) {
982
+ case "row": return mutableRowAttrsSchema.parse(attrs);
983
+ case "tab": return mutableTabAttrsSchema.parse(attrs);
984
+ case "tabset": {
985
+ const parsed = mutableTabsetAttrsSchema.parse(attrs);
986
+ return parsed.selected === void 0 ? parsed : {
987
+ ...parsed,
988
+ selected: clampSelected(node.children.length, parsed.selected)
989
+ };
990
+ }
991
+ default: return assertNever(node);
992
+ }
894
993
  };
895
994
  const applyAction = (draft, action) => {
896
995
  switch (action.type) {
@@ -914,7 +1013,7 @@ const applyAction = (draft, action) => {
914
1013
  }
915
1014
  case "deleteTab": {
916
1015
  const location = findTab(draft, action.tabId);
917
- if (location) location.container.children.splice(location.index, 1);
1016
+ if (location) detachTab(location.container, location.index);
918
1017
  return;
919
1018
  }
920
1019
  case "deleteTabset": {
@@ -928,7 +1027,7 @@ const applyAction = (draft, action) => {
928
1027
  case "floatTab": {
929
1028
  const location = findTab(draft, action.tabId);
930
1029
  if (!location) return;
931
- const removed = location.container.children.splice(location.index, 1).at(0);
1030
+ const removed = detachTab(location.container, location.index);
932
1031
  if (removed) pushFloat(draft, wrapTabInLayout(removed), action.geometry, action.floatId);
933
1032
  return;
934
1033
  }
@@ -943,7 +1042,7 @@ const applyAction = (draft, action) => {
943
1042
  case "moveNode": {
944
1043
  const source = findTab(draft, action.sourceId);
945
1044
  if (!source) return;
946
- const removed = source.container.children.splice(source.index, 1).at(0);
1045
+ const removed = detachTab(source.container, source.index);
947
1046
  if (!removed) return;
948
1047
  insertTab(draft, removed, {
949
1048
  id: action.targetId,
@@ -965,17 +1064,15 @@ const applyAction = (draft, action) => {
965
1064
  }
966
1065
  return;
967
1066
  }
968
- if (action.location.startsWith("split-")) {
969
- const sourceRoot = findRootContaining(draft, action.sourceId);
970
- const detached = sourceRoot ? removeTabsetReturning(sourceRoot, action.sourceId) : void 0;
971
- if (detached) placeBesideTarget(draft, detached, action.targetId, action.location);
972
- }
1067
+ const sourceRoot = findRootContaining(draft, action.sourceId);
1068
+ const detached = sourceRoot ? removeTabsetReturning(sourceRoot, action.sourceId) : void 0;
1069
+ if (detached) placeBesideTarget(draft, detached, action.targetId, action.location);
973
1070
  return;
974
1071
  }
975
1072
  case "renameFloat": {
976
1073
  const float = findFloat(draft, action.floatId);
977
1074
  if (float) {
978
- const others = (draft.floats ?? []).filter((other) => other.id !== action.floatId);
1075
+ const others = draft.floats.filter((other) => other.id !== action.floatId);
979
1076
  float.name = uniqueFloatName(action.name, others);
980
1077
  }
981
1078
  return;
@@ -987,7 +1084,7 @@ const applyAction = (draft, action) => {
987
1084
  }
988
1085
  case "selectTab": {
989
1086
  const tabset = findTabset(draft, action.tabsetId);
990
- if (tabset) tabset.selected = action.index;
1087
+ if (tabset) tabset.selected = clampSelected(tabset.children.length, action.index);
991
1088
  return;
992
1089
  }
993
1090
  case "setActiveTabset":
@@ -1006,7 +1103,7 @@ const applyAction = (draft, action) => {
1006
1103
  return;
1007
1104
  case "updateNodeAttributes": {
1008
1105
  const node = findAttributedNode(draft, action.nodeId);
1009
- if (node) Object.assign(node, action.attrs);
1106
+ if (node) Object.assign(node, parseNodeAttrs(node, action.attrs));
1010
1107
  return;
1011
1108
  }
1012
1109
  default: assertNever(action);
@@ -1164,12 +1261,13 @@ const stackModel = (model, orientation = "column") => {
1164
1261
  children,
1165
1262
  id: model.layout.id,
1166
1263
  orientation,
1167
- type: "row"
1264
+ type: "row",
1265
+ weight: model.layout.weight
1168
1266
  },
1169
1267
  maximizedTabsetId: void 0
1170
1268
  });
1171
1269
  };
1172
1270
 
1173
1271
  //#endregion
1174
- export { DEFAULT_SNAP_THRESHOLD, HISTORY_LIMIT, actionSchema, canRedo, canUndo, collectRoots, collectTabsets, collectTabsetsInRow, createHistory, createNodeId, createTabId, dashfooMachine, dashfooSchema, dimensionSchema, dispatch, dockLocationSchema, dockZonePolygons, dragDockMachine, edgeSchema, findAttributedNode, findDuplicateIds, findFloat, findRootContaining, findRow, findTab, findTabset, findTabsetParent, floatNode, floatNodeSchema, fromJSON, geometrySchema, getFirstTabset, globalAttributesSchema, jsonValueSchema, model, mutableNodeAttrsSchema, normalize, orientationSchema, parseModel, redo, reducer, resolveDockTarget, resolveSnapTargets, row, rowNodeSchema, snapEnabled, snapSchema, snapSizes, stackModel, tab, tabNodeSchema, tabset, tabsetNodeSchema, toJSON, undo, unitSchema, zoneRect };
1272
+ export { DEFAULT_SNAP_THRESHOLD, DEFAULT_WEIGHT, HISTORY_LIMIT, actionSchema, canRedo, canUndo, collectRoots, collectTabsets, collectTabsetsInRow, createHistory, createNodeId, createTabId, dashfooMachine, dashfooSchema, decideSnap, dimensionSchema, dispatch, dockLocationFor, dockLocationSchema, dockZonePolygons, dragDockMachine, edgeSchema, findAttributedNode, findDuplicateIds, findFloat, findRootContaining, findRow, findTab, findTabset, findTabsetParent, floatNode, floatNodeSchema, fromJSON, geometrySchema, getFirstTabset, globalAttributesSchema, jsonValueSchema, model, mutableNodeAttrsSchema, normalize, orientationSchema, parseModel, redo, reducer, resolveDockTarget, resolveSnapTargets, row, rowNodeSchema, settleSnap, snapEnabled, snapSchema, snapSizes, splitEdge, stackModel, tab, tabNodeSchema, tabset, tabsetNodeObjectSchema, tabsetNodeSchema, toJSON, undo, unitSchema, zoneRect };
1175
1273
  //# sourceMappingURL=index.js.map