@adecore/ui 0.18.4 → 0.20.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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/dist/ColumnResizeHandle.d.ts +3 -2
  3. package/dist/ColumnResizeHandle.js +2 -2
  4. package/dist/SlidingColumn.d.ts +2 -1
  5. package/dist/SlidingColumn.js +11 -4
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.js +4 -0
  8. package/dist/locales/en.json +10 -0
  9. package/dist/locales/nl.json +10 -0
  10. package/dist/theme.css +181 -0
  11. package/dist/useColumnResize.js +12 -17
  12. package/dist/workspace/SplitView.d.ts +49 -0
  13. package/dist/workspace/SplitView.js +421 -0
  14. package/dist/workspace/TabStrip.d.ts +42 -0
  15. package/dist/workspace/TabStrip.js +168 -0
  16. package/dist/workspace/Workspace.d.ts +15 -0
  17. package/dist/workspace/Workspace.js +27 -0
  18. package/dist/workspace/context.d.ts +6 -0
  19. package/dist/workspace/context.js +5 -0
  20. package/dist/workspace/geometry.d.ts +39 -0
  21. package/dist/workspace/geometry.js +153 -0
  22. package/dist/workspace/model.d.ts +83 -0
  23. package/dist/workspace/model.js +360 -0
  24. package/dist/workspace/preview.d.ts +35 -0
  25. package/dist/workspace/preview.js +72 -0
  26. package/package.json +1 -1
  27. package/src/ColumnResizeHandle.tsx +4 -3
  28. package/src/SlidingColumn.tsx +23 -6
  29. package/src/index.ts +19 -0
  30. package/src/locales/en.json +10 -0
  31. package/src/locales/nl.json +10 -0
  32. package/src/theme.css +181 -0
  33. package/src/useColumnResize.ts +12 -18
  34. package/src/workspace/SplitView.tsx +656 -0
  35. package/src/workspace/TabStrip.tsx +296 -0
  36. package/src/workspace/Workspace.tsx +72 -0
  37. package/src/workspace/context.ts +8 -0
  38. package/src/workspace/geometry.ts +190 -0
  39. package/src/workspace/model.ts +440 -0
  40. package/src/workspace/preview.ts +116 -0
@@ -0,0 +1,27 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { useRender } from '@base-ui-components/react/use-render';
4
+ import { WorkspaceContext } from './context.js';
5
+ export function Workspace({ layout = 'standard', toolbar, sidebar, sidePanel, sidebarWidth = 248, sidePanelWidth = 320, sidebarOpen, sidePanelOpen, children, className, ref, render, style, ...props }) {
6
+ const hasSidebar = sidebar !== undefined && sidebar !== null && sidebar !== false;
7
+ const hasPanel = sidePanel !== undefined && sidePanel !== null && sidePanel !== false;
8
+ const leftOpen = hasSidebar && (sidebarOpen ?? true);
9
+ const rightOpen = hasPanel && (sidePanelOpen ?? true);
10
+ const hasToolbar = toolbar !== undefined && toolbar !== null && toolbar !== false;
11
+ const element = useRender({
12
+ render,
13
+ ref,
14
+ defaultTagName: 'div',
15
+ props: {
16
+ ...props,
17
+ className: clsx('ade-workspace', className),
18
+ style,
19
+ 'data-layout': layout,
20
+ 'data-toolbar': hasToolbar || undefined,
21
+ 'data-sidebar': leftOpen || undefined,
22
+ 'data-side-panel': rightOpen || undefined,
23
+ children: (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "ade-workspace-sidebar", "data-auto": sidebarWidth === 'auto' || undefined, style: { width: sidebarWidth }, children: sidebar })), _jsxs("div", { className: "ade-workspace-center", children: [hasToolbar && _jsx("div", { className: "ade-workspace-toolbar", children: toolbar }), _jsx("div", { className: "ade-workspace-content", children: children })] }), hasPanel && (_jsx("aside", { className: "ade-workspace-side-panel", "data-auto": sidePanelWidth === 'auto' || undefined, style: { width: sidePanelWidth }, children: sidePanel }))] }))
24
+ }
25
+ });
26
+ return _jsx(WorkspaceContext, { value: { layout, edges: { top: !hasToolbar, right: !rightOpen, bottom: true, left: !leftOpen } }, children: element });
27
+ }
@@ -0,0 +1,6 @@
1
+ import type { SplitEdges } from './geometry.ts';
2
+ export type WorkspaceLayout = 'standard' | 'roomy';
3
+ export declare const WorkspaceContext: import("react").Context<{
4
+ layout: WorkspaceLayout;
5
+ edges: SplitEdges;
6
+ }>;
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+ export const WorkspaceContext = createContext({
3
+ layout: 'standard',
4
+ edges: { top: true, right: true, bottom: true, left: true }
5
+ });
@@ -0,0 +1,39 @@
1
+ import { type SplitAxis, type SplitLayout, type SplitPane, type SplitSide } from './model.ts';
2
+ export interface SplitRect {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface SplitEdges {
9
+ top?: boolean;
10
+ right?: boolean;
11
+ bottom?: boolean;
12
+ left?: boolean;
13
+ }
14
+ export interface SplitMinimum {
15
+ width: number;
16
+ height: number;
17
+ }
18
+ export interface SplitPaneRect extends SplitRect {
19
+ pane: SplitPane;
20
+ }
21
+ export interface SplitDivider extends SplitRect {
22
+ splitId: string;
23
+ index: number;
24
+ axis: SplitAxis;
25
+ lengths: number[];
26
+ minimums: number[];
27
+ }
28
+ export interface SplitGeometry {
29
+ panes: SplitPaneRect[];
30
+ dividers: SplitDivider[];
31
+ }
32
+ export declare function splitGeometry(layout: SplitLayout, width: number, height: number, gap: number, minimum: (pane: SplitPane) => SplitMinimum): SplitGeometry;
33
+ export declare function resizeSplit(divider: SplitDivider, delta: number, symmetric?: boolean, snapDistance?: number, minimumShare?: number): number[];
34
+ export declare function paneRadius(rect: SplitRect, width: number, height: number, edges: SplitEdges, radius: number): string;
35
+ export declare function splitSideAt(rect: SplitRect, x: number, y: number): SplitSide | undefined;
36
+ export declare function tabGapAt(rects: readonly {
37
+ left: number;
38
+ right: number;
39
+ }[], x: number): number;
@@ -0,0 +1,153 @@
1
+ import { findSplitNode } from './model.js';
2
+ function distribute(total, weights, minimums) {
3
+ const floor = minimums.reduce((sum, size) => sum + size, 0);
4
+ if (floor >= total) {
5
+ return minimums.map((size) => (floor ? (size * total) / floor : total / minimums.length));
6
+ }
7
+ const result = weights.map(() => 0);
8
+ const pending = new Set(weights.map((_, i) => i));
9
+ let remaining = total;
10
+ while (pending.size) {
11
+ const sum = [...pending].reduce((value, i) => value + weights[i], 0);
12
+ const small = [...pending].filter((i) => (remaining * weights[i]) / sum < minimums[i]);
13
+ if (!small.length) {
14
+ pending.forEach((i) => {
15
+ result[i] = (remaining * weights[i]) / sum;
16
+ });
17
+ break;
18
+ }
19
+ small.forEach((i) => {
20
+ result[i] = minimums[i];
21
+ remaining -= minimums[i];
22
+ pending.delete(i);
23
+ });
24
+ }
25
+ return result;
26
+ }
27
+ export function splitGeometry(layout, width, height, gap, minimum) {
28
+ const result = { panes: [], dividers: [] };
29
+ const minimums = new Map();
30
+ const measure = (node) => {
31
+ if (node.type === 'pane') {
32
+ const value = minimum(node);
33
+ const safe = {
34
+ width: Math.max(0, Number.isFinite(value.width) ? value.width : 0),
35
+ height: Math.max(0, Number.isFinite(value.height) ? value.height : 0)
36
+ };
37
+ minimums.set(node.id, safe);
38
+ return safe;
39
+ }
40
+ const children = node.children.map(measure);
41
+ const horizontal = node.axis === 'horizontal';
42
+ const value = {
43
+ width: horizontal
44
+ ? children.reduce((sum, item) => sum + item.width, 0) + gap * (children.length - 1)
45
+ : Math.max(...children.map((item) => item.width)),
46
+ height: horizontal
47
+ ? Math.max(...children.map((item) => item.height))
48
+ : children.reduce((sum, item) => sum + item.height, 0) + gap * (children.length - 1)
49
+ };
50
+ minimums.set(node.id, value);
51
+ return value;
52
+ };
53
+ const walk = (node, rect) => {
54
+ if (node.type === 'pane') {
55
+ result.panes.push({ ...rect, pane: node });
56
+ return;
57
+ }
58
+ const horizontal = node.axis === 'horizontal';
59
+ const axisLength = horizontal ? rect.width : rect.height;
60
+ const spacing = Math.min(gap, axisLength / (node.children.length - 1));
61
+ const available = Math.max(0, axisLength - spacing * (node.children.length - 1));
62
+ const limits = node.children.map((child) => minimums.get(child.id)[horizontal ? 'width' : 'height']);
63
+ const lengths = distribute(available, node.sizes, limits);
64
+ let offset = 0;
65
+ node.children.forEach((child, i) => {
66
+ const end = Math.round(offset + lengths[i]);
67
+ const start = Math.round(offset);
68
+ walk(child, horizontal ? { ...rect, x: rect.x + start, width: end - start } : { ...rect, y: rect.y + start, height: end - start });
69
+ if (i < node.children.length - 1) {
70
+ result.dividers.push({
71
+ ...rect,
72
+ ...(horizontal ? { x: rect.x + end, width: spacing } : { y: rect.y + end, height: spacing }),
73
+ splitId: node.id,
74
+ index: i,
75
+ axis: node.axis,
76
+ lengths,
77
+ minimums: limits
78
+ });
79
+ }
80
+ offset += lengths[i] + spacing;
81
+ });
82
+ };
83
+ const root = layout.maximized ? (findSplitNode(layout.root, layout.maximized) ?? layout.root) : layout.root;
84
+ measure(root);
85
+ walk(root, { x: 0, y: 0, width: Math.max(0, width), height: Math.max(0, height) });
86
+ return result;
87
+ }
88
+ export function resizeSplit(divider, delta, symmetric = false, snapDistance = 0, minimumShare = 0) {
89
+ const lengths = [...divider.lengths];
90
+ const { index } = divider;
91
+ const total = lengths.reduce((sum, size) => sum + size, 0);
92
+ if (total <= 0 || lengths.some((size) => size <= 0)) {
93
+ return lengths.map(() => 1 / lengths.length);
94
+ }
95
+ const mirror = lengths.length - divider.index - 2;
96
+ const affected = new Set([index, index + 1, ...(symmetric ? [mirror, mirror + 1] : [])]);
97
+ const span = [...affected].reduce((sum, i) => sum + lengths[i], 0);
98
+ const share = Number.isFinite(minimumShare) ? Math.max(0, Math.min(1 / affected.size, minimumShare)) : 0;
99
+ const min = divider.minimums.map((size, i) => Math.min(lengths[i], Math.max(size, affected.has(i) ? span * share : 0)));
100
+ if (symmetric && mirror !== index) {
101
+ const changes = lengths.map(() => 0);
102
+ changes[index]++;
103
+ changes[index + 1]--;
104
+ changes[mirror]--;
105
+ changes[mirror + 1]++;
106
+ let lower = -Infinity;
107
+ let upper = Infinity;
108
+ changes.forEach((coefficient, i) => {
109
+ if (coefficient > 0) {
110
+ lower = Math.max(lower, (min[i] - lengths[i]) / coefficient);
111
+ }
112
+ if (coefficient < 0) {
113
+ upper = Math.min(upper, (lengths[i] - min[i]) / -coefficient);
114
+ }
115
+ });
116
+ const change = Math.max(lower, Math.min(delta, upper));
117
+ return lengths.map((size, i) => Math.max(Number.EPSILON, (size + changes[i] * change) / total));
118
+ }
119
+ const change = Math.max(min[index] - lengths[index], Math.min(delta, lengths[index + 1] - min[index + 1]));
120
+ const pair = lengths[index] + lengths[index + 1];
121
+ const middle = pair / 2;
122
+ const next = lengths[index] + change;
123
+ // A snap must not cross either pane's minimum, including minimums inherited from nested splits.
124
+ lengths[index] = Math.abs(next - middle) <= snapDistance && middle >= min[index] && middle >= min[index + 1] ? middle : next;
125
+ lengths[index + 1] = pair - lengths[index];
126
+ return lengths.map((size) => Math.max(Number.EPSILON, size / total));
127
+ }
128
+ export function paneRadius(rect, width, height, edges, radius) {
129
+ const left = rect.x === 0 && edges.left;
130
+ const top = rect.y === 0 && edges.top;
131
+ const right = rect.x + rect.width >= width && edges.right;
132
+ const bottom = rect.y + rect.height >= height && edges.bottom;
133
+ return [top || left, top || right, bottom || right, bottom || left].map((flush) => `${flush ? 0 : radius}px`).join(' ');
134
+ }
135
+ export function splitSideAt(rect, x, y) {
136
+ const horizontal = (x - rect.x) / rect.width;
137
+ const vertical = (y - rect.y) / rect.height;
138
+ const nearest = Math.min(horizontal, 1 - horizontal, vertical, 1 - vertical);
139
+ if (nearest > 0.25) {
140
+ return undefined;
141
+ }
142
+ if (nearest === horizontal) {
143
+ return 'left';
144
+ }
145
+ if (nearest === 1 - horizontal) {
146
+ return 'right';
147
+ }
148
+ return nearest === vertical ? 'top' : 'bottom';
149
+ }
150
+ export function tabGapAt(rects, x) {
151
+ const index = rects.findIndex((rect) => x < (rect.left + rect.right) / 2);
152
+ return index < 0 ? rects.length : index;
153
+ }
@@ -0,0 +1,83 @@
1
+ export type SplitAxis = 'horizontal' | 'vertical';
2
+ export type SplitSide = 'left' | 'right' | 'top' | 'bottom';
3
+ export interface SplitPane {
4
+ type: 'pane';
5
+ id: string;
6
+ views: string[];
7
+ active: string | null;
8
+ }
9
+ export interface SplitBranch {
10
+ type: 'split';
11
+ id: string;
12
+ axis: SplitAxis;
13
+ children: SplitNode[];
14
+ sizes: number[];
15
+ }
16
+ export type SplitNode = SplitPane | SplitBranch;
17
+ export interface SplitLayout {
18
+ root: SplitNode;
19
+ focused: string;
20
+ maximized: string | null;
21
+ }
22
+ export type SplitCommand = {
23
+ type: 'activate';
24
+ paneId: string;
25
+ viewId: string;
26
+ } | {
27
+ type: 'focus';
28
+ paneId: string;
29
+ } | {
30
+ type: 'maximize';
31
+ paneId: string | null;
32
+ } | {
33
+ type: 'close';
34
+ viewId: string;
35
+ } | {
36
+ type: 'closePane';
37
+ paneId: string;
38
+ } | {
39
+ type: 'insert';
40
+ paneId: string;
41
+ viewId: string;
42
+ index?: number;
43
+ } | {
44
+ type: 'split';
45
+ paneId: string;
46
+ side: SplitSide;
47
+ newPane: SplitPane;
48
+ splitId: string;
49
+ } | {
50
+ type: 'move';
51
+ sourceId: string;
52
+ targetId: string;
53
+ viewId?: string;
54
+ side?: SplitSide;
55
+ index?: number;
56
+ newPaneId?: string;
57
+ splitId?: string;
58
+ } | {
59
+ type: 'swap';
60
+ sourceId: string;
61
+ targetId: string;
62
+ viewId?: string;
63
+ } | {
64
+ type: 'resize';
65
+ splitId: string;
66
+ sizes: number[];
67
+ } | {
68
+ type: 'equalize';
69
+ splitId: string;
70
+ index?: number;
71
+ } | {
72
+ type: 'equalizeAxis';
73
+ axis: SplitAxis;
74
+ length?: number;
75
+ gap?: number;
76
+ };
77
+ export declare function createSplitLayout(views?: string[], paneId?: string): SplitLayout;
78
+ export declare function splitPanes(node: SplitNode): SplitPane[];
79
+ export declare function findSplitNode(node: SplitNode, id: string): SplitNode | undefined;
80
+ /** Repairs persisted data at the application boundary; view payloads never enter the layout. */
81
+ export declare function normalizeSplitLayout(value: unknown): SplitLayout;
82
+ /** Invalid or stale commands are no-ops. IDs for new nodes come from the caller. */
83
+ export declare function updateSplitLayout(layout: SplitLayout, command: SplitCommand): SplitLayout;
@@ -0,0 +1,360 @@
1
+ export function createSplitLayout(views = [], paneId = 'main') {
2
+ const unique = [...new Set(views)];
3
+ return { root: { type: 'pane', id: paneId, views: unique, active: unique[0] ?? null }, focused: paneId, maximized: null };
4
+ }
5
+ export function splitPanes(node) {
6
+ return node.type === 'pane' ? [node] : node.children.flatMap(splitPanes);
7
+ }
8
+ export function findSplitNode(node, id) {
9
+ if (node.id === id) {
10
+ return node;
11
+ }
12
+ return node.type === 'split' ? node.children.map((child) => findSplitNode(child, id)).find(Boolean) : undefined;
13
+ }
14
+ function shares(values, length) {
15
+ const safe = Array.from({ length }, (_, i) => (Number.isFinite(values[i]) && values[i] > 0 ? values[i] : 1));
16
+ const largest = Math.max(...safe, 1);
17
+ const total = safe.reduce((sum, value) => sum + value / largest, 0);
18
+ return safe.map((value) => value / largest / total);
19
+ }
20
+ function flattenSplits(node) {
21
+ if (node.type === 'pane') {
22
+ return node;
23
+ }
24
+ const children = [];
25
+ const sizes = [];
26
+ node.children.forEach((child, i) => {
27
+ const next = flattenSplits(child);
28
+ // A divider balances neighboring tracks, not a pane against a hidden group on the same axis.
29
+ if (next.type === 'split' && next.axis === node.axis) {
30
+ children.push(...next.children);
31
+ sizes.push(...next.sizes.map((size) => size * node.sizes[i]));
32
+ }
33
+ else {
34
+ children.push(next);
35
+ sizes.push(node.sizes[i]);
36
+ }
37
+ });
38
+ if (children.length === node.children.length && children.every((child, i) => child === node.children[i])) {
39
+ return node;
40
+ }
41
+ return { ...node, children, sizes: shares(sizes, children.length) };
42
+ }
43
+ /** Repairs persisted data at the application boundary; view payloads never enter the layout. */
44
+ export function normalizeSplitLayout(value) {
45
+ const ids = new Set();
46
+ const views = new Set();
47
+ const seen = new Set();
48
+ let serial = 0;
49
+ const nextId = (candidate) => {
50
+ let id = typeof candidate === 'string' && candidate.length > 0 ? candidate : `pane-${serial++}`;
51
+ while (ids.has(id)) {
52
+ id = `pane-${serial++}`;
53
+ }
54
+ ids.add(id);
55
+ return id;
56
+ };
57
+ const read = (input, depth) => {
58
+ if (input === null || typeof input !== 'object' || seen.has(input) || depth > 64) {
59
+ return null;
60
+ }
61
+ seen.add(input);
62
+ const row = input;
63
+ if (row.type === 'pane') {
64
+ const items = Array.isArray(row.views)
65
+ ? row.views.filter((view) => {
66
+ if (typeof view !== 'string' || view.length === 0 || views.has(view)) {
67
+ return false;
68
+ }
69
+ views.add(view);
70
+ return true;
71
+ })
72
+ : [];
73
+ return {
74
+ type: 'pane',
75
+ id: nextId(row.id),
76
+ views: items,
77
+ active: typeof row.active === 'string' && items.includes(row.active) ? row.active : (items[0] ?? null)
78
+ };
79
+ }
80
+ if (row.type !== 'split' || !Array.isArray(row.children)) {
81
+ return null;
82
+ }
83
+ const children = [];
84
+ const sizes = [];
85
+ row.children.forEach((child, i) => {
86
+ const result = read(child, depth + 1);
87
+ if (result) {
88
+ children.push(result);
89
+ sizes.push(Array.isArray(row.sizes) ? Number(row.sizes[i]) : 1);
90
+ }
91
+ });
92
+ if (children.length < 2) {
93
+ return children[0] ?? null;
94
+ }
95
+ return {
96
+ type: 'split',
97
+ id: nextId(row.id),
98
+ axis: row.axis === 'vertical' ? 'vertical' : 'horizontal',
99
+ children,
100
+ sizes: shares(sizes, children.length)
101
+ };
102
+ };
103
+ const input = value !== null && typeof value === 'object' ? value : {};
104
+ const root = flattenSplits(read(input.root, 0) ?? createSplitLayout().root);
105
+ const panes = splitPanes(root);
106
+ return {
107
+ root,
108
+ focused: panes.find((pane) => pane.id === input.focused)?.id ?? panes[0].id,
109
+ maximized: panes.find((pane) => pane.id === input.maximized)?.id ?? null
110
+ };
111
+ }
112
+ function replace(node, id, transform) {
113
+ if (node.id === id) {
114
+ return transform(node);
115
+ }
116
+ if (node.type === 'pane') {
117
+ return node;
118
+ }
119
+ const children = [];
120
+ const sizes = [];
121
+ node.children.forEach((child, i) => {
122
+ const next = replace(child, id, transform);
123
+ if (next) {
124
+ children.push(next);
125
+ sizes.push(node.sizes[i]);
126
+ }
127
+ });
128
+ if (children.every((child, i) => child === node.children[i]) && children.length === node.children.length) {
129
+ return node;
130
+ }
131
+ if (children.length < 2) {
132
+ return children[0] ?? null;
133
+ }
134
+ return { ...node, children, sizes: shares(sizes, children.length) };
135
+ }
136
+ function finish(layout, root, focus = layout.focused) {
137
+ const next = flattenSplits(root ?? createSplitLayout([], layout.focused).root);
138
+ const panes = splitPanes(next);
139
+ return {
140
+ root: next,
141
+ focused: panes.some((pane) => pane.id === focus) ? focus : panes[0].id,
142
+ maximized: panes.some((pane) => pane.id === layout.maximized) ? layout.maximized : null
143
+ };
144
+ }
145
+ function without(pane, views) {
146
+ const remaining = pane.views.filter((view) => !views.includes(view));
147
+ const position = Math.max(0, pane.views.indexOf(pane.active ?? ''));
148
+ return {
149
+ ...pane,
150
+ views: remaining,
151
+ active: remaining.includes(pane.active ?? '') ? pane.active : (remaining[Math.min(position, remaining.length - 1)] ?? null)
152
+ };
153
+ }
154
+ function beside(target, pane, side, id) {
155
+ return {
156
+ type: 'split',
157
+ id,
158
+ axis: side === 'left' || side === 'right' ? 'horizontal' : 'vertical',
159
+ children: side === 'left' || side === 'top' ? [pane, target] : [target, pane],
160
+ sizes: [0.5, 0.5]
161
+ };
162
+ }
163
+ function equalizeAxis(root, axis, length, gap) {
164
+ const tracks = new Map();
165
+ const measure = (node) => {
166
+ const children = node.type === 'split' ? node.children.map(measure) : [];
167
+ const count = node.type === 'pane' ? 1 : node.axis === axis ? children.reduce((sum, value) => sum + value, 0) : Math.max(...children);
168
+ tracks.set(node.id, count);
169
+ return count;
170
+ };
171
+ measure(root);
172
+ const distribute = (node, extent) => {
173
+ if (node.type === 'pane') {
174
+ return node;
175
+ }
176
+ if (node.axis !== axis) {
177
+ return { ...node, children: node.children.map((child) => distribute(child, extent)) };
178
+ }
179
+ const count = tracks.get(node.id);
180
+ const spacing = Math.min(gap, extent / (count - 1));
181
+ const unit = Math.max(0, extent - spacing * (count - 1)) / count;
182
+ // Spanning panels also occupy the gaps between their tracks.
183
+ const lengths = node.children.map((child) => {
184
+ const span = tracks.get(child.id);
185
+ return Math.max(Number.EPSILON, unit * span + spacing * (span - 1));
186
+ });
187
+ return { ...node, sizes: shares(lengths, lengths.length), children: node.children.map((child, i) => distribute(child, lengths[i])) };
188
+ };
189
+ return distribute(root, length);
190
+ }
191
+ /** Invalid or stale commands are no-ops. IDs for new nodes come from the caller. */
192
+ export function updateSplitLayout(layout, command) {
193
+ const panes = splitPanes(layout.root);
194
+ const pane = 'paneId' in command ? panes.find((item) => item.id === command.paneId) : undefined;
195
+ switch (command.type) {
196
+ case 'focus':
197
+ return pane && layout.focused !== pane.id ? { ...layout, focused: pane.id } : layout;
198
+ case 'maximize':
199
+ return command.paneId === null || pane ? { ...layout, maximized: command.paneId, focused: pane?.id ?? layout.focused } : layout;
200
+ case 'activate':
201
+ return pane?.views.includes(command.viewId)
202
+ ? finish(layout, replace(layout.root, pane.id, () => ({ ...pane, active: command.viewId })), pane.id)
203
+ : layout;
204
+ case 'close': {
205
+ const source = panes.find((item) => item.views.includes(command.viewId));
206
+ if (!source) {
207
+ return layout;
208
+ }
209
+ const next = without(source, [command.viewId]);
210
+ return finish(layout, replace(layout.root, source.id, () => (next.views.length || panes.length === 1 ? next : null)));
211
+ }
212
+ case 'closePane':
213
+ return pane
214
+ ? finish(layout, replace(layout.root, pane.id, () => null))
215
+ : layout;
216
+ case 'insert': {
217
+ if (!pane) {
218
+ return layout;
219
+ }
220
+ const source = panes.find((item) => item.views.includes(command.viewId));
221
+ if (source) {
222
+ return updateSplitLayout(layout, { type: 'move', sourceId: source.id, targetId: pane.id, viewId: command.viewId, index: command.index });
223
+ }
224
+ const views = [...pane.views];
225
+ views.splice(Math.max(0, Math.min(views.length, command.index ?? views.length)), 0, command.viewId);
226
+ return finish(layout, replace(layout.root, pane.id, () => ({ ...pane, views, active: command.viewId })), pane.id);
227
+ }
228
+ case 'split': {
229
+ if (!pane ||
230
+ findSplitNode(layout.root, command.newPane.id) ||
231
+ findSplitNode(layout.root, command.splitId) ||
232
+ command.newPane.id === command.splitId) {
233
+ return layout;
234
+ }
235
+ const allViews = new Set(panes.flatMap((item) => item.views));
236
+ if (command.newPane.views.some((view) => allViews.has(view)) || new Set(command.newPane.views).size !== command.newPane.views.length) {
237
+ return layout;
238
+ }
239
+ const added = {
240
+ ...command.newPane,
241
+ active: command.newPane.views.includes(command.newPane.active ?? '') ? command.newPane.active : (command.newPane.views[0] ?? null)
242
+ };
243
+ return finish({ ...layout, maximized: null }, replace(layout.root, pane.id, () => beside(pane, added, command.side, command.splitId)), added.id);
244
+ }
245
+ case 'swap': {
246
+ const source = panes.find((item) => item.id === command.sourceId);
247
+ const target = panes.find((item) => item.id === command.targetId);
248
+ if (!source ||
249
+ !target ||
250
+ source === target ||
251
+ source.views.length === 0 ||
252
+ (command.viewId !== undefined && !source.views.includes(command.viewId))) {
253
+ return layout;
254
+ }
255
+ if (command.viewId === undefined || source.views.length === 1) {
256
+ const exchange = (node) => {
257
+ if (node.id === source.id) {
258
+ return target;
259
+ }
260
+ if (node.id === target.id) {
261
+ return source;
262
+ }
263
+ return node.type === 'pane' ? node : { ...node, children: node.children.map(exchange) };
264
+ };
265
+ return finish({ ...layout, maximized: null }, exchange(layout.root), source.id);
266
+ }
267
+ const moving = command.viewId;
268
+ const displaced = target.active;
269
+ const sourceNext = displaced === null
270
+ ? without(source, [moving])
271
+ : {
272
+ ...source,
273
+ views: source.views.map((view) => (view === moving ? displaced : view)),
274
+ active: source.active === moving ? displaced : source.active
275
+ };
276
+ const targetNext = {
277
+ ...target,
278
+ views: displaced === null ? [moving] : target.views.map((view) => (view === displaced ? moving : view)),
279
+ active: moving
280
+ };
281
+ const root = replace(layout.root, source.id, () => sourceNext);
282
+ return finish({ ...layout, maximized: null }, replace(root, target.id, () => targetNext), target.id);
283
+ }
284
+ case 'move': {
285
+ const source = panes.find((item) => item.id === command.sourceId);
286
+ const target = panes.find((item) => item.id === command.targetId);
287
+ if (!source || !target || (command.viewId !== undefined && !source.views.includes(command.viewId))) {
288
+ return layout;
289
+ }
290
+ const moving = command.viewId === undefined ? source.views : [command.viewId];
291
+ if (!moving.length || (source === target && (command.viewId === undefined || (command.side !== undefined && source.views.length === 1)))) {
292
+ return layout;
293
+ }
294
+ let added;
295
+ if (command.side) {
296
+ const id = command.viewId === undefined ? source.id : command.newPaneId;
297
+ if (!id ||
298
+ !command.splitId ||
299
+ id === command.splitId ||
300
+ findSplitNode(layout.root, command.splitId) ||
301
+ (findSplitNode(layout.root, id) && !(id === source.id && command.viewId === undefined))) {
302
+ return layout;
303
+ }
304
+ added = { type: 'pane', id, views: moving, active: command.viewId ?? source.active };
305
+ }
306
+ const remaining = without(source, moving);
307
+ const root = replace(layout.root, source.id, () => (remaining.views.length || source === target ? remaining : null));
308
+ if (!root) {
309
+ return layout;
310
+ }
311
+ const next = replace(root, target.id, (node) => {
312
+ if (node.type !== 'pane') {
313
+ return node;
314
+ }
315
+ if (added && command.side && command.splitId) {
316
+ return beside(node, added, command.side, command.splitId);
317
+ }
318
+ const gap = Math.max(0, Math.min(target.views.length, command.index ?? target.views.length));
319
+ const lifted = source === target ? target.views.slice(0, gap).filter((view) => moving.includes(view)).length : 0;
320
+ const views = [...node.views];
321
+ views.splice(gap - lifted, 0, ...moving);
322
+ return { ...node, views, active: command.viewId ?? source.active ?? views[0] ?? null };
323
+ });
324
+ return finish({ ...layout, maximized: null }, next, added?.id ?? target.id);
325
+ }
326
+ case 'equalizeAxis': {
327
+ const length = command.length ?? 1;
328
+ const gap = command.gap ?? 0;
329
+ if (layout.root.type === 'pane' || !Number.isFinite(length) || length <= 0 || !Number.isFinite(gap) || gap < 0) {
330
+ return layout;
331
+ }
332
+ return finish(layout, equalizeAxis(layout.root, command.axis, length, gap));
333
+ }
334
+ case 'resize':
335
+ case 'equalize': {
336
+ const node = findSplitNode(layout.root, command.splitId);
337
+ if (!node || node.type !== 'split') {
338
+ return layout;
339
+ }
340
+ let sizes = [...node.sizes];
341
+ if (command.type === 'resize') {
342
+ if (command.sizes.length !== node.children.length || command.sizes.some((size) => !Number.isFinite(size) || size <= 0)) {
343
+ return layout;
344
+ }
345
+ sizes = shares(command.sizes, node.children.length);
346
+ }
347
+ else if (command.index === undefined) {
348
+ sizes = node.children.map(() => 1 / node.children.length);
349
+ }
350
+ else {
351
+ const index = command.index;
352
+ if (index < 0 || index >= sizes.length - 1) {
353
+ return layout;
354
+ }
355
+ sizes[index] = sizes[index + 1] = (sizes[index] + sizes[index + 1]) / 2;
356
+ }
357
+ return finish(layout, replace(layout.root, node.id, () => ({ ...node, sizes })));
358
+ }
359
+ }
360
+ }