@dashfoo/core 0.5.1 → 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/README.md +20 -20
- package/dist/index.d.ts +250 -159
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +283 -160
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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().
|
|
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().
|
|
102
|
+
weight: z.number().default(1)
|
|
68
103
|
}));
|
|
69
104
|
const geometrySchema = z.object({
|
|
70
105
|
height: z.number(),
|
|
@@ -95,15 +130,15 @@ const globalAttributesSchema = z.object({
|
|
|
95
130
|
});
|
|
96
131
|
const dashfooSchema = z.object({
|
|
97
132
|
activeTabsetId: z.string().optional(),
|
|
133
|
+
floats: z.array(floatNodeSchema).default(() => []),
|
|
98
134
|
global: globalAttributesSchema,
|
|
99
135
|
layout: rowNodeSchema,
|
|
100
136
|
maximizedTabsetId: z.string().optional(),
|
|
101
|
-
version: z.literal(1)
|
|
102
|
-
floats: z.array(floatNodeSchema).optional()
|
|
137
|
+
version: z.literal(1)
|
|
103
138
|
});
|
|
104
139
|
|
|
105
140
|
//#endregion
|
|
106
|
-
//#region src/state/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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",
|
|
@@ -138,6 +185,7 @@ const dockLocationSchema = z.enum([
|
|
|
138
185
|
"split-right",
|
|
139
186
|
"split-top"
|
|
140
187
|
]);
|
|
188
|
+
const splitWeightsSchema = z.array(z.number());
|
|
141
189
|
const actionSchema = z.discriminatedUnion("type", [
|
|
142
190
|
z.object({
|
|
143
191
|
index: z.number().int().optional(),
|
|
@@ -189,7 +237,7 @@ const actionSchema = z.discriminatedUnion("type", [
|
|
|
189
237
|
z.object({
|
|
190
238
|
rowId: z.string(),
|
|
191
239
|
type: z.literal("adjustSplit"),
|
|
192
|
-
weights:
|
|
240
|
+
weights: splitWeightsSchema
|
|
193
241
|
}),
|
|
194
242
|
z.object({
|
|
195
243
|
attrs: mutableNodeAttrsSchema,
|
|
@@ -242,11 +290,54 @@ const createTabId = () => createNodeId("tab");
|
|
|
242
290
|
|
|
243
291
|
//#endregion
|
|
244
292
|
//#region src/model/tree.ts
|
|
245
|
-
const collectRoots = (model) => [model.layout, ...
|
|
246
|
-
const findFloat = (model, 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);
|
|
295
|
+
/**
|
|
296
|
+
* Depth-first pre-order over one root: the row itself, then each child in
|
|
297
|
+
* order, descending into nested rows and into a tabset's tabs. Returning a
|
|
298
|
+
* defined value from `visit` stops the walk and becomes the result, so one
|
|
299
|
+
* traversal serves both find-first and collect-all.
|
|
300
|
+
*
|
|
301
|
+
* Every query below used to carry its own copy of this recursion, and they had
|
|
302
|
+
* drifted in child order: some scanned a row's own tabsets before descending,
|
|
303
|
+
* others interleaved. With unique ids all orders agree, so they now share this
|
|
304
|
+
* one; a model with duplicate ids (which the builder and the parser both warn
|
|
305
|
+
* about) can see a different duplicate win than it did before.
|
|
306
|
+
*/
|
|
307
|
+
const walk = (row, visit, parent, index = -1) => {
|
|
308
|
+
const atRoot = visit({
|
|
309
|
+
index,
|
|
310
|
+
node: row,
|
|
311
|
+
parent
|
|
312
|
+
});
|
|
313
|
+
if (atRoot !== void 0) return atRoot;
|
|
314
|
+
for (const [childIndex, child] of row.children.entries()) {
|
|
315
|
+
if (child.type === "row") {
|
|
316
|
+
const inRow = walk(child, visit, row, childIndex);
|
|
317
|
+
if (inRow !== void 0) return inRow;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const atTabset = visit({
|
|
321
|
+
index: childIndex,
|
|
322
|
+
node: child,
|
|
323
|
+
parent: row
|
|
324
|
+
});
|
|
325
|
+
if (atTabset !== void 0) return atTabset;
|
|
326
|
+
for (const [tabIndex, tab] of child.children.entries()) {
|
|
327
|
+
const atTab = visit({
|
|
328
|
+
index: tabIndex,
|
|
329
|
+
node: tab,
|
|
330
|
+
parent: child
|
|
331
|
+
});
|
|
332
|
+
if (atTab !== void 0) return atTab;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const locate = (row, id) => walk(row, (found) => found.node.id === id ? found : void 0);
|
|
247
337
|
const collectTabsetsInRow = (row, acc) => {
|
|
248
|
-
|
|
249
|
-
|
|
338
|
+
walk(row, ({ node }) => {
|
|
339
|
+
if (node.type === "tabset") acc.push(node);
|
|
340
|
+
});
|
|
250
341
|
};
|
|
251
342
|
const collectTabsets = (model) => {
|
|
252
343
|
const acc = [];
|
|
@@ -258,7 +349,7 @@ const findTabset = (model, tabsetId) => collectTabsets(model).find((tabset) => t
|
|
|
258
349
|
const findTabInContainer = (container, tabId) => {
|
|
259
350
|
const index = container.children.findIndex((tab) => tab.id === tabId);
|
|
260
351
|
if (index === -1) return;
|
|
261
|
-
const tab = container.children
|
|
352
|
+
const tab = container.children.at(index);
|
|
262
353
|
if (!tab) return;
|
|
263
354
|
return {
|
|
264
355
|
container,
|
|
@@ -273,25 +364,10 @@ const findTab = (model, tabId) => {
|
|
|
273
364
|
}
|
|
274
365
|
};
|
|
275
366
|
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
|
-
}
|
|
367
|
+
const found = locate(row, id)?.node;
|
|
368
|
+
return found?.type === "row" ? found : void 0;
|
|
294
369
|
};
|
|
370
|
+
const findAttributedNodeInRow = (row, id) => locate(row, id)?.node;
|
|
295
371
|
const findAttributedNode = (model, id) => {
|
|
296
372
|
for (const root of collectRoots(model)) {
|
|
297
373
|
const found = findAttributedNodeInRow(root, id);
|
|
@@ -302,27 +378,20 @@ const findRootContaining = (model, nodeId) => {
|
|
|
302
378
|
for (const root of collectRoots(model)) if (findAttributedNodeInRow(root, nodeId)) return root;
|
|
303
379
|
};
|
|
304
380
|
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
|
-
}
|
|
381
|
+
const found = locate(row, tabsetId);
|
|
382
|
+
return found?.node.type === "tabset" && found.parent?.type === "row" ? {
|
|
383
|
+
index: found.index,
|
|
384
|
+
parent: found.parent
|
|
385
|
+
} : void 0;
|
|
314
386
|
};
|
|
315
387
|
const collectIdsInRow = (row, acc) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
acc.push(child.id);
|
|
320
|
-
for (const tab of child.children) acc.push(tab.id);
|
|
321
|
-
}
|
|
388
|
+
walk(row, ({ node }) => {
|
|
389
|
+
acc.push(node.id);
|
|
390
|
+
});
|
|
322
391
|
};
|
|
323
392
|
const findDuplicateIds = (model) => {
|
|
324
393
|
const ids = [];
|
|
325
|
-
for (const float of model.floats
|
|
394
|
+
for (const float of model.floats) ids.push(float.id);
|
|
326
395
|
for (const root of collectRoots(model)) collectIdsInRow(root, ids);
|
|
327
396
|
const seen = /* @__PURE__ */ new Set();
|
|
328
397
|
const duplicates = /* @__PURE__ */ new Set();
|
|
@@ -355,7 +424,8 @@ const tabset = (children, options = {}) => {
|
|
|
355
424
|
...max === void 0 ? {} : { max: toDimension(max) },
|
|
356
425
|
...min === void 0 ? {} : { min: toDimension(min) },
|
|
357
426
|
id: options.id ?? createNodeId("tabset"),
|
|
358
|
-
selected: options.selected ?? 0
|
|
427
|
+
selected: clampSelected(children.length, options.selected ?? 0),
|
|
428
|
+
weight: options.weight ?? 1
|
|
359
429
|
};
|
|
360
430
|
};
|
|
361
431
|
const row = (children, options = {}) => {
|
|
@@ -367,7 +437,8 @@ const row = (children, options = {}) => {
|
|
|
367
437
|
...max === void 0 ? {} : { max: toDimension(max) },
|
|
368
438
|
...min === void 0 ? {} : { min: toDimension(min) },
|
|
369
439
|
id: options.id ?? createNodeId("row"),
|
|
370
|
-
orientation: options.orientation ?? "row"
|
|
440
|
+
orientation: options.orientation ?? "row",
|
|
441
|
+
weight: options.weight ?? 1
|
|
371
442
|
};
|
|
372
443
|
};
|
|
373
444
|
const floatNode = (layout, geometry, options = {}) => ({
|
|
@@ -382,13 +453,20 @@ const model = (layout, options = {}) => {
|
|
|
382
453
|
layout,
|
|
383
454
|
version: 1,
|
|
384
455
|
...options,
|
|
456
|
+
floats: options.floats ?? [],
|
|
385
457
|
global: options.global ?? {}
|
|
386
458
|
};
|
|
387
459
|
const duplicates = findDuplicateIds(built);
|
|
388
|
-
if (duplicates.length > 0) console.warn(`[dashfoo] builder produced duplicate node ids: ${duplicates.join(", ")}
|
|
460
|
+
if (duplicates.length > 0) console.warn(`[dashfoo] builder produced duplicate node ids: ${duplicates.join(", ")}. Pass explicit ids when reusing a component`);
|
|
389
461
|
return built;
|
|
390
462
|
};
|
|
391
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
|
+
|
|
392
470
|
//#endregion
|
|
393
471
|
//#region src/geometry/geometry.ts
|
|
394
472
|
const DEFAULT_BAND_FRACTION = .22;
|
|
@@ -409,6 +487,23 @@ const closestEdge = (d) => {
|
|
|
409
487
|
if (min === d.top) return "top";
|
|
410
488
|
return "bottom";
|
|
411
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
|
+
};
|
|
412
507
|
const resolveDockTarget = (pointer, rect, opts) => {
|
|
413
508
|
const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
|
|
414
509
|
if (rect.width <= 0 || rect.height <= 0) return { kind: "tab" };
|
|
@@ -541,7 +636,8 @@ const zoneRect = (rect, location) => {
|
|
|
541
636
|
x: rect.x,
|
|
542
637
|
y: rect.y
|
|
543
638
|
};
|
|
544
|
-
|
|
639
|
+
case "center": return rect;
|
|
640
|
+
default: return assertNever(location);
|
|
545
641
|
}
|
|
546
642
|
};
|
|
547
643
|
|
|
@@ -603,35 +699,54 @@ const snapSizes = (sizes, boundaryIndex, config) => {
|
|
|
603
699
|
snapped: true
|
|
604
700
|
};
|
|
605
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
|
+
};
|
|
606
714
|
|
|
607
715
|
//#endregion
|
|
608
716
|
//#region src/model/invariants.ts
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
717
|
+
/**
|
|
718
|
+
* A row that collapses to a single child hands its slot in the parent to
|
|
719
|
+
* that child, so the slot's sizing constraints have to travel with it.
|
|
720
|
+
* Dropping them silently widens a pinned sidebar, and `normalize` runs on
|
|
721
|
+
* load and on every persist, so the loss is permanent.
|
|
722
|
+
*
|
|
723
|
+
* `snap` only exists on rows, so it is carried only to a row survivor.
|
|
724
|
+
*/
|
|
725
|
+
const inheritSlot = (row, survivor) => {
|
|
726
|
+
const sized = {
|
|
727
|
+
...survivor,
|
|
728
|
+
weight: row.weight,
|
|
729
|
+
...row.min === void 0 ? {} : { min: row.min },
|
|
730
|
+
...row.max === void 0 ? {} : { max: row.max }
|
|
731
|
+
};
|
|
732
|
+
if (sized.type === "row" && row.snap !== void 0) return {
|
|
733
|
+
...sized,
|
|
734
|
+
snap: row.snap
|
|
735
|
+
};
|
|
736
|
+
return sized;
|
|
615
737
|
};
|
|
616
|
-
const normalizeTabset = (tabset) => ({
|
|
617
|
-
...tabset,
|
|
618
|
-
selected: clampSelected(tabset.children.length, tabset.selected)
|
|
619
|
-
});
|
|
620
738
|
const normalizeRowChildren = (children) => {
|
|
621
739
|
const out = [];
|
|
622
740
|
for (const child of children) {
|
|
623
741
|
if (child.type === "tabset") {
|
|
624
|
-
if (child.children.length > 0) out.push(
|
|
742
|
+
if (child.children.length > 0) out.push(child);
|
|
625
743
|
continue;
|
|
626
744
|
}
|
|
627
745
|
const grandchildren = normalizeRowChildren(child.children);
|
|
628
746
|
if (grandchildren.length === 0) continue;
|
|
629
747
|
if (grandchildren.length === 1) {
|
|
630
748
|
const only = grandchildren[0];
|
|
631
|
-
if (only !== void 0) out.push(child
|
|
632
|
-
...only,
|
|
633
|
-
weight: child.weight
|
|
634
|
-
});
|
|
749
|
+
if (only !== void 0) out.push(inheritSlot(child, only));
|
|
635
750
|
continue;
|
|
636
751
|
}
|
|
637
752
|
out.push({
|
|
@@ -659,22 +774,22 @@ const normalizeLayout = (root) => {
|
|
|
659
774
|
};
|
|
660
775
|
const normalize = (model) => {
|
|
661
776
|
const layout = normalizeLayout(model.layout);
|
|
662
|
-
const
|
|
663
|
-
for (const float of model.floats
|
|
777
|
+
const floats = [];
|
|
778
|
+
for (const float of model.floats) {
|
|
664
779
|
const floatLayout = normalizeLayout(float.layout);
|
|
665
|
-
if (rowContainsTabset(floatLayout))
|
|
780
|
+
if (rowContainsTabset(floatLayout)) floats.push({
|
|
666
781
|
...float,
|
|
667
782
|
layout: floatLayout
|
|
668
783
|
});
|
|
669
784
|
}
|
|
670
|
-
const floats = healedFloats.length > 0 ? healedFloats : void 0;
|
|
671
785
|
const withRoots = {
|
|
672
786
|
...model,
|
|
673
787
|
floats,
|
|
674
788
|
layout
|
|
675
789
|
};
|
|
676
|
-
const
|
|
677
|
-
const
|
|
790
|
+
const tabsets = collectTabsets(withRoots);
|
|
791
|
+
const tabsetIds = new Set(tabsets.map((tabset) => tabset.id));
|
|
792
|
+
const firstTabsetId = tabsets[0]?.id;
|
|
678
793
|
const activeTabsetId = model.activeTabsetId !== void 0 && tabsetIds.has(model.activeTabsetId) ? model.activeTabsetId : firstTabsetId;
|
|
679
794
|
const maximizedTabsetId = model.maximizedTabsetId !== void 0 && tabsetIds.has(model.maximizedTabsetId) ? model.maximizedTabsetId : void 0;
|
|
680
795
|
return {
|
|
@@ -686,50 +801,40 @@ const normalize = (model) => {
|
|
|
686
801
|
|
|
687
802
|
//#endregion
|
|
688
803
|
//#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
804
|
const removeTabsetReturning = (row, tabsetId) => {
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
805
|
+
const found = findTabsetParent(row, tabsetId);
|
|
806
|
+
if (!found) return;
|
|
807
|
+
const [removed] = found.parent.children.splice(found.index, 1);
|
|
808
|
+
return removed?.type === "tabset" ? removed : void 0;
|
|
809
|
+
};
|
|
810
|
+
const removeTabset = (row, tabsetId) => removeTabsetReturning(row, tabsetId) !== void 0;
|
|
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
|
+
};
|
|
708
817
|
};
|
|
709
|
-
const splitOrientation = (location) => location === "split-left" || location === "split-right" ? "row" : "column";
|
|
710
|
-
const splitsBefore = (location) => location === "split-left" || location === "split-top";
|
|
711
818
|
const placeBesideTarget = (draft, placed, targetId, location) => {
|
|
712
819
|
const targetTabset = findTabset(draft, targetId);
|
|
713
820
|
const found = findTabsetParent(findRootContaining(draft, targetId) ?? draft.layout, targetId);
|
|
714
821
|
if (!targetTabset || !found) return;
|
|
715
|
-
const orientation =
|
|
716
|
-
const before = splitsBefore(location);
|
|
822
|
+
const { before, orientation } = splitPlacement(location);
|
|
717
823
|
if (found.parent.orientation === orientation) {
|
|
718
|
-
const
|
|
719
|
-
targetTabset.weight =
|
|
720
|
-
placed.weight =
|
|
824
|
+
const half = targetTabset.weight / 2;
|
|
825
|
+
targetTabset.weight = half;
|
|
826
|
+
placed.weight = half;
|
|
721
827
|
found.parent.children.splice(before ? found.index : found.index + 1, 0, placed);
|
|
722
828
|
} else {
|
|
723
|
-
const targetWeight = targetTabset.weight;
|
|
724
|
-
targetTabset.weight = 50;
|
|
725
|
-
placed.weight = 50;
|
|
726
829
|
const newRow = {
|
|
727
830
|
children: before ? [placed, targetTabset] : [targetTabset, placed],
|
|
728
831
|
id: createNodeId("row"),
|
|
729
832
|
orientation,
|
|
730
|
-
type: "row"
|
|
833
|
+
type: "row",
|
|
834
|
+
weight: targetTabset.weight
|
|
731
835
|
};
|
|
732
|
-
|
|
836
|
+
targetTabset.weight = 50;
|
|
837
|
+
placed.weight = 50;
|
|
733
838
|
found.parent.children.splice(found.index, 1, newRow);
|
|
734
839
|
}
|
|
735
840
|
draft.activeTabsetId = placed.id;
|
|
@@ -739,39 +844,54 @@ const insertTab = (draft, tab, target) => {
|
|
|
739
844
|
const targetTabset = findTabset(draft, targetId);
|
|
740
845
|
if (!targetTabset) return;
|
|
741
846
|
if (location === "center") {
|
|
742
|
-
const at = index ?? targetTabset.children.length;
|
|
847
|
+
const at = Math.min(Math.max(index ?? targetTabset.children.length, 0), targetTabset.children.length);
|
|
743
848
|
targetTabset.children.splice(at, 0, tab);
|
|
744
849
|
targetTabset.selected = at;
|
|
745
850
|
return;
|
|
746
851
|
}
|
|
747
|
-
|
|
852
|
+
const newTabset = {
|
|
748
853
|
children: [tab],
|
|
749
854
|
id: createNodeId("tabset"),
|
|
750
855
|
selected: 0,
|
|
751
856
|
type: "tabset",
|
|
752
|
-
weight:
|
|
753
|
-
}
|
|
857
|
+
weight: 1
|
|
858
|
+
};
|
|
859
|
+
placeBesideTarget(draft, newTabset, targetId, location);
|
|
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;
|
|
754
870
|
};
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
871
|
+
const mergeTabsInto = (target, tabs, selectedOffset) => {
|
|
872
|
+
const mergeStart = target.children.length;
|
|
873
|
+
target.children.push(...tabs);
|
|
874
|
+
target.selected = mergeStart + selectedOffset;
|
|
758
875
|
};
|
|
759
876
|
const wrapTabInLayout = (tab) => ({
|
|
760
877
|
children: [{
|
|
761
878
|
children: [tab],
|
|
762
879
|
id: createNodeId("tabset"),
|
|
763
880
|
selected: 0,
|
|
764
|
-
type: "tabset"
|
|
881
|
+
type: "tabset",
|
|
882
|
+
weight: 1
|
|
765
883
|
}],
|
|
766
884
|
id: createNodeId("row"),
|
|
767
885
|
orientation: "row",
|
|
768
|
-
type: "row"
|
|
886
|
+
type: "row",
|
|
887
|
+
weight: 1
|
|
769
888
|
});
|
|
770
889
|
const wrapTabsetInLayout = (tabset) => ({
|
|
771
890
|
children: [tabset],
|
|
772
891
|
id: createNodeId("row"),
|
|
773
892
|
orientation: "row",
|
|
774
|
-
type: "row"
|
|
893
|
+
type: "row",
|
|
894
|
+
weight: 1
|
|
775
895
|
});
|
|
776
896
|
|
|
777
897
|
//#endregion
|
|
@@ -794,13 +914,13 @@ const pushFloat = (draft, layout, geometry, floatId) => {
|
|
|
794
914
|
geometry: geometry ?? DEFAULT_FLOAT_GEOMETRY,
|
|
795
915
|
id: floatId ?? createNodeId("float"),
|
|
796
916
|
layout,
|
|
797
|
-
name: uniqueFloatName("Panel", draft.floats
|
|
917
|
+
name: uniqueFloatName("Panel", draft.floats),
|
|
798
918
|
type: "float"
|
|
799
919
|
};
|
|
800
|
-
draft.floats = [...draft.floats
|
|
920
|
+
draft.floats = [...draft.floats, float];
|
|
801
921
|
const tabsets = [];
|
|
802
|
-
|
|
803
|
-
const first = tabsets
|
|
922
|
+
collectTabsetsInRow(layout, tabsets);
|
|
923
|
+
const first = tabsets.at(0);
|
|
804
924
|
if (first) draft.activeTabsetId = first.id;
|
|
805
925
|
};
|
|
806
926
|
const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
|
|
@@ -811,8 +931,8 @@ const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
|
|
|
811
931
|
};
|
|
812
932
|
const resolveMainTarget = (draft, targetId) => {
|
|
813
933
|
const mainTabsets = [];
|
|
814
|
-
|
|
815
|
-
if (targetId) {
|
|
934
|
+
collectTabsetsInRow(draft.layout, mainTabsets);
|
|
935
|
+
if (targetId !== void 0) {
|
|
816
936
|
const explicit = mainTabsets.find((tabset) => tabset.id === targetId);
|
|
817
937
|
if (explicit) return explicit;
|
|
818
938
|
}
|
|
@@ -823,18 +943,17 @@ const resolveMainTarget = (draft, targetId) => {
|
|
|
823
943
|
return mainTabsets[0];
|
|
824
944
|
};
|
|
825
945
|
const dockFloat = (draft, floatId, targetId, location) => {
|
|
826
|
-
const floats = draft.floats
|
|
946
|
+
const floats = draft.floats;
|
|
827
947
|
const index = floats.findIndex((float) => float.id === floatId);
|
|
828
948
|
if (index === -1) return;
|
|
829
|
-
const
|
|
830
|
-
draft.floats = floats;
|
|
949
|
+
const float = floats.splice(index, 1).at(0);
|
|
831
950
|
if (!float) return;
|
|
832
951
|
const tabsets = [];
|
|
833
|
-
|
|
952
|
+
collectTabsetsInRow(float.layout, tabsets);
|
|
834
953
|
const tabs = tabsets.flatMap((tabset) => tabset.children);
|
|
835
954
|
if (tabs.length === 0) return;
|
|
836
|
-
const leadTabset = tabsets
|
|
837
|
-
const selectedOffset = leadTabset ?
|
|
955
|
+
const leadTabset = tabsets.at(0);
|
|
956
|
+
const selectedOffset = leadTabset ? clampSelected(leadTabset.children.length, leadTabset.selected) : 0;
|
|
838
957
|
const target = resolveMainTarget(draft, targetId);
|
|
839
958
|
if (!target) {
|
|
840
959
|
draft.layout = float.layout;
|
|
@@ -842,27 +961,35 @@ const dockFloat = (draft, floatId, targetId, location) => {
|
|
|
842
961
|
return;
|
|
843
962
|
}
|
|
844
963
|
if (location === "center") {
|
|
845
|
-
|
|
846
|
-
target.children.push(...tabs);
|
|
847
|
-
target.selected = mergeStart + selectedOffset;
|
|
964
|
+
mergeTabsInto(target, tabs, selectedOffset);
|
|
848
965
|
draft.activeTabsetId = target.id;
|
|
849
966
|
return;
|
|
850
967
|
}
|
|
851
968
|
const where = location ?? "split-right";
|
|
852
|
-
|
|
969
|
+
placeBesideTarget(draft, tabsets.length === 1 && leadTabset ? leadTabset : {
|
|
853
970
|
children: tabs,
|
|
854
971
|
id: createNodeId("tabset"),
|
|
855
972
|
selected: selectedOffset,
|
|
856
|
-
type: "tabset"
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
placeBesideTarget(draft, placed, target.id, where);
|
|
973
|
+
type: "tabset",
|
|
974
|
+
weight: 1
|
|
975
|
+
}, target.id, where);
|
|
860
976
|
};
|
|
861
977
|
|
|
862
978
|
//#endregion
|
|
863
979
|
//#region src/state/reducer.ts
|
|
864
|
-
const
|
|
865
|
-
|
|
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
|
+
}
|
|
866
993
|
};
|
|
867
994
|
const applyAction = (draft, action) => {
|
|
868
995
|
switch (action.type) {
|
|
@@ -877,7 +1004,7 @@ const applyAction = (draft, action) => {
|
|
|
877
1004
|
const row = findRow(findRootContaining(draft, action.rowId) ?? draft.layout, action.rowId);
|
|
878
1005
|
if (!row) return;
|
|
879
1006
|
for (let index = 0; index < action.weights.length; index++) {
|
|
880
|
-
const child = row.children
|
|
1007
|
+
const child = row.children.at(index);
|
|
881
1008
|
const weight = action.weights[index];
|
|
882
1009
|
if (!child || weight === void 0) continue;
|
|
883
1010
|
child.weight = weight;
|
|
@@ -886,7 +1013,7 @@ const applyAction = (draft, action) => {
|
|
|
886
1013
|
}
|
|
887
1014
|
case "deleteTab": {
|
|
888
1015
|
const location = findTab(draft, action.tabId);
|
|
889
|
-
if (location) location.container
|
|
1016
|
+
if (location) detachTab(location.container, location.index);
|
|
890
1017
|
return;
|
|
891
1018
|
}
|
|
892
1019
|
case "deleteTabset": {
|
|
@@ -900,7 +1027,7 @@ const applyAction = (draft, action) => {
|
|
|
900
1027
|
case "floatTab": {
|
|
901
1028
|
const location = findTab(draft, action.tabId);
|
|
902
1029
|
if (!location) return;
|
|
903
|
-
const
|
|
1030
|
+
const removed = detachTab(location.container, location.index);
|
|
904
1031
|
if (removed) pushFloat(draft, wrapTabInLayout(removed), action.geometry, action.floatId);
|
|
905
1032
|
return;
|
|
906
1033
|
}
|
|
@@ -915,7 +1042,7 @@ const applyAction = (draft, action) => {
|
|
|
915
1042
|
case "moveNode": {
|
|
916
1043
|
const source = findTab(draft, action.sourceId);
|
|
917
1044
|
if (!source) return;
|
|
918
|
-
const
|
|
1045
|
+
const removed = detachTab(source.container, source.index);
|
|
919
1046
|
if (!removed) return;
|
|
920
1047
|
insertTab(draft, removed, {
|
|
921
1048
|
id: action.targetId,
|
|
@@ -931,26 +1058,21 @@ const applyAction = (draft, action) => {
|
|
|
931
1058
|
if (action.location === "center") {
|
|
932
1059
|
const target = findTabset(draft, action.targetId);
|
|
933
1060
|
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;
|
|
1061
|
+
mergeTabsInto(target, source.children, clampSelected(source.children.length, source.selected));
|
|
938
1062
|
const sourceRoot = findRootContaining(draft, action.sourceId);
|
|
939
1063
|
if (sourceRoot) removeTabset(sourceRoot, action.sourceId);
|
|
940
1064
|
}
|
|
941
1065
|
return;
|
|
942
1066
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (detached) placeBesideTarget(draft, detached, action.targetId, action.location);
|
|
947
|
-
}
|
|
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);
|
|
948
1070
|
return;
|
|
949
1071
|
}
|
|
950
1072
|
case "renameFloat": {
|
|
951
1073
|
const float = findFloat(draft, action.floatId);
|
|
952
1074
|
if (float) {
|
|
953
|
-
const others =
|
|
1075
|
+
const others = draft.floats.filter((other) => other.id !== action.floatId);
|
|
954
1076
|
float.name = uniqueFloatName(action.name, others);
|
|
955
1077
|
}
|
|
956
1078
|
return;
|
|
@@ -962,7 +1084,7 @@ const applyAction = (draft, action) => {
|
|
|
962
1084
|
}
|
|
963
1085
|
case "selectTab": {
|
|
964
1086
|
const tabset = findTabset(draft, action.tabsetId);
|
|
965
|
-
if (tabset) tabset.selected = action.index;
|
|
1087
|
+
if (tabset) tabset.selected = clampSelected(tabset.children.length, action.index);
|
|
966
1088
|
return;
|
|
967
1089
|
}
|
|
968
1090
|
case "setActiveTabset":
|
|
@@ -981,7 +1103,7 @@ const applyAction = (draft, action) => {
|
|
|
981
1103
|
return;
|
|
982
1104
|
case "updateNodeAttributes": {
|
|
983
1105
|
const node = findAttributedNode(draft, action.nodeId);
|
|
984
|
-
if (node) Object.assign(node, action.attrs);
|
|
1106
|
+
if (node) Object.assign(node, parseNodeAttrs(node, action.attrs));
|
|
985
1107
|
return;
|
|
986
1108
|
}
|
|
987
1109
|
default: assertNever(action);
|
|
@@ -1139,12 +1261,13 @@ const stackModel = (model, orientation = "column") => {
|
|
|
1139
1261
|
children,
|
|
1140
1262
|
id: model.layout.id,
|
|
1141
1263
|
orientation,
|
|
1142
|
-
type: "row"
|
|
1264
|
+
type: "row",
|
|
1265
|
+
weight: model.layout.weight
|
|
1143
1266
|
},
|
|
1144
1267
|
maximizedTabsetId: void 0
|
|
1145
1268
|
});
|
|
1146
1269
|
};
|
|
1147
1270
|
|
|
1148
1271
|
//#endregion
|
|
1149
|
-
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 };
|
|
1150
1273
|
//# sourceMappingURL=index.js.map
|