@dashfoo/core 0.4.0 → 0.5.1

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,2 +1,1150 @@
1
- import{z as e}from"zod";import{assign as t,emit as n,setup as r}from"xstate";const i=e.lazy(()=>e.union([e.string(),e.number(),e.boolean(),e.null(),e.array(i),e.record(e.string(),i)])),a=e.enum([`top`,`bottom`,`left`,`right`]),o=e.enum([`px`,`%`,`em`,`rem`,`vh`,`vw`]),s=e.enum([`row`,`column`]),c=e.object({unit:o,value:e.number()}),l=e.object({divisions:e.union([e.number().positive(),e.literal(`panels`)]).optional(),step:e.number().min(0).optional(),threshold:e.number().positive().optional()}),u=e.object({component:e.string(),config:i.optional(),enableClose:e.boolean().optional(),enableDrag:e.boolean().optional(),enableRename:e.boolean().optional(),id:e.string(),name:e.string(),type:e.literal(`tab`)}),d=e.object({children:e.array(u),enableClose:e.boolean().optional(),enableMaximize:e.boolean().optional(),id:e.string(),max:c.optional(),min:c.optional(),name:e.string().optional(),selected:e.number().int(),type:e.literal(`tabset`),weight:e.number().optional()}),f=e.lazy(()=>e.object({children:e.array(e.union([f,d])),id:e.string(),max:c.optional(),min:c.optional(),orientation:s,snap:l.optional(),type:e.literal(`row`),weight:e.number().optional()})),p=e.object({enableSplitDock:e.boolean().optional(),enableSplitResize:e.boolean().optional(),snap:l.optional(),splitterSize:e.number().optional(),tabEnableClose:e.boolean().optional(),tabEnableDrag:e.boolean().optional(),tabEnableRename:e.boolean().optional(),tabLocation:e.enum([`top`,`bottom`]).optional(),tabSetEnableMaximize:e.boolean().optional(),tabSetEnableTabStrip:e.boolean().optional(),tabSetMinSize:e.number().optional()}),m=e.object({activeTabsetId:e.string().optional(),global:p,layout:f,maximizedTabsetId:e.string().optional(),version:e.literal(1)}),h=u.pick({config:!0,enableClose:!0,enableDrag:!0,enableRename:!0,name:!0}).partial(),g=d.pick({enableClose:!0,enableMaximize:!0,max:!0,min:!0,selected:!0,weight:!0}).partial(),ee=e.object({max:c.optional(),min:c.optional(),orientation:s,snap:l.optional(),weight:e.number()}).partial(),_=e.union([h,g,ee]),v=e.enum([`center`,`split-bottom`,`split-left`,`split-right`,`split-top`]),y=e.discriminatedUnion(`type`,[e.object({index:e.number().int().optional(),location:v,tab:u,targetId:e.string(),type:e.literal(`addNode`)}),e.object({index:e.number().int().optional(),location:v,sourceId:e.string(),targetId:e.string(),type:e.literal(`moveNode`)}),e.object({index:e.number().int().optional(),location:v,sourceId:e.string(),targetId:e.string(),type:e.literal(`moveTabset`)}),e.object({index:e.number().int(),tabsetId:e.string(),type:e.literal(`selectTab`)}),e.object({tabsetId:e.string(),type:e.literal(`setActiveTabset`)}),e.object({tabsetId:e.string().nullable(),type:e.literal(`setMaximizedTabset`)}),e.object({name:e.string(),tabId:e.string(),type:e.literal(`renameTab`)}),e.object({tabId:e.string(),type:e.literal(`deleteTab`)}),e.object({tabsetId:e.string(),type:e.literal(`deleteTabset`)}),e.object({rowId:e.string(),type:e.literal(`adjustSplit`),weights:e.array(e.number())}),e.object({attrs:_,nodeId:e.string(),type:e.literal(`updateNodeAttributes`)}),e.object({attrs:p.partial(),type:e.literal(`updateGlobalAttributes`)})]),b=(e=`node`)=>`${e}-${crypto.randomUUID()}`,te=()=>b(`tab`),x=(e,t)=>{for(let n of e.children)n.type===`tabset`?t.push(n):x(n,t)},S=e=>{let t=[];return x(e.layout,t),t},ne=e=>S(e)[0],C=(e,t)=>S(e).find(e=>e.id===t),re=(e,t)=>{let n=e.children.findIndex(e=>e.id===t);if(n===-1)return;let r=e.children[n];if(r)return{container:e,index:n,tab:r}},w=(e,t)=>{for(let n of S(e)){let e=re(n,t);if(e)return e}},T=(e,t)=>{if(e.id===t)return e;for(let n of e.children)if(n.type===`row`){let e=T(n,t);if(e)return e}},E=(e,t)=>{if(e.id===t)return e;for(let n of e.children){if(n.type===`row`){let e=E(n,t);if(e)return e;continue}if(n.id===t)return n;let e=n.children.find(e=>e.id===t);if(e)return e}},D=(e,t)=>E(e.layout,t),O=(e,t)=>{let n=e.children.findIndex(e=>e.type===`tabset`&&e.id===t);if(n!==-1)return{index:n,parent:e};for(let n of e.children)if(n.type===`row`){let e=O(n,t);if(e)return e}},k=(e,t)=>{t.push(e.id);for(let n of e.children)if(n.type===`row`)k(n,t);else{t.push(n.id);for(let e of n.children)t.push(e.id)}},A=e=>{let t=[];k(e.layout,t);let n=new Set,r=new Set;for(let e of t)n.has(e)&&r.add(e),n.add(e);return[...r]},j=e=>typeof e==`number`?{unit:`px`,value:e}:e,M=(e,t,n={})=>({component:e,name:t,type:`tab`,...n,id:n.id??e}),ie=(e,t={})=>{let{max:n,min:r,...i}=t;return{children:e,type:`tabset`,...i,...n===void 0?{}:{max:j(n)},...r===void 0?{}:{min:j(r)},id:t.id??b(`tabset`),selected:t.selected??0}},ae=(e,t={})=>{let{max:n,min:r,...i}=t;return{children:e,type:`row`,...i,...n===void 0?{}:{max:j(n)},...r===void 0?{}:{min:j(r)},id:t.id??b(`row`),orientation:t.orientation??`row`}},oe=(e,t={})=>{let n={layout:e,version:1,...t,global:t.global??{}},r=A(n);return r.length>0&&console.warn(`[dashfoo] builder produced duplicate node ids: ${r.join(`, `)} — pass explicit ids when reusing a component`),n},N=.22,se=(e,t)=>{let n=(e.x-t.x)/t.width,r=(e.y-t.y)/t.height;return{bottom:1-r,left:n,right:1-n,top:r}},ce=e=>{let t=Math.min(e.left,e.right,e.top,e.bottom);return t===e.left?`left`:t===e.right?`right`:t===e.top?`top`:`bottom`},le=(e,t,n)=>{let r=n?.bandFraction??N;if(t.width<=0||t.height<=0)return{kind:`tab`};let i=se(e,t);return Math.min(i.left,i.right,i.top,i.bottom)>r?{kind:`tab`}:{edge:ce(i),kind:`split`}},ue=(e,t)=>{let{height:n,width:r,x:i,y:a}=e,o={x:i,y:a},s={x:i+r,y:a},c={x:i+r,y:a+n},l={x:i,y:a+n};if(r<=0||n<=0)return[{location:`center`,points:[o,s,c,l]}];let u=t?.bandFraction??N,d=u*r,f=u*n,p={x:i+d,y:a+f},m={x:i+r-d,y:a+f},h={x:i+r-d,y:a+n-f},g={x:i+d,y:a+n-f};return[{location:`center`,points:[p,m,h,g]},{location:`split-left`,points:[o,p,g,l]},{location:`split-right`,points:[s,c,h,m]},{location:`split-top`,points:[o,s,m,p]},{location:`split-bottom`,points:[l,g,h,c]}]},de=(e,t)=>{let n=e.width/2,r=e.height/2;switch(t){case`split-bottom`:return{height:r,width:e.width,x:e.x,y:e.y+r};case`split-left`:return{height:e.height,width:n,x:e.x,y:e.y};case`split-right`:return{height:e.height,width:n,x:e.x+n,y:e.y};case`split-top`:return{height:r,width:e.width,x:e.x,y:e.y};default:return e}},fe=4,P=1e6,F=(e,t)=>{if(!(t<=0))for(let n=t;n<100;n+=t){let t=Math.round(n*P)/P;t<100&&e.add(t)}},I=(e,t)=>{let n=new Set;if(e.step!==void 0&&e.step>0&&F(n,e.step),e.divisions!==void 0){let r=e.divisions===`panels`?t:e.divisions;r>=2&&F(n,100/r)}return[...n].toSorted((e,t)=>e-t)},L=e=>e!==null&&((e.step??0)>0||e.divisions===`panels`||typeof e.divisions==`number`&&e.divisions>=2),R=(e,t)=>t.reduce((t,n)=>t===void 0||Math.abs(n-e)<Math.abs(t-e)?n:t,void 0),z=(e,t,n)=>{let r=n.threshold??4,i=e[t],a=e[t+1];if(t<0||i===void 0||a===void 0)return{sizes:e,snapped:!1};let o=I(n,e.length);if(o.length===0)return{sizes:e,snapped:!1};let s=0;for(let n=0;n<=t;n++)s+=e[n]??0;let c=R(s,o);if(c===void 0||Math.abs(c-s)>r)return{sizes:e,snapped:!1};let l=c-s;if(i+l<0||a-l<0)return{sizes:e,snapped:!1};let u=[...e];return u[t]=i+l,u[t+1]=a-l,{sizes:u,snapped:!0}},B=(e,t)=>{if(e===0)return 0;let n=Number.isFinite(t)?Math.trunc(t):0;return n<0?0:n>e-1?e-1:n},V=e=>({...e,selected:B(e.children.length,e.selected)}),H=e=>{let t=[];for(let n of e){if(n.type===`tabset`){n.children.length>0&&t.push(V(n));continue}let e=H(n.children);if(e.length!==0){if(e.length===1){let r=e[0];r!==void 0&&t.push(n.weight===void 0?r:{...r,weight:n.weight});continue}t.push({...n,children:e})}}return t},pe=e=>{let t=H(e.children),n=e.orientation,r=t[0];for(;t.length===1&&r?.type===`row`;)t=r.children,n=r.orientation,r=t[0];return{...e,children:t,orientation:n}},U=e=>{let t=pe(e.layout),n={...e,layout:t},r=new Set(S(n).map(e=>e.id)),i=S(n)[0]?.id,a=e.activeTabsetId!==void 0&&r.has(e.activeTabsetId)?e.activeTabsetId:i,o=e.maximizedTabsetId!==void 0&&r.has(e.maximizedTabsetId)?e.maximizedTabsetId:void 0;return{...n,activeTabsetId:a,maximizedTabsetId:o}},me=e=>{throw Error(`Unhandled action: ${JSON.stringify(e)}`)},W=(e,t)=>{let n=e.children.findIndex(e=>e.type===`tabset`&&e.id===t);if(n!==-1)return e.children.splice(n,1),!0;for(let n of e.children)if(n.type===`row`&&W(n,t))return!0;return!1},G=(e,t)=>{let n=e.children.findIndex(e=>e.type===`tabset`&&e.id===t);if(n!==-1){let[t]=e.children.splice(n,1);return t?.type===`tabset`?t:void 0}for(let n of e.children)if(n.type===`row`){let e=G(n,t);if(e)return e}},he=e=>e===`split-left`||e===`split-right`?`row`:`column`,ge=e=>e===`split-left`||e===`split-top`,K=(e,t,n,r)=>{let i=C(e,n),a=O(e.layout,n);if(!i||!a)return;let o=he(r),s=ge(r);if(a.parent.orientation===o){let e=i.weight??100;i.weight=e/2,t.weight=e/2,a.parent.children.splice(s?a.index:a.index+1,0,t)}else{let e=i.weight;i.weight=50,t.weight=50;let n={children:s?[t,i]:[i,t],id:b(`row`),orientation:o,type:`row`};e!==void 0&&(n.weight=e),a.parent.children.splice(a.index,1,n)}e.activeTabsetId=t.id},q=(e,t,n)=>{let{id:r,index:i,location:a}=n,o=C(e,r);if(o){if(a===`center`){let e=i??o.children.length;o.children.splice(e,0,t),o.selected=e;return}K(e,{children:[t],id:b(`tabset`),selected:0,type:`tabset`,weight:50},r,a)}},_e=(e,t)=>{switch(t.type){case`addNode`:q(e,t.tab,{id:t.targetId,index:t.index,location:t.location});return;case`adjustSplit`:{let n=T(e.layout,t.rowId);if(!n)return;for(let e=0;e<t.weights.length;e++){let r=n.children[e],i=t.weights[e];!r||i===void 0||(r.weight=i)}return}case`deleteTab`:{let n=w(e,t.tabId);n&&n.container.children.splice(n.index,1);return}case`deleteTabset`:W(e.layout,t.tabsetId);return;case`moveNode`:{let n=w(e,t.sourceId);if(!n)return;let[r]=n.container.children.splice(n.index,1);if(!r)return;q(e,r,{id:t.targetId,index:t.index,location:t.location});return}case`moveTabset`:{if(t.sourceId===t.targetId)return;let n=C(e,t.sourceId);if(!n)return;if(t.location===`center`){let r=C(e,t.targetId);if(r){let i=r.children.length,a=Math.min(Math.max(n.selected,0),n.children.length-1);r.children.push(...n.children),r.selected=i+a,W(e.layout,t.sourceId)}return}if(t.location.startsWith(`split-`)){let n=G(e.layout,t.sourceId);n&&K(e,n,t.targetId,t.location)}return}case`renameTab`:{let n=w(e,t.tabId);n&&(n.tab.name=t.name);return}case`selectTab`:{let n=C(e,t.tabsetId);n&&(n.selected=t.index);return}case`setActiveTabset`:C(e,t.tabsetId)&&(e.activeTabsetId=t.tabsetId);return;case`setMaximizedTabset`:e.maximizedTabsetId=t.tabsetId??void 0;return;case`updateGlobalAttributes`:Object.assign(e.global,t.attrs);return;case`updateNodeAttributes`:{let n=D(e,t.nodeId);n&&Object.assign(n,t.attrs);return}default:me(t)}},J=(e,t)=>{let n=structuredClone(e);return _e(n,t),U(n)},ve=100,Y=e=>({future:[],past:[],present:e}),ye=e=>e.past.length>0,be=e=>e.future.length>0,X=(e,t)=>{let n=J(e.present,t);return{future:[],past:[...e.past,e.present].slice(-100),present:n}},Z=e=>{let t=e.past.at(-1);return t===void 0?e:{future:[e.present,...e.future],past:e.past.slice(0,-1),present:t}},Q=e=>{let[t,...n]=e.future;return t===void 0?e:{future:n,past:[...e.past,e.present].slice(-100),present:t}},xe=r({types:{context:{},events:{},input:{}}}).createMachine({context:({input:e})=>({history:Y(e.model)}),id:`dashfoo`,initial:`ready`,states:{ready:{on:{DISPATCH:{actions:t({history:({context:e,event:t})=>X(e.history,t.action)})},REDO:{actions:t({history:({context:e})=>Q(e.history)})},SET_MODEL:{actions:t({history:({event:e})=>Y(e.model)})},UNDO:{actions:t({history:({context:e})=>Z(e.history)})}}}}}),Se=e=>{if(e.subject===null||e.intent===null)throw Error(`drop requires a subject and a drop intent`);return{intent:e.intent,subject:e.subject}},Ce=r({guards:{hasValidDrop:({context:e})=>e.subject!==null&&e.intent!==null},types:{context:{},emitted:{},events:{}}}).createMachine({context:{intent:null,subject:null},id:`dragDock`,initial:`idle`,states:{dragging:{on:{CANCEL:{target:`idle`},DROP:[{actions:n(({context:e})=>{let{intent:t,subject:n}=Se(e);return{action:n.kind===`external`?{index:t.index,location:t.location,tab:n.tab,targetId:t.targetId,type:`addNode`}:{index:t.index,location:t.location,sourceId:n.id,targetId:t.targetId,type:n.kind===`tabset`?`moveTabset`:`moveNode`},type:`COMMIT`}}),guard:`hasValidDrop`,target:`idle`},{target:`idle`}],OVER:{actions:t({intent:({event:e})=>e.intent})}}},idle:{entry:t({intent:null,subject:null}),on:{START:{actions:t({subject:({event:e})=>e.subject}),target:`dragging`}}}}}),$=e=>{let t=U(m.parse(e)),n=A(t);return n.length>0&&console.warn(`[dashfoo] duplicate node ids in the loaded layout: ${n.join(`, `)}`),t},we=e=>JSON.stringify(e),Te=e=>$(JSON.parse(e)),Ee=(e,t=`column`)=>{let n=[];for(let t of S(e))n.push({...t,weight:1});return U({...e,layout:{children:n,id:e.layout.id,orientation:t,type:`row`},maximizedTabsetId:void 0})};export{fe as DEFAULT_SNAP_THRESHOLD,ve as HISTORY_LIMIT,y as actionSchema,be as canRedo,ye as canUndo,S as collectTabsets,Y as createHistory,b as createNodeId,te as createTabId,xe as dashfooMachine,m as dashfooSchema,c as dimensionSchema,X as dispatch,v as dockLocationSchema,ue as dockZonePolygons,Ce as dragDockMachine,a as edgeSchema,D as findAttributedNode,A as findDuplicateIds,T as findRow,w as findTab,C as findTabset,O as findTabsetParent,Te as fromJSON,ne as getFirstTabset,p as globalAttributesSchema,i as jsonValueSchema,oe as model,_ as mutableNodeAttrsSchema,U as normalize,s as orientationSchema,$ as parseModel,Q as redo,J as reducer,le as resolveDockTarget,I as resolveSnapTargets,ae as row,f as rowNodeSchema,L as snapEnabled,l as snapSchema,z as snapSizes,Ee as stackModel,M as tab,u as tabNodeSchema,ie as tabset,d as tabsetNodeSchema,we as toJSON,Z as undo,o as unitSchema,de as zoneRect};
1
+ import { z } from "zod";
2
+ import { assign, emit, setup } from "xstate";
3
+
4
+ //#region src/model/schema.ts
5
+ const jsonValueSchema = z.lazy(() => z.union([
6
+ z.string(),
7
+ z.number(),
8
+ z.boolean(),
9
+ z.null(),
10
+ z.array(jsonValueSchema),
11
+ z.record(z.string(), jsonValueSchema)
12
+ ]));
13
+ const edgeSchema = z.enum([
14
+ "top",
15
+ "bottom",
16
+ "left",
17
+ "right"
18
+ ]);
19
+ const unitSchema = z.enum([
20
+ "px",
21
+ "%",
22
+ "em",
23
+ "rem",
24
+ "vh",
25
+ "vw"
26
+ ]);
27
+ const orientationSchema = z.enum(["row", "column"]);
28
+ const dimensionSchema = z.object({
29
+ unit: unitSchema,
30
+ value: z.number()
31
+ });
32
+ const snapSchema = z.object({
33
+ divisions: z.union([z.number().positive(), z.literal("panels")]).optional(),
34
+ step: z.number().min(0).optional(),
35
+ threshold: z.number().positive().optional()
36
+ });
37
+ const tabNodeSchema = z.object({
38
+ component: z.string(),
39
+ config: jsonValueSchema.optional(),
40
+ enableClose: z.boolean().optional(),
41
+ enableDrag: z.boolean().optional(),
42
+ enableRename: z.boolean().optional(),
43
+ id: z.string(),
44
+ name: z.string(),
45
+ type: z.literal("tab")
46
+ });
47
+ const tabsetNodeSchema = z.object({
48
+ children: z.array(tabNodeSchema),
49
+ enableClose: z.boolean().optional(),
50
+ enableMaximize: z.boolean().optional(),
51
+ id: z.string(),
52
+ max: dimensionSchema.optional(),
53
+ min: dimensionSchema.optional(),
54
+ name: z.string().optional(),
55
+ selected: z.number().int(),
56
+ type: z.literal("tabset"),
57
+ weight: z.number().optional()
58
+ });
59
+ const rowNodeSchema = z.lazy(() => z.object({
60
+ children: z.array(z.union([rowNodeSchema, tabsetNodeSchema])),
61
+ id: z.string(),
62
+ max: dimensionSchema.optional(),
63
+ min: dimensionSchema.optional(),
64
+ orientation: orientationSchema,
65
+ snap: snapSchema.optional(),
66
+ type: z.literal("row"),
67
+ weight: z.number().optional()
68
+ }));
69
+ const geometrySchema = z.object({
70
+ height: z.number(),
71
+ left: z.number(),
72
+ top: z.number(),
73
+ width: z.number()
74
+ });
75
+ const floatNodeSchema = z.object({
76
+ geometry: geometrySchema,
77
+ id: z.string(),
78
+ layout: rowNodeSchema,
79
+ minimized: z.boolean().optional(),
80
+ name: z.string().optional(),
81
+ type: z.literal("float")
82
+ });
83
+ const globalAttributesSchema = z.object({
84
+ enableSplitDock: z.boolean().optional(),
85
+ enableSplitResize: z.boolean().optional(),
86
+ snap: snapSchema.optional(),
87
+ splitterSize: z.number().optional(),
88
+ tabEnableClose: z.boolean().optional(),
89
+ tabEnableDrag: z.boolean().optional(),
90
+ tabEnableRename: z.boolean().optional(),
91
+ tabLocation: z.enum(["top", "bottom"]).optional(),
92
+ tabSetEnableMaximize: z.boolean().optional(),
93
+ tabSetEnableTabStrip: z.boolean().optional(),
94
+ tabSetMinSize: z.number().optional()
95
+ });
96
+ const dashfooSchema = z.object({
97
+ activeTabsetId: z.string().optional(),
98
+ global: globalAttributesSchema,
99
+ layout: rowNodeSchema,
100
+ maximizedTabsetId: z.string().optional(),
101
+ version: z.literal(1),
102
+ floats: z.array(floatNodeSchema).optional()
103
+ });
104
+
105
+ //#endregion
106
+ //#region src/state/actions.ts
107
+ const mutableTabAttrsSchema = tabNodeSchema.pick({
108
+ config: true,
109
+ enableClose: true,
110
+ enableDrag: true,
111
+ enableRename: true,
112
+ name: true
113
+ }).partial();
114
+ const mutableTabsetAttrsSchema = tabsetNodeSchema.pick({
115
+ enableClose: true,
116
+ enableMaximize: true,
117
+ max: true,
118
+ min: true,
119
+ selected: true,
120
+ weight: true
121
+ }).partial();
122
+ const mutableRowAttrsSchema = z.object({
123
+ max: dimensionSchema.optional(),
124
+ min: dimensionSchema.optional(),
125
+ orientation: orientationSchema,
126
+ snap: snapSchema.optional(),
127
+ weight: z.number()
128
+ }).partial();
129
+ const mutableNodeAttrsSchema = z.union([
130
+ mutableTabAttrsSchema,
131
+ mutableTabsetAttrsSchema,
132
+ mutableRowAttrsSchema
133
+ ]);
134
+ const dockLocationSchema = z.enum([
135
+ "center",
136
+ "split-bottom",
137
+ "split-left",
138
+ "split-right",
139
+ "split-top"
140
+ ]);
141
+ const actionSchema = z.discriminatedUnion("type", [
142
+ z.object({
143
+ index: z.number().int().optional(),
144
+ location: dockLocationSchema,
145
+ tab: tabNodeSchema,
146
+ targetId: z.string(),
147
+ type: z.literal("addNode")
148
+ }),
149
+ z.object({
150
+ index: z.number().int().optional(),
151
+ location: dockLocationSchema,
152
+ sourceId: z.string(),
153
+ targetId: z.string(),
154
+ type: z.literal("moveNode")
155
+ }),
156
+ z.object({
157
+ index: z.number().int().optional(),
158
+ location: dockLocationSchema,
159
+ sourceId: z.string(),
160
+ targetId: z.string(),
161
+ type: z.literal("moveTabset")
162
+ }),
163
+ z.object({
164
+ index: z.number().int(),
165
+ tabsetId: z.string(),
166
+ type: z.literal("selectTab")
167
+ }),
168
+ z.object({
169
+ tabsetId: z.string(),
170
+ type: z.literal("setActiveTabset")
171
+ }),
172
+ z.object({
173
+ tabsetId: z.string().nullable(),
174
+ type: z.literal("setMaximizedTabset")
175
+ }),
176
+ z.object({
177
+ name: z.string(),
178
+ tabId: z.string(),
179
+ type: z.literal("renameTab")
180
+ }),
181
+ z.object({
182
+ tabId: z.string(),
183
+ type: z.literal("deleteTab")
184
+ }),
185
+ z.object({
186
+ tabsetId: z.string(),
187
+ type: z.literal("deleteTabset")
188
+ }),
189
+ z.object({
190
+ rowId: z.string(),
191
+ type: z.literal("adjustSplit"),
192
+ weights: z.array(z.number())
193
+ }),
194
+ z.object({
195
+ attrs: mutableNodeAttrsSchema,
196
+ nodeId: z.string(),
197
+ type: z.literal("updateNodeAttributes")
198
+ }),
199
+ z.object({
200
+ attrs: globalAttributesSchema.partial(),
201
+ type: z.literal("updateGlobalAttributes")
202
+ }),
203
+ z.object({
204
+ floatId: z.string().optional(),
205
+ geometry: geometrySchema.optional(),
206
+ tabId: z.string(),
207
+ type: z.literal("floatTab")
208
+ }),
209
+ z.object({
210
+ floatId: z.string().optional(),
211
+ geometry: geometrySchema.optional(),
212
+ tabsetId: z.string(),
213
+ type: z.literal("floatTabset")
214
+ }),
215
+ z.object({
216
+ floatId: z.string(),
217
+ location: dockLocationSchema.optional(),
218
+ targetId: z.string().optional(),
219
+ type: z.literal("dockFloat")
220
+ }),
221
+ z.object({
222
+ floatId: z.string(),
223
+ geometry: geometrySchema,
224
+ type: z.literal("moveFloat")
225
+ }),
226
+ z.object({
227
+ floatId: z.string(),
228
+ minimized: z.boolean(),
229
+ type: z.literal("setFloatMinimized")
230
+ }),
231
+ z.object({
232
+ floatId: z.string(),
233
+ name: z.string(),
234
+ type: z.literal("renameFloat")
235
+ })
236
+ ]);
237
+
238
+ //#endregion
239
+ //#region src/model/ids.ts
240
+ const createNodeId = (prefix = "node") => `${prefix}-${crypto.randomUUID()}`;
241
+ const createTabId = () => createNodeId("tab");
242
+
243
+ //#endregion
244
+ //#region src/model/tree.ts
245
+ const collectRoots = (model) => [model.layout, ...(model.floats ?? []).map((float) => float.layout)];
246
+ const findFloat = (model, floatId) => (model.floats ?? []).find((float) => float.id === floatId);
247
+ const collectTabsetsInRow = (row, acc) => {
248
+ for (const child of row.children) if (child.type === "tabset") acc.push(child);
249
+ else collectTabsetsInRow(child, acc);
250
+ };
251
+ const collectTabsets = (model) => {
252
+ const acc = [];
253
+ for (const root of collectRoots(model)) collectTabsetsInRow(root, acc);
254
+ return acc;
255
+ };
256
+ const getFirstTabset = (model) => collectTabsets(model)[0];
257
+ const findTabset = (model, tabsetId) => collectTabsets(model).find((tabset) => tabset.id === tabsetId);
258
+ const findTabInContainer = (container, tabId) => {
259
+ const index = container.children.findIndex((tab) => tab.id === tabId);
260
+ if (index === -1) return;
261
+ const tab = container.children[index];
262
+ if (!tab) return;
263
+ return {
264
+ container,
265
+ index,
266
+ tab
267
+ };
268
+ };
269
+ const findTab = (model, tabId) => {
270
+ for (const tabset of collectTabsets(model)) {
271
+ const found = findTabInContainer(tabset, tabId);
272
+ if (found) return found;
273
+ }
274
+ };
275
+ 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
+ }
294
+ };
295
+ const findAttributedNode = (model, id) => {
296
+ for (const root of collectRoots(model)) {
297
+ const found = findAttributedNodeInRow(root, id);
298
+ if (found) return found;
299
+ }
300
+ };
301
+ const findRootContaining = (model, nodeId) => {
302
+ for (const root of collectRoots(model)) if (findAttributedNodeInRow(root, nodeId)) return root;
303
+ };
304
+ 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
+ }
314
+ };
315
+ 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
+ }
322
+ };
323
+ const findDuplicateIds = (model) => {
324
+ const ids = [];
325
+ for (const float of model.floats ?? []) ids.push(float.id);
326
+ for (const root of collectRoots(model)) collectIdsInRow(root, ids);
327
+ const seen = /* @__PURE__ */ new Set();
328
+ const duplicates = /* @__PURE__ */ new Set();
329
+ for (const id of ids) {
330
+ if (seen.has(id)) duplicates.add(id);
331
+ seen.add(id);
332
+ }
333
+ return [...duplicates];
334
+ };
335
+
336
+ //#endregion
337
+ //#region src/model/builders.ts
338
+ const toDimension = (value) => typeof value === "number" ? {
339
+ unit: "px",
340
+ value
341
+ } : value;
342
+ const tab = (component, name, options = {}) => ({
343
+ component,
344
+ name,
345
+ type: "tab",
346
+ ...options,
347
+ id: options.id ?? component
348
+ });
349
+ const tabset = (children, options = {}) => {
350
+ const { max, min, ...rest } = options;
351
+ return {
352
+ children,
353
+ type: "tabset",
354
+ ...rest,
355
+ ...max === void 0 ? {} : { max: toDimension(max) },
356
+ ...min === void 0 ? {} : { min: toDimension(min) },
357
+ id: options.id ?? createNodeId("tabset"),
358
+ selected: options.selected ?? 0
359
+ };
360
+ };
361
+ const row = (children, options = {}) => {
362
+ const { max, min, ...rest } = options;
363
+ return {
364
+ children,
365
+ type: "row",
366
+ ...rest,
367
+ ...max === void 0 ? {} : { max: toDimension(max) },
368
+ ...min === void 0 ? {} : { min: toDimension(min) },
369
+ id: options.id ?? createNodeId("row"),
370
+ orientation: options.orientation ?? "row"
371
+ };
372
+ };
373
+ const floatNode = (layout, geometry, options = {}) => ({
374
+ geometry,
375
+ layout,
376
+ type: "float",
377
+ ...options,
378
+ id: options.id ?? createNodeId("float")
379
+ });
380
+ const model = (layout, options = {}) => {
381
+ const built = {
382
+ layout,
383
+ version: 1,
384
+ ...options,
385
+ global: options.global ?? {}
386
+ };
387
+ 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`);
389
+ return built;
390
+ };
391
+
392
+ //#endregion
393
+ //#region src/geometry/geometry.ts
394
+ const DEFAULT_BAND_FRACTION = .22;
395
+ const edgeDistances = (pointer, rect) => {
396
+ const fx = (pointer.x - rect.x) / rect.width;
397
+ const fy = (pointer.y - rect.y) / rect.height;
398
+ return {
399
+ bottom: 1 - fy,
400
+ left: fx,
401
+ right: 1 - fx,
402
+ top: fy
403
+ };
404
+ };
405
+ const closestEdge = (d) => {
406
+ const min = Math.min(d.left, d.right, d.top, d.bottom);
407
+ if (min === d.left) return "left";
408
+ if (min === d.right) return "right";
409
+ if (min === d.top) return "top";
410
+ return "bottom";
411
+ };
412
+ const resolveDockTarget = (pointer, rect, opts) => {
413
+ const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
414
+ if (rect.width <= 0 || rect.height <= 0) return { kind: "tab" };
415
+ const distances = edgeDistances(pointer, rect);
416
+ if (Math.min(distances.left, distances.right, distances.top, distances.bottom) > band) return { kind: "tab" };
417
+ return {
418
+ edge: closestEdge(distances),
419
+ kind: "split"
420
+ };
421
+ };
422
+ const dockZonePolygons = (rect, opts) => {
423
+ const { height: h, width: w, x, y } = rect;
424
+ const nw = {
425
+ x,
426
+ y
427
+ };
428
+ const ne = {
429
+ x: x + w,
430
+ y
431
+ };
432
+ const se = {
433
+ x: x + w,
434
+ y: y + h
435
+ };
436
+ const sw = {
437
+ x,
438
+ y: y + h
439
+ };
440
+ if (w <= 0 || h <= 0) return [{
441
+ location: "center",
442
+ points: [
443
+ nw,
444
+ ne,
445
+ se,
446
+ sw
447
+ ]
448
+ }];
449
+ const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
450
+ const bx = band * w;
451
+ const by = band * h;
452
+ const innerNw = {
453
+ x: x + bx,
454
+ y: y + by
455
+ };
456
+ const innerNe = {
457
+ x: x + w - bx,
458
+ y: y + by
459
+ };
460
+ const innerSe = {
461
+ x: x + w - bx,
462
+ y: y + h - by
463
+ };
464
+ const innerSw = {
465
+ x: x + bx,
466
+ y: y + h - by
467
+ };
468
+ return [
469
+ {
470
+ location: "center",
471
+ points: [
472
+ innerNw,
473
+ innerNe,
474
+ innerSe,
475
+ innerSw
476
+ ]
477
+ },
478
+ {
479
+ location: "split-left",
480
+ points: [
481
+ nw,
482
+ innerNw,
483
+ innerSw,
484
+ sw
485
+ ]
486
+ },
487
+ {
488
+ location: "split-right",
489
+ points: [
490
+ ne,
491
+ se,
492
+ innerSe,
493
+ innerNe
494
+ ]
495
+ },
496
+ {
497
+ location: "split-top",
498
+ points: [
499
+ nw,
500
+ ne,
501
+ innerNe,
502
+ innerNw
503
+ ]
504
+ },
505
+ {
506
+ location: "split-bottom",
507
+ points: [
508
+ sw,
509
+ innerSw,
510
+ innerSe,
511
+ se
512
+ ]
513
+ }
514
+ ];
515
+ };
516
+ const zoneRect = (rect, location) => {
517
+ const halfW = rect.width / 2;
518
+ const halfH = rect.height / 2;
519
+ switch (location) {
520
+ case "split-bottom": return {
521
+ height: halfH,
522
+ width: rect.width,
523
+ x: rect.x,
524
+ y: rect.y + halfH
525
+ };
526
+ case "split-left": return {
527
+ height: rect.height,
528
+ width: halfW,
529
+ x: rect.x,
530
+ y: rect.y
531
+ };
532
+ case "split-right": return {
533
+ height: rect.height,
534
+ width: halfW,
535
+ x: rect.x + halfW,
536
+ y: rect.y
537
+ };
538
+ case "split-top": return {
539
+ height: halfH,
540
+ width: rect.width,
541
+ x: rect.x,
542
+ y: rect.y
543
+ };
544
+ default: return rect;
545
+ }
546
+ };
547
+
548
+ //#endregion
549
+ //#region src/geometry/snap.ts
550
+ const DEFAULT_SNAP_THRESHOLD = 4;
551
+ const TARGET_PRECISION = 1e6;
552
+ const addMultiples = (targets, increment) => {
553
+ if (increment <= 0) return;
554
+ for (let position = increment; position < 100; position += increment) {
555
+ const rounded = Math.round(position * TARGET_PRECISION) / TARGET_PRECISION;
556
+ if (rounded < 100) targets.add(rounded);
557
+ }
558
+ };
559
+ const resolveSnapTargets = (config, panelCount) => {
560
+ const targets = /* @__PURE__ */ new Set();
561
+ if (config.step !== void 0 && config.step > 0) addMultiples(targets, config.step);
562
+ if (config.divisions !== void 0) {
563
+ const divisions = config.divisions === "panels" ? panelCount : config.divisions;
564
+ if (divisions >= 2) addMultiples(targets, 100 / divisions);
565
+ }
566
+ return [...targets].toSorted((a, b) => a - b);
567
+ };
568
+ const snapEnabled = (config) => config !== null && ((config.step ?? 0) > 0 || config.divisions === "panels" || typeof config.divisions === "number" && config.divisions >= 2);
569
+ const nearestTarget = (position, targets) => targets.reduce((best, target) => {
570
+ if (best === void 0 || Math.abs(target - position) < Math.abs(best - position)) return target;
571
+ return best;
572
+ }, void 0);
573
+ const snapSizes = (sizes, boundaryIndex, config) => {
574
+ const threshold = config.threshold ?? 4;
575
+ const left = sizes[boundaryIndex];
576
+ const right = sizes[boundaryIndex + 1];
577
+ if (boundaryIndex < 0 || left === void 0 || right === void 0) return {
578
+ sizes,
579
+ snapped: false
580
+ };
581
+ const targets = resolveSnapTargets(config, sizes.length);
582
+ if (targets.length === 0) return {
583
+ sizes,
584
+ snapped: false
585
+ };
586
+ let position = 0;
587
+ for (let index = 0; index <= boundaryIndex; index++) position += sizes[index] ?? 0;
588
+ const target = nearestTarget(position, targets);
589
+ if (target === void 0 || Math.abs(target - position) > threshold) return {
590
+ sizes,
591
+ snapped: false
592
+ };
593
+ const delta = target - position;
594
+ if (left + delta < 0 || right - delta < 0) return {
595
+ sizes,
596
+ snapped: false
597
+ };
598
+ const next = [...sizes];
599
+ next[boundaryIndex] = left + delta;
600
+ next[boundaryIndex + 1] = right - delta;
601
+ return {
602
+ sizes: next,
603
+ snapped: true
604
+ };
605
+ };
606
+
607
+ //#endregion
608
+ //#region src/model/invariants.ts
609
+ const clampSelected = (length, selected) => {
610
+ if (length === 0) return 0;
611
+ const base = Number.isFinite(selected) ? Math.trunc(selected) : 0;
612
+ if (base < 0) return 0;
613
+ if (base > length - 1) return length - 1;
614
+ return base;
615
+ };
616
+ const normalizeTabset = (tabset) => ({
617
+ ...tabset,
618
+ selected: clampSelected(tabset.children.length, tabset.selected)
619
+ });
620
+ const normalizeRowChildren = (children) => {
621
+ const out = [];
622
+ for (const child of children) {
623
+ if (child.type === "tabset") {
624
+ if (child.children.length > 0) out.push(normalizeTabset(child));
625
+ continue;
626
+ }
627
+ const grandchildren = normalizeRowChildren(child.children);
628
+ if (grandchildren.length === 0) continue;
629
+ if (grandchildren.length === 1) {
630
+ const only = grandchildren[0];
631
+ if (only !== void 0) out.push(child.weight === void 0 ? only : {
632
+ ...only,
633
+ weight: child.weight
634
+ });
635
+ continue;
636
+ }
637
+ out.push({
638
+ ...child,
639
+ children: grandchildren
640
+ });
641
+ }
642
+ return out;
643
+ };
644
+ const rowContainsTabset = (row) => row.children.some((child) => child.type === "tabset" ? true : rowContainsTabset(child));
645
+ const normalizeLayout = (root) => {
646
+ let children = normalizeRowChildren(root.children);
647
+ let orientation = root.orientation;
648
+ let inner = children[0];
649
+ while (children.length === 1 && inner?.type === "row") {
650
+ children = inner.children;
651
+ orientation = inner.orientation;
652
+ inner = children[0];
653
+ }
654
+ return {
655
+ ...root,
656
+ children,
657
+ orientation
658
+ };
659
+ };
660
+ const normalize = (model) => {
661
+ const layout = normalizeLayout(model.layout);
662
+ const healedFloats = [];
663
+ for (const float of model.floats ?? []) {
664
+ const floatLayout = normalizeLayout(float.layout);
665
+ if (rowContainsTabset(floatLayout)) healedFloats.push({
666
+ ...float,
667
+ layout: floatLayout
668
+ });
669
+ }
670
+ const floats = healedFloats.length > 0 ? healedFloats : void 0;
671
+ const withRoots = {
672
+ ...model,
673
+ floats,
674
+ layout
675
+ };
676
+ const tabsetIds = new Set(collectTabsets(withRoots).map((tabset) => tabset.id));
677
+ const firstTabsetId = collectTabsets(withRoots)[0]?.id;
678
+ const activeTabsetId = model.activeTabsetId !== void 0 && tabsetIds.has(model.activeTabsetId) ? model.activeTabsetId : firstTabsetId;
679
+ const maximizedTabsetId = model.maximizedTabsetId !== void 0 && tabsetIds.has(model.maximizedTabsetId) ? model.maximizedTabsetId : void 0;
680
+ return {
681
+ ...withRoots,
682
+ activeTabsetId,
683
+ maximizedTabsetId
684
+ };
685
+ };
686
+
687
+ //#endregion
688
+ //#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
+ 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
+ }
708
+ };
709
+ const splitOrientation = (location) => location === "split-left" || location === "split-right" ? "row" : "column";
710
+ const splitsBefore = (location) => location === "split-left" || location === "split-top";
711
+ const placeBesideTarget = (draft, placed, targetId, location) => {
712
+ const targetTabset = findTabset(draft, targetId);
713
+ const found = findTabsetParent(findRootContaining(draft, targetId) ?? draft.layout, targetId);
714
+ if (!targetTabset || !found) return;
715
+ const orientation = splitOrientation(location);
716
+ const before = splitsBefore(location);
717
+ if (found.parent.orientation === orientation) {
718
+ const targetWeight = targetTabset.weight ?? 100;
719
+ targetTabset.weight = targetWeight / 2;
720
+ placed.weight = targetWeight / 2;
721
+ found.parent.children.splice(before ? found.index : found.index + 1, 0, placed);
722
+ } else {
723
+ const targetWeight = targetTabset.weight;
724
+ targetTabset.weight = 50;
725
+ placed.weight = 50;
726
+ const newRow = {
727
+ children: before ? [placed, targetTabset] : [targetTabset, placed],
728
+ id: createNodeId("row"),
729
+ orientation,
730
+ type: "row"
731
+ };
732
+ if (targetWeight !== void 0) newRow.weight = targetWeight;
733
+ found.parent.children.splice(found.index, 1, newRow);
734
+ }
735
+ draft.activeTabsetId = placed.id;
736
+ };
737
+ const insertTab = (draft, tab, target) => {
738
+ const { id: targetId, index, location } = target;
739
+ const targetTabset = findTabset(draft, targetId);
740
+ if (!targetTabset) return;
741
+ if (location === "center") {
742
+ const at = index ?? targetTabset.children.length;
743
+ targetTabset.children.splice(at, 0, tab);
744
+ targetTabset.selected = at;
745
+ return;
746
+ }
747
+ placeBesideTarget(draft, {
748
+ children: [tab],
749
+ id: createNodeId("tabset"),
750
+ selected: 0,
751
+ type: "tabset",
752
+ weight: 50
753
+ }, targetId, location);
754
+ };
755
+ const tabsetsInRow = (row, acc) => {
756
+ for (const child of row.children) if (child.type === "tabset") acc.push(child);
757
+ else tabsetsInRow(child, acc);
758
+ };
759
+ const wrapTabInLayout = (tab) => ({
760
+ children: [{
761
+ children: [tab],
762
+ id: createNodeId("tabset"),
763
+ selected: 0,
764
+ type: "tabset"
765
+ }],
766
+ id: createNodeId("row"),
767
+ orientation: "row",
768
+ type: "row"
769
+ });
770
+ const wrapTabsetInLayout = (tabset) => ({
771
+ children: [tabset],
772
+ id: createNodeId("row"),
773
+ orientation: "row",
774
+ type: "row"
775
+ });
776
+
777
+ //#endregion
778
+ //#region src/state/floats.ts
779
+ const DEFAULT_FLOAT_GEOMETRY = {
780
+ height: 360,
781
+ left: 80,
782
+ top: 80,
783
+ width: 480
784
+ };
785
+ const uniqueFloatName = (base, floats) => {
786
+ const used = new Set(floats.map((float) => float.name));
787
+ if (!used.has(base)) return base;
788
+ let index = 1;
789
+ while (used.has(`${base} ${index}`)) index += 1;
790
+ return `${base} ${index}`;
791
+ };
792
+ const pushFloat = (draft, layout, geometry, floatId) => {
793
+ const float = {
794
+ geometry: geometry ?? DEFAULT_FLOAT_GEOMETRY,
795
+ id: floatId ?? createNodeId("float"),
796
+ layout,
797
+ name: uniqueFloatName("Panel", draft.floats ?? []),
798
+ type: "float"
799
+ };
800
+ draft.floats = [...draft.floats ?? [], float];
801
+ const tabsets = [];
802
+ tabsetsInRow(layout, tabsets);
803
+ const first = tabsets[0];
804
+ if (first) draft.activeTabsetId = first.id;
805
+ };
806
+ const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
807
+ const root = findRootContaining(draft, tabsetId);
808
+ if (!root) return;
809
+ const detached = removeTabsetReturning(root, tabsetId);
810
+ if (detached) pushFloat(draft, wrapTabsetInLayout(detached), geometry, floatId);
811
+ };
812
+ const resolveMainTarget = (draft, targetId) => {
813
+ const mainTabsets = [];
814
+ tabsetsInRow(draft.layout, mainTabsets);
815
+ if (targetId) {
816
+ const explicit = mainTabsets.find((tabset) => tabset.id === targetId);
817
+ if (explicit) return explicit;
818
+ }
819
+ if (draft.activeTabsetId !== void 0) {
820
+ const active = mainTabsets.find((tabset) => tabset.id === draft.activeTabsetId);
821
+ if (active) return active;
822
+ }
823
+ return mainTabsets[0];
824
+ };
825
+ const dockFloat = (draft, floatId, targetId, location) => {
826
+ const floats = draft.floats ?? [];
827
+ const index = floats.findIndex((float) => float.id === floatId);
828
+ if (index === -1) return;
829
+ const [float] = floats.splice(index, 1);
830
+ draft.floats = floats;
831
+ if (!float) return;
832
+ const tabsets = [];
833
+ tabsetsInRow(float.layout, tabsets);
834
+ const tabs = tabsets.flatMap((tabset) => tabset.children);
835
+ 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;
838
+ const target = resolveMainTarget(draft, targetId);
839
+ if (!target) {
840
+ draft.layout = float.layout;
841
+ draft.activeTabsetId = leadTabset?.id;
842
+ return;
843
+ }
844
+ if (location === "center") {
845
+ const mergeStart = target.children.length;
846
+ target.children.push(...tabs);
847
+ target.selected = mergeStart + selectedOffset;
848
+ draft.activeTabsetId = target.id;
849
+ return;
850
+ }
851
+ const where = location ?? "split-right";
852
+ const placed = tabsets.length === 1 && leadTabset ? leadTabset : {
853
+ children: tabs,
854
+ id: createNodeId("tabset"),
855
+ selected: selectedOffset,
856
+ type: "tabset"
857
+ };
858
+ placed.weight = 50;
859
+ placeBesideTarget(draft, placed, target.id, where);
860
+ };
861
+
862
+ //#endregion
863
+ //#region src/state/reducer.ts
864
+ const assertNever = (value) => {
865
+ throw new Error(`Unhandled action: ${JSON.stringify(value)}`);
866
+ };
867
+ const applyAction = (draft, action) => {
868
+ switch (action.type) {
869
+ case "addNode":
870
+ insertTab(draft, action.tab, {
871
+ id: action.targetId,
872
+ index: action.index,
873
+ location: action.location
874
+ });
875
+ return;
876
+ case "adjustSplit": {
877
+ const row = findRow(findRootContaining(draft, action.rowId) ?? draft.layout, action.rowId);
878
+ if (!row) return;
879
+ for (let index = 0; index < action.weights.length; index++) {
880
+ const child = row.children[index];
881
+ const weight = action.weights[index];
882
+ if (!child || weight === void 0) continue;
883
+ child.weight = weight;
884
+ }
885
+ return;
886
+ }
887
+ case "deleteTab": {
888
+ const location = findTab(draft, action.tabId);
889
+ if (location) location.container.children.splice(location.index, 1);
890
+ return;
891
+ }
892
+ case "deleteTabset": {
893
+ const root = findRootContaining(draft, action.tabsetId);
894
+ if (root) removeTabset(root, action.tabsetId);
895
+ return;
896
+ }
897
+ case "dockFloat":
898
+ dockFloat(draft, action.floatId, action.targetId, action.location);
899
+ return;
900
+ case "floatTab": {
901
+ const location = findTab(draft, action.tabId);
902
+ if (!location) return;
903
+ const [removed] = location.container.children.splice(location.index, 1);
904
+ if (removed) pushFloat(draft, wrapTabInLayout(removed), action.geometry, action.floatId);
905
+ return;
906
+ }
907
+ case "floatTabset":
908
+ floatTabsetById(draft, action.tabsetId, action.geometry, action.floatId);
909
+ return;
910
+ case "moveFloat": {
911
+ const float = findFloat(draft, action.floatId);
912
+ if (float) float.geometry = action.geometry;
913
+ return;
914
+ }
915
+ case "moveNode": {
916
+ const source = findTab(draft, action.sourceId);
917
+ if (!source) return;
918
+ const [removed] = source.container.children.splice(source.index, 1);
919
+ if (!removed) return;
920
+ insertTab(draft, removed, {
921
+ id: action.targetId,
922
+ index: action.index,
923
+ location: action.location
924
+ });
925
+ return;
926
+ }
927
+ case "moveTabset": {
928
+ if (action.sourceId === action.targetId) return;
929
+ const source = findTabset(draft, action.sourceId);
930
+ if (!source) return;
931
+ if (action.location === "center") {
932
+ const target = findTabset(draft, action.targetId);
933
+ 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;
938
+ const sourceRoot = findRootContaining(draft, action.sourceId);
939
+ if (sourceRoot) removeTabset(sourceRoot, action.sourceId);
940
+ }
941
+ return;
942
+ }
943
+ if (action.location.startsWith("split-")) {
944
+ const sourceRoot = findRootContaining(draft, action.sourceId);
945
+ const detached = sourceRoot ? removeTabsetReturning(sourceRoot, action.sourceId) : void 0;
946
+ if (detached) placeBesideTarget(draft, detached, action.targetId, action.location);
947
+ }
948
+ return;
949
+ }
950
+ case "renameFloat": {
951
+ const float = findFloat(draft, action.floatId);
952
+ if (float) {
953
+ const others = (draft.floats ?? []).filter((other) => other.id !== action.floatId);
954
+ float.name = uniqueFloatName(action.name, others);
955
+ }
956
+ return;
957
+ }
958
+ case "renameTab": {
959
+ const location = findTab(draft, action.tabId);
960
+ if (location) location.tab.name = action.name;
961
+ return;
962
+ }
963
+ case "selectTab": {
964
+ const tabset = findTabset(draft, action.tabsetId);
965
+ if (tabset) tabset.selected = action.index;
966
+ return;
967
+ }
968
+ case "setActiveTabset":
969
+ if (findTabset(draft, action.tabsetId)) draft.activeTabsetId = action.tabsetId;
970
+ return;
971
+ case "setFloatMinimized": {
972
+ const float = findFloat(draft, action.floatId);
973
+ if (float) float.minimized = action.minimized;
974
+ return;
975
+ }
976
+ case "setMaximizedTabset":
977
+ draft.maximizedTabsetId = action.tabsetId ?? void 0;
978
+ return;
979
+ case "updateGlobalAttributes":
980
+ Object.assign(draft.global, action.attrs);
981
+ return;
982
+ case "updateNodeAttributes": {
983
+ const node = findAttributedNode(draft, action.nodeId);
984
+ if (node) Object.assign(node, action.attrs);
985
+ return;
986
+ }
987
+ default: assertNever(action);
988
+ }
989
+ };
990
+ const reducer = (model, action) => {
991
+ const draft = structuredClone(model);
992
+ applyAction(draft, action);
993
+ return normalize(draft);
994
+ };
995
+
996
+ //#endregion
997
+ //#region src/state/history.ts
998
+ const HISTORY_LIMIT = 100;
999
+ const createHistory = (present) => ({
1000
+ future: [],
1001
+ past: [],
1002
+ present
1003
+ });
1004
+ const canUndo = (history) => history.past.length > 0;
1005
+ const canRedo = (history) => history.future.length > 0;
1006
+ const dispatch = (history, action) => {
1007
+ const present = reducer(history.present, action);
1008
+ return {
1009
+ future: [],
1010
+ past: [...history.past, history.present].slice(-100),
1011
+ present
1012
+ };
1013
+ };
1014
+ const undo = (history) => {
1015
+ const previous = history.past.at(-1);
1016
+ if (previous === void 0) return history;
1017
+ return {
1018
+ future: [history.present, ...history.future],
1019
+ past: history.past.slice(0, -1),
1020
+ present: previous
1021
+ };
1022
+ };
1023
+ const redo = (history) => {
1024
+ const [next, ...rest] = history.future;
1025
+ if (next === void 0) return history;
1026
+ return {
1027
+ future: rest,
1028
+ past: [...history.past, history.present].slice(-100),
1029
+ present: next
1030
+ };
1031
+ };
1032
+
1033
+ //#endregion
1034
+ //#region src/machines/dashfoo-machine.ts
1035
+ const dashfooMachine = setup({ types: {
1036
+ context: {},
1037
+ events: {},
1038
+ input: {}
1039
+ } }).createMachine({
1040
+ context: ({ input }) => ({ history: createHistory(input.model) }),
1041
+ id: "dashfoo",
1042
+ initial: "ready",
1043
+ states: { ready: { on: {
1044
+ DISPATCH: { actions: assign({ history: ({ context, event }) => dispatch(context.history, event.action) }) },
1045
+ REDO: { actions: assign({ history: ({ context }) => redo(context.history) }) },
1046
+ SET_MODEL: { actions: assign({ history: ({ event }) => createHistory(event.model) }) },
1047
+ UNDO: { actions: assign({ history: ({ context }) => undo(context.history) }) }
1048
+ } } }
1049
+ });
1050
+
1051
+ //#endregion
1052
+ //#region src/machines/drag-dock-machine.ts
1053
+ const requireDrop = (context) => {
1054
+ if (context.subject === null || context.intent === null) throw new Error("drop requires a subject and a drop intent");
1055
+ return {
1056
+ intent: context.intent,
1057
+ subject: context.subject
1058
+ };
1059
+ };
1060
+ const dragDockMachine = setup({
1061
+ guards: { hasValidDrop: ({ context }) => context.subject !== null && context.intent !== null },
1062
+ types: {
1063
+ context: {},
1064
+ emitted: {},
1065
+ events: {}
1066
+ }
1067
+ }).createMachine({
1068
+ context: {
1069
+ intent: null,
1070
+ subject: null
1071
+ },
1072
+ id: "dragDock",
1073
+ initial: "idle",
1074
+ states: {
1075
+ dragging: { on: {
1076
+ CANCEL: { target: "idle" },
1077
+ DROP: [{
1078
+ actions: emit(({ context }) => {
1079
+ const { intent, subject } = requireDrop(context);
1080
+ return {
1081
+ action: subject.kind === "external" ? {
1082
+ index: intent.index,
1083
+ location: intent.location,
1084
+ tab: subject.tab,
1085
+ targetId: intent.targetId,
1086
+ type: "addNode"
1087
+ } : {
1088
+ index: intent.index,
1089
+ location: intent.location,
1090
+ sourceId: subject.id,
1091
+ targetId: intent.targetId,
1092
+ type: subject.kind === "tabset" ? "moveTabset" : "moveNode"
1093
+ },
1094
+ type: "COMMIT"
1095
+ };
1096
+ }),
1097
+ guard: "hasValidDrop",
1098
+ target: "idle"
1099
+ }, { target: "idle" }],
1100
+ OVER: { actions: assign({ intent: ({ event }) => event.intent }) }
1101
+ } },
1102
+ idle: {
1103
+ entry: assign({
1104
+ intent: null,
1105
+ subject: null
1106
+ }),
1107
+ on: { START: {
1108
+ actions: assign({ subject: ({ event }) => event.subject }),
1109
+ target: "dragging"
1110
+ } }
1111
+ }
1112
+ }
1113
+ });
1114
+
1115
+ //#endregion
1116
+ //#region src/model/serialize.ts
1117
+ const parseModel = (value) => {
1118
+ const model = normalize(dashfooSchema.parse(value));
1119
+ const duplicates = findDuplicateIds(model);
1120
+ if (duplicates.length > 0) console.warn(`[dashfoo] duplicate node ids in the loaded layout: ${duplicates.join(", ")}`);
1121
+ return model;
1122
+ };
1123
+ const toJSON = (model) => JSON.stringify(model);
1124
+ const fromJSON = (json) => parseModel(JSON.parse(json));
1125
+
1126
+ //#endregion
1127
+ //#region src/model/stack.ts
1128
+ const stackModel = (model, orientation = "column") => {
1129
+ const mainTabsets = [];
1130
+ collectTabsetsInRow(model.layout, mainTabsets);
1131
+ const children = [];
1132
+ for (const tabset of mainTabsets) children.push({
1133
+ ...tabset,
1134
+ weight: 1
1135
+ });
1136
+ return normalize({
1137
+ ...model,
1138
+ layout: {
1139
+ children,
1140
+ id: model.layout.id,
1141
+ orientation,
1142
+ type: "row"
1143
+ },
1144
+ maximizedTabsetId: void 0
1145
+ });
1146
+ };
1147
+
1148
+ //#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 };
2
1150
  //# sourceMappingURL=index.js.map