@dashfoo/core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2 +1,1175 @@
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({height:e.number(),left:e.number(),top:e.number(),width:e.number()}),m=e.object({geometry:p,id:e.string(),layout:f,minimized:e.boolean().optional(),name:e.string().optional(),type:e.literal(`float`)}),h=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()}),g=e.object({activeTabsetId:e.string().optional(),global:h,layout:f,maximizedTabsetId:e.string().optional(),version:e.literal(1),floats:e.array(m).optional()}),ee=u.pick({config:!0,enableClose:!0,enableDrag:!0,enableRename:!0,name:!0}).partial(),te=d.pick({enableClose:!0,enableMaximize:!0,max:!0,min:!0,selected:!0,weight:!0}).partial(),ne=e.object({max:c.optional(),min:c.optional(),orientation:s,snap:l.optional(),weight:e.number()}).partial(),_=e.union([ee,te,ne]),v=e.enum([`center`,`split-bottom`,`split-left`,`split-right`,`split-top`]),re=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:h.partial(),type:e.literal(`updateGlobalAttributes`)}),e.object({floatId:e.string().optional(),geometry:p.optional(),tabId:e.string(),type:e.literal(`floatTab`)}),e.object({floatId:e.string().optional(),geometry:p.optional(),tabsetId:e.string(),type:e.literal(`floatTabset`)}),e.object({floatId:e.string(),location:v.optional(),targetId:e.string().optional(),type:e.literal(`dockFloat`)}),e.object({floatId:e.string(),geometry:p,type:e.literal(`moveFloat`)}),e.object({floatId:e.string(),minimized:e.boolean(),type:e.literal(`setFloatMinimized`)}),e.object({floatId:e.string(),name:e.string(),type:e.literal(`renameFloat`)})]),y=(e=`node`)=>`${e}-${crypto.randomUUID()}`,ie=()=>y(`tab`),b=e=>[e.layout,...(e.floats??[]).map(e=>e.layout)],x=(e,t)=>(e.floats??[]).find(e=>e.id===t),S=(e,t)=>{for(let n of e.children)n.type===`tabset`?t.push(n):S(n,t)},C=e=>{let t=[];for(let n of b(e))S(n,t);return t},ae=e=>C(e)[0],w=(e,t)=>C(e).find(e=>e.id===t),oe=(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}},T=(e,t)=>{for(let n of C(e)){let e=oe(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}},D=(e,t)=>{if(e.id===t)return e;for(let n of e.children){if(n.type===`row`){let e=D(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}},O=(e,t)=>{for(let n of b(e)){let e=D(n,t);if(e)return e}},k=(e,t)=>{for(let n of b(e))if(D(n,t))return n},A=(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=A(n,t);if(e)return e}},se=(e,t)=>{t.push(e.id);for(let n of e.children)if(n.type===`row`)se(n,t);else{t.push(n.id);for(let e of n.children)t.push(e.id)}},j=e=>{let t=[];for(let n of e.floats??[])t.push(n.id);for(let n of b(e))se(n,t);let n=new Set,r=new Set;for(let e of t)n.has(e)&&r.add(e),n.add(e);return[...r]},M=e=>typeof e==`number`?{unit:`px`,value:e}:e,ce=(e,t,n={})=>({component:e,name:t,type:`tab`,...n,id:n.id??e}),le=(e,t={})=>{let{max:n,min:r,...i}=t;return{children:e,type:`tabset`,...i,...n===void 0?{}:{max:M(n)},...r===void 0?{}:{min:M(r)},id:t.id??y(`tabset`),selected:t.selected??0}},ue=(e,t={})=>{let{max:n,min:r,...i}=t;return{children:e,type:`row`,...i,...n===void 0?{}:{max:M(n)},...r===void 0?{}:{min:M(r)},id:t.id??y(`row`),orientation:t.orientation??`row`}},de=(e,t,n={})=>({geometry:t,layout:e,type:`float`,...n,id:n.id??y(`float`)}),fe=(e,t={})=>{let n={layout:e,version:1,...t,global:t.global??{}},r=j(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,pe=(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}},me=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`},he=(e,t,n)=>{let r=n?.bandFraction??N;if(t.width<=0||t.height<=0)return{kind:`tab`};let i=pe(e,t);return Math.min(i.left,i.right,i.top,i.bottom)>r?{kind:`tab`}:{edge:me(i),kind:`split`}},ge=(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]}]},_e=(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}},ve=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)},ye=e=>e!==null&&((e.step??0)>0||e.divisions===`panels`||typeof e.divisions==`number`&&e.divisions>=2),be=(e,t)=>t.reduce((t,n)=>t===void 0||Math.abs(n-e)<Math.abs(t-e)?n:t,void 0),xe=(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=be(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}},Se=(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},Ce=e=>({...e,selected:Se(e.children.length,e.selected)}),L=e=>{let t=[];for(let n of e){if(n.type===`tabset`){n.children.length>0&&t.push(Ce(n));continue}let e=L(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},R=e=>e.children.some(e=>e.type===`tabset`?!0:R(e)),z=e=>{let t=L(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}},B=e=>{let t=z(e.layout),n=[];for(let t of e.floats??[]){let e=z(t.layout);R(e)&&n.push({...t,layout:e})}let r=n.length>0?n:void 0,i={...e,floats:r,layout:t},a=new Set(C(i).map(e=>e.id)),o=C(i)[0]?.id,s=e.activeTabsetId!==void 0&&a.has(e.activeTabsetId)?e.activeTabsetId:o,c=e.maximizedTabsetId!==void 0&&a.has(e.maximizedTabsetId)?e.maximizedTabsetId:void 0;return{...i,activeTabsetId:s,maximizedTabsetId:c}},V=(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`&&V(n,t))return!0;return!1},H=(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=H(n,t);if(e)return e}},we=e=>e===`split-left`||e===`split-right`?`row`:`column`,Te=e=>e===`split-left`||e===`split-top`,U=(e,t,n,r)=>{let i=w(e,n),a=A(k(e,n)??e.layout,n);if(!i||!a)return;let o=we(r),s=Te(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:y(`row`),orientation:o,type:`row`};e!==void 0&&(n.weight=e),a.parent.children.splice(a.index,1,n)}e.activeTabsetId=t.id},W=(e,t,n)=>{let{id:r,index:i,location:a}=n,o=w(e,r);if(o){if(a===`center`){let e=i??o.children.length;o.children.splice(e,0,t),o.selected=e;return}U(e,{children:[t],id:y(`tabset`),selected:0,type:`tabset`,weight:50},r,a)}},G=(e,t)=>{for(let n of e.children)n.type===`tabset`?t.push(n):G(n,t)},Ee=e=>({children:[{children:[e],id:y(`tabset`),selected:0,type:`tabset`}],id:y(`row`),orientation:`row`,type:`row`}),De=e=>({children:[e],id:y(`row`),orientation:`row`,type:`row`}),Oe={height:360,left:80,top:80,width:480},K=(e,t)=>{let n=new Set(t.map(e=>e.name));if(!n.has(e))return e;let r=1;for(;n.has(`${e} ${r}`);)r+=1;return`${e} ${r}`},q=(e,t,n,r)=>{let i={geometry:n??Oe,id:r??y(`float`),layout:t,name:K(`Panel`,e.floats??[]),type:`float`};e.floats=[...e.floats??[],i];let a=[];G(t,a);let o=a[0];o&&(e.activeTabsetId=o.id)},ke=(e,t,n,r)=>{let i=k(e,t);if(!i)return;let a=H(i,t);a&&q(e,De(a),n,r)},Ae=(e,t)=>{let n=[];if(G(e.layout,n),t){let e=n.find(e=>e.id===t);if(e)return e}if(e.activeTabsetId!==void 0){let t=n.find(t=>t.id===e.activeTabsetId);if(t)return t}return n[0]},je=(e,t,n,r)=>{let i=e.floats??[],a=i.findIndex(e=>e.id===t);if(a===-1)return;let[o]=i.splice(a,1);if(e.floats=i,!o)return;let s=[];G(o.layout,s);let c=s.flatMap(e=>e.children);if(c.length===0)return;let l=s[0],u=l?Math.min(Math.max(l.selected,0),l.children.length-1):0,d=Ae(e,n);if(!d){e.layout=o.layout,e.activeTabsetId=l?.id;return}if(r===`center`){let t=d.children.length;d.children.push(...c),d.selected=t+u,e.activeTabsetId=d.id;return}let f=r??`split-right`,p=s.length===1&&l?l:{children:c,id:y(`tabset`),selected:u,type:`tabset`};p.weight=50,U(e,p,d.id,f)},Me=e=>{throw Error(`Unhandled action: ${JSON.stringify(e)}`)},Ne=(e,t)=>{switch(t.type){case`addNode`:W(e,t.tab,{id:t.targetId,index:t.index,location:t.location});return;case`adjustSplit`:{let n=E(k(e,t.rowId)??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=T(e,t.tabId);n&&n.container.children.splice(n.index,1);return}case`deleteTabset`:{let n=k(e,t.tabsetId);n&&V(n,t.tabsetId);return}case`dockFloat`:je(e,t.floatId,t.targetId,t.location);return;case`floatTab`:{let n=T(e,t.tabId);if(!n)return;let[r]=n.container.children.splice(n.index,1);r&&q(e,Ee(r),t.geometry,t.floatId);return}case`floatTabset`:ke(e,t.tabsetId,t.geometry,t.floatId);return;case`moveFloat`:{let n=x(e,t.floatId);n&&(n.geometry=t.geometry);return}case`moveNode`:{let n=T(e,t.sourceId);if(!n)return;let[r]=n.container.children.splice(n.index,1);if(!r)return;W(e,r,{id:t.targetId,index:t.index,location:t.location});return}case`moveTabset`:{if(t.sourceId===t.targetId)return;let n=w(e,t.sourceId);if(!n)return;if(t.location===`center`){let r=w(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;let o=k(e,t.sourceId);o&&V(o,t.sourceId)}return}if(t.location.startsWith(`split-`)){let n=k(e,t.sourceId),r=n?H(n,t.sourceId):void 0;r&&U(e,r,t.targetId,t.location)}return}case`renameFloat`:{let n=x(e,t.floatId);if(n){let r=(e.floats??[]).filter(e=>e.id!==t.floatId);n.name=K(t.name,r)}return}case`renameTab`:{let n=T(e,t.tabId);n&&(n.tab.name=t.name);return}case`selectTab`:{let n=w(e,t.tabsetId);n&&(n.selected=t.index);return}case`setActiveTabset`:w(e,t.tabsetId)&&(e.activeTabsetId=t.tabsetId);return;case`setFloatMinimized`:{let n=x(e,t.floatId);n&&(n.minimized=t.minimized);return}case`setMaximizedTabset`:e.maximizedTabsetId=t.tabsetId??void 0;return;case`updateGlobalAttributes`:Object.assign(e.global,t.attrs);return;case`updateNodeAttributes`:{let n=O(e,t.nodeId);n&&Object.assign(n,t.attrs);return}default:Me(t)}},J=(e,t)=>{let n=structuredClone(e);return Ne(n,t),B(n)},Pe=100,Y=e=>({future:[],past:[],present:e}),Fe=e=>e.past.length>0,Ie=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}},Le=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)})}}}}}),Re=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}},ze=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}=Re(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=B(g.parse(e)),n=j(t);return n.length>0&&console.warn(`[dashfoo] duplicate node ids in the loaded layout: ${n.join(`, `)}`),t},Be=e=>JSON.stringify(e),Ve=e=>$(JSON.parse(e)),He=(e,t=`column`)=>{let n=[];S(e.layout,n);let r=[];for(let e of n)r.push({...e,weight:1});return B({...e,layout:{children:r,id:e.layout.id,orientation:t,type:`row`},maximizedTabsetId:void 0})};export{ve as DEFAULT_SNAP_THRESHOLD,Pe as HISTORY_LIMIT,re as actionSchema,Ie as canRedo,Fe as canUndo,b as collectRoots,C as collectTabsets,S as collectTabsetsInRow,Y as createHistory,y as createNodeId,ie as createTabId,Le as dashfooMachine,g as dashfooSchema,c as dimensionSchema,X as dispatch,v as dockLocationSchema,ge as dockZonePolygons,ze as dragDockMachine,a as edgeSchema,O as findAttributedNode,j as findDuplicateIds,x as findFloat,k as findRootContaining,E as findRow,T as findTab,w as findTabset,A as findTabsetParent,de as floatNode,m as floatNodeSchema,Ve as fromJSON,p as geometrySchema,ae as getFirstTabset,h as globalAttributesSchema,i as jsonValueSchema,fe as model,_ as mutableNodeAttrsSchema,B as normalize,s as orientationSchema,$ as parseModel,Q as redo,J as reducer,he as resolveDockTarget,I as resolveSnapTargets,ue as row,f as rowNodeSchema,ye as snapEnabled,l as snapSchema,xe as snapSizes,He as stackModel,ce as tab,u as tabNodeSchema,le as tabset,d as tabsetNodeSchema,Be as toJSON,Z as undo,o as unitSchema,_e 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
+ floats: z.array(floatNodeSchema).optional(),
99
+ global: globalAttributesSchema,
100
+ layout: rowNodeSchema,
101
+ maximizedTabsetId: z.string().optional(),
102
+ version: z.literal(1)
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 splitWeightsSchema = z.array(z.number());
142
+ const actionSchema = z.discriminatedUnion("type", [
143
+ z.object({
144
+ index: z.number().int().optional(),
145
+ location: dockLocationSchema,
146
+ tab: tabNodeSchema,
147
+ targetId: z.string(),
148
+ type: z.literal("addNode")
149
+ }),
150
+ z.object({
151
+ index: z.number().int().optional(),
152
+ location: dockLocationSchema,
153
+ sourceId: z.string(),
154
+ targetId: z.string(),
155
+ type: z.literal("moveNode")
156
+ }),
157
+ z.object({
158
+ index: z.number().int().optional(),
159
+ location: dockLocationSchema,
160
+ sourceId: z.string(),
161
+ targetId: z.string(),
162
+ type: z.literal("moveTabset")
163
+ }),
164
+ z.object({
165
+ index: z.number().int(),
166
+ tabsetId: z.string(),
167
+ type: z.literal("selectTab")
168
+ }),
169
+ z.object({
170
+ tabsetId: z.string(),
171
+ type: z.literal("setActiveTabset")
172
+ }),
173
+ z.object({
174
+ tabsetId: z.string().nullable(),
175
+ type: z.literal("setMaximizedTabset")
176
+ }),
177
+ z.object({
178
+ name: z.string(),
179
+ tabId: z.string(),
180
+ type: z.literal("renameTab")
181
+ }),
182
+ z.object({
183
+ tabId: z.string(),
184
+ type: z.literal("deleteTab")
185
+ }),
186
+ z.object({
187
+ tabsetId: z.string(),
188
+ type: z.literal("deleteTabset")
189
+ }),
190
+ z.object({
191
+ rowId: z.string(),
192
+ type: z.literal("adjustSplit"),
193
+ weights: splitWeightsSchema
194
+ }),
195
+ z.object({
196
+ attrs: mutableNodeAttrsSchema,
197
+ nodeId: z.string(),
198
+ type: z.literal("updateNodeAttributes")
199
+ }),
200
+ z.object({
201
+ attrs: globalAttributesSchema.partial(),
202
+ type: z.literal("updateGlobalAttributes")
203
+ }),
204
+ z.object({
205
+ floatId: z.string().optional(),
206
+ geometry: geometrySchema.optional(),
207
+ tabId: z.string(),
208
+ type: z.literal("floatTab")
209
+ }),
210
+ z.object({
211
+ floatId: z.string().optional(),
212
+ geometry: geometrySchema.optional(),
213
+ tabsetId: z.string(),
214
+ type: z.literal("floatTabset")
215
+ }),
216
+ z.object({
217
+ floatId: z.string(),
218
+ location: dockLocationSchema.optional(),
219
+ targetId: z.string().optional(),
220
+ type: z.literal("dockFloat")
221
+ }),
222
+ z.object({
223
+ floatId: z.string(),
224
+ geometry: geometrySchema,
225
+ type: z.literal("moveFloat")
226
+ }),
227
+ z.object({
228
+ floatId: z.string(),
229
+ minimized: z.boolean(),
230
+ type: z.literal("setFloatMinimized")
231
+ }),
232
+ z.object({
233
+ floatId: z.string(),
234
+ name: z.string(),
235
+ type: z.literal("renameFloat")
236
+ })
237
+ ]);
238
+
239
+ //#endregion
240
+ //#region src/model/ids.ts
241
+ const createNodeId = (prefix = "node") => `${prefix}-${crypto.randomUUID()}`;
242
+ const createTabId = () => createNodeId("tab");
243
+
244
+ //#endregion
245
+ //#region src/model/tree.ts
246
+ const collectRoots = (model) => [model.layout, ...(model.floats ?? []).map((float) => float.layout)];
247
+ const findFloat = (model, floatId) => (model.floats ?? []).find((float) => float.id === floatId);
248
+ /**
249
+ * Depth-first pre-order over one root: the row itself, then each child in
250
+ * order, descending into nested rows and into a tabset's tabs. Returning a
251
+ * defined value from `visit` stops the walk and becomes the result, so one
252
+ * traversal serves both find-first and collect-all.
253
+ *
254
+ * Every query below used to carry its own copy of this recursion, and they had
255
+ * drifted in child order: some scanned a row's own tabsets before descending,
256
+ * others interleaved. With unique ids all orders agree, so they now share this
257
+ * one; a model with duplicate ids (which the builder and the parser both warn
258
+ * about) can see a different duplicate win than it did before.
259
+ */
260
+ const walk = (row, visit, parent, index = -1) => {
261
+ const atRoot = visit({
262
+ index,
263
+ node: row,
264
+ parent
265
+ });
266
+ if (atRoot !== void 0) return atRoot;
267
+ for (const [childIndex, child] of row.children.entries()) {
268
+ if (child.type === "row") {
269
+ const inRow = walk(child, visit, row, childIndex);
270
+ if (inRow !== void 0) return inRow;
271
+ continue;
272
+ }
273
+ const atTabset = visit({
274
+ index: childIndex,
275
+ node: child,
276
+ parent: row
277
+ });
278
+ if (atTabset !== void 0) return atTabset;
279
+ for (const [tabIndex, tab] of child.children.entries()) {
280
+ const atTab = visit({
281
+ index: tabIndex,
282
+ node: tab,
283
+ parent: child
284
+ });
285
+ if (atTab !== void 0) return atTab;
286
+ }
287
+ }
288
+ };
289
+ const locate = (row, id) => walk(row, (found) => found.node.id === id ? found : void 0);
290
+ const collectTabsetsInRow = (row, acc) => {
291
+ walk(row, ({ node }) => {
292
+ if (node.type === "tabset") acc.push(node);
293
+ });
294
+ };
295
+ const collectTabsets = (model) => {
296
+ const acc = [];
297
+ for (const root of collectRoots(model)) collectTabsetsInRow(root, acc);
298
+ return acc;
299
+ };
300
+ const getFirstTabset = (model) => collectTabsets(model)[0];
301
+ const findTabset = (model, tabsetId) => collectTabsets(model).find((tabset) => tabset.id === tabsetId);
302
+ const findTabInContainer = (container, tabId) => {
303
+ const index = container.children.findIndex((tab) => tab.id === tabId);
304
+ if (index === -1) return;
305
+ const tab = container.children.at(index);
306
+ if (!tab) return;
307
+ return {
308
+ container,
309
+ index,
310
+ tab
311
+ };
312
+ };
313
+ const findTab = (model, tabId) => {
314
+ for (const tabset of collectTabsets(model)) {
315
+ const found = findTabInContainer(tabset, tabId);
316
+ if (found) return found;
317
+ }
318
+ };
319
+ const findRow = (row, id) => {
320
+ const found = locate(row, id)?.node;
321
+ return found?.type === "row" ? found : void 0;
322
+ };
323
+ const findAttributedNodeInRow = (row, id) => locate(row, id)?.node;
324
+ const findAttributedNode = (model, id) => {
325
+ for (const root of collectRoots(model)) {
326
+ const found = findAttributedNodeInRow(root, id);
327
+ if (found) return found;
328
+ }
329
+ };
330
+ const findRootContaining = (model, nodeId) => {
331
+ for (const root of collectRoots(model)) if (findAttributedNodeInRow(root, nodeId)) return root;
332
+ };
333
+ const findTabsetParent = (row, tabsetId) => {
334
+ const found = locate(row, tabsetId);
335
+ return found?.node.type === "tabset" && found.parent?.type === "row" ? {
336
+ index: found.index,
337
+ parent: found.parent
338
+ } : void 0;
339
+ };
340
+ const collectIdsInRow = (row, acc) => {
341
+ walk(row, ({ node }) => {
342
+ acc.push(node.id);
343
+ });
344
+ };
345
+ const findDuplicateIds = (model) => {
346
+ const ids = [];
347
+ for (const float of model.floats ?? []) ids.push(float.id);
348
+ for (const root of collectRoots(model)) collectIdsInRow(root, ids);
349
+ const seen = /* @__PURE__ */ new Set();
350
+ const duplicates = /* @__PURE__ */ new Set();
351
+ for (const id of ids) {
352
+ if (seen.has(id)) duplicates.add(id);
353
+ seen.add(id);
354
+ }
355
+ return [...duplicates];
356
+ };
357
+
358
+ //#endregion
359
+ //#region src/model/builders.ts
360
+ const toDimension = (value) => typeof value === "number" ? {
361
+ unit: "px",
362
+ value
363
+ } : value;
364
+ const tab = (component, name, options = {}) => ({
365
+ component,
366
+ name,
367
+ type: "tab",
368
+ ...options,
369
+ id: options.id ?? component
370
+ });
371
+ const tabset = (children, options = {}) => {
372
+ const { max, min, ...rest } = options;
373
+ return {
374
+ children,
375
+ type: "tabset",
376
+ ...rest,
377
+ ...max === void 0 ? {} : { max: toDimension(max) },
378
+ ...min === void 0 ? {} : { min: toDimension(min) },
379
+ id: options.id ?? createNodeId("tabset"),
380
+ selected: options.selected ?? 0
381
+ };
382
+ };
383
+ const row = (children, options = {}) => {
384
+ const { max, min, ...rest } = options;
385
+ return {
386
+ children,
387
+ type: "row",
388
+ ...rest,
389
+ ...max === void 0 ? {} : { max: toDimension(max) },
390
+ ...min === void 0 ? {} : { min: toDimension(min) },
391
+ id: options.id ?? createNodeId("row"),
392
+ orientation: options.orientation ?? "row"
393
+ };
394
+ };
395
+ const floatNode = (layout, geometry, options = {}) => ({
396
+ geometry,
397
+ layout,
398
+ type: "float",
399
+ ...options,
400
+ id: options.id ?? createNodeId("float")
401
+ });
402
+ const model = (layout, options = {}) => {
403
+ const built = {
404
+ layout,
405
+ version: 1,
406
+ ...options,
407
+ global: options.global ?? {}
408
+ };
409
+ const duplicates = findDuplicateIds(built);
410
+ if (duplicates.length > 0) console.warn(`[dashfoo] builder produced duplicate node ids: ${duplicates.join(", ")}. Pass explicit ids when reusing a component`);
411
+ return built;
412
+ };
413
+
414
+ //#endregion
415
+ //#region src/geometry/geometry.ts
416
+ const DEFAULT_BAND_FRACTION = .22;
417
+ const edgeDistances = (pointer, rect) => {
418
+ const fx = (pointer.x - rect.x) / rect.width;
419
+ const fy = (pointer.y - rect.y) / rect.height;
420
+ return {
421
+ bottom: 1 - fy,
422
+ left: fx,
423
+ right: 1 - fx,
424
+ top: fy
425
+ };
426
+ };
427
+ const closestEdge = (d) => {
428
+ const min = Math.min(d.left, d.right, d.top, d.bottom);
429
+ if (min === d.left) return "left";
430
+ if (min === d.right) return "right";
431
+ if (min === d.top) return "top";
432
+ return "bottom";
433
+ };
434
+ const resolveDockTarget = (pointer, rect, opts) => {
435
+ const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
436
+ if (rect.width <= 0 || rect.height <= 0) return { kind: "tab" };
437
+ const distances = edgeDistances(pointer, rect);
438
+ if (Math.min(distances.left, distances.right, distances.top, distances.bottom) > band) return { kind: "tab" };
439
+ return {
440
+ edge: closestEdge(distances),
441
+ kind: "split"
442
+ };
443
+ };
444
+ const dockZonePolygons = (rect, opts) => {
445
+ const { height: h, width: w, x, y } = rect;
446
+ const nw = {
447
+ x,
448
+ y
449
+ };
450
+ const ne = {
451
+ x: x + w,
452
+ y
453
+ };
454
+ const se = {
455
+ x: x + w,
456
+ y: y + h
457
+ };
458
+ const sw = {
459
+ x,
460
+ y: y + h
461
+ };
462
+ if (w <= 0 || h <= 0) return [{
463
+ location: "center",
464
+ points: [
465
+ nw,
466
+ ne,
467
+ se,
468
+ sw
469
+ ]
470
+ }];
471
+ const band = opts?.bandFraction ?? DEFAULT_BAND_FRACTION;
472
+ const bx = band * w;
473
+ const by = band * h;
474
+ const innerNw = {
475
+ x: x + bx,
476
+ y: y + by
477
+ };
478
+ const innerNe = {
479
+ x: x + w - bx,
480
+ y: y + by
481
+ };
482
+ const innerSe = {
483
+ x: x + w - bx,
484
+ y: y + h - by
485
+ };
486
+ const innerSw = {
487
+ x: x + bx,
488
+ y: y + h - by
489
+ };
490
+ return [
491
+ {
492
+ location: "center",
493
+ points: [
494
+ innerNw,
495
+ innerNe,
496
+ innerSe,
497
+ innerSw
498
+ ]
499
+ },
500
+ {
501
+ location: "split-left",
502
+ points: [
503
+ nw,
504
+ innerNw,
505
+ innerSw,
506
+ sw
507
+ ]
508
+ },
509
+ {
510
+ location: "split-right",
511
+ points: [
512
+ ne,
513
+ se,
514
+ innerSe,
515
+ innerNe
516
+ ]
517
+ },
518
+ {
519
+ location: "split-top",
520
+ points: [
521
+ nw,
522
+ ne,
523
+ innerNe,
524
+ innerNw
525
+ ]
526
+ },
527
+ {
528
+ location: "split-bottom",
529
+ points: [
530
+ sw,
531
+ innerSw,
532
+ innerSe,
533
+ se
534
+ ]
535
+ }
536
+ ];
537
+ };
538
+ const zoneRect = (rect, location) => {
539
+ const halfW = rect.width / 2;
540
+ const halfH = rect.height / 2;
541
+ switch (location) {
542
+ case "split-bottom": return {
543
+ height: halfH,
544
+ width: rect.width,
545
+ x: rect.x,
546
+ y: rect.y + halfH
547
+ };
548
+ case "split-left": return {
549
+ height: rect.height,
550
+ width: halfW,
551
+ x: rect.x,
552
+ y: rect.y
553
+ };
554
+ case "split-right": return {
555
+ height: rect.height,
556
+ width: halfW,
557
+ x: rect.x + halfW,
558
+ y: rect.y
559
+ };
560
+ case "split-top": return {
561
+ height: halfH,
562
+ width: rect.width,
563
+ x: rect.x,
564
+ y: rect.y
565
+ };
566
+ case "center": return rect;
567
+ default: return rect;
568
+ }
569
+ };
570
+
571
+ //#endregion
572
+ //#region src/geometry/snap.ts
573
+ const DEFAULT_SNAP_THRESHOLD = 4;
574
+ const TARGET_PRECISION = 1e6;
575
+ const addMultiples = (targets, increment) => {
576
+ if (increment <= 0) return;
577
+ for (let position = increment; position < 100; position += increment) {
578
+ const rounded = Math.round(position * TARGET_PRECISION) / TARGET_PRECISION;
579
+ if (rounded < 100) targets.add(rounded);
580
+ }
581
+ };
582
+ const resolveSnapTargets = (config, panelCount) => {
583
+ const targets = /* @__PURE__ */ new Set();
584
+ if (config.step !== void 0 && config.step > 0) addMultiples(targets, config.step);
585
+ if (config.divisions !== void 0) {
586
+ const divisions = config.divisions === "panels" ? panelCount : config.divisions;
587
+ if (divisions >= 2) addMultiples(targets, 100 / divisions);
588
+ }
589
+ return [...targets].toSorted((a, b) => a - b);
590
+ };
591
+ const snapEnabled = (config) => config !== null && ((config.step ?? 0) > 0 || config.divisions === "panels" || typeof config.divisions === "number" && config.divisions >= 2);
592
+ const nearestTarget = (position, targets) => targets.reduce((best, target) => {
593
+ if (best === void 0 || Math.abs(target - position) < Math.abs(best - position)) return target;
594
+ return best;
595
+ }, void 0);
596
+ const snapSizes = (sizes, boundaryIndex, config) => {
597
+ const threshold = config.threshold ?? 4;
598
+ const left = sizes[boundaryIndex];
599
+ const right = sizes[boundaryIndex + 1];
600
+ if (boundaryIndex < 0 || left === void 0 || right === void 0) return {
601
+ sizes,
602
+ snapped: false
603
+ };
604
+ const targets = resolveSnapTargets(config, sizes.length);
605
+ if (targets.length === 0) return {
606
+ sizes,
607
+ snapped: false
608
+ };
609
+ let position = 0;
610
+ for (let index = 0; index <= boundaryIndex; index++) position += sizes[index] ?? 0;
611
+ const target = nearestTarget(position, targets);
612
+ if (target === void 0 || Math.abs(target - position) > threshold) return {
613
+ sizes,
614
+ snapped: false
615
+ };
616
+ const delta = target - position;
617
+ if (left + delta < 0 || right - delta < 0) return {
618
+ sizes,
619
+ snapped: false
620
+ };
621
+ const next = [...sizes];
622
+ next[boundaryIndex] = left + delta;
623
+ next[boundaryIndex + 1] = right - delta;
624
+ return {
625
+ sizes: next,
626
+ snapped: true
627
+ };
628
+ };
629
+
630
+ //#endregion
631
+ //#region src/model/invariants.ts
632
+ const clampSelected = (length, selected) => {
633
+ if (length === 0) return 0;
634
+ const base = Number.isFinite(selected) ? Math.trunc(selected) : 0;
635
+ if (base < 0) return 0;
636
+ if (base > length - 1) return length - 1;
637
+ return base;
638
+ };
639
+ const normalizeTabset = (tabset) => ({
640
+ ...tabset,
641
+ selected: clampSelected(tabset.children.length, tabset.selected)
642
+ });
643
+ /**
644
+ * A row that collapses to a single child hands its slot in the parent to
645
+ * that child, so the slot's sizing constraints have to travel with it.
646
+ * Dropping them silently widens a pinned sidebar, and `normalize` runs on
647
+ * load and on every persist, so the loss is permanent.
648
+ *
649
+ * `snap` only exists on rows, so it is carried only to a row survivor.
650
+ */
651
+ const inheritSlot = (row, survivor) => {
652
+ const sized = {
653
+ ...survivor,
654
+ ...row.weight === void 0 ? {} : { weight: row.weight },
655
+ ...row.min === void 0 ? {} : { min: row.min },
656
+ ...row.max === void 0 ? {} : { max: row.max }
657
+ };
658
+ if (sized.type === "row" && row.snap !== void 0) return {
659
+ ...sized,
660
+ snap: row.snap
661
+ };
662
+ return sized;
663
+ };
664
+ const normalizeRowChildren = (children) => {
665
+ const out = [];
666
+ for (const child of children) {
667
+ if (child.type === "tabset") {
668
+ if (child.children.length > 0) out.push(normalizeTabset(child));
669
+ continue;
670
+ }
671
+ const grandchildren = normalizeRowChildren(child.children);
672
+ if (grandchildren.length === 0) continue;
673
+ if (grandchildren.length === 1) {
674
+ const only = grandchildren[0];
675
+ if (only !== void 0) out.push(inheritSlot(child, only));
676
+ continue;
677
+ }
678
+ out.push({
679
+ ...child,
680
+ children: grandchildren
681
+ });
682
+ }
683
+ return out;
684
+ };
685
+ const rowContainsTabset = (row) => row.children.some((child) => child.type === "tabset" ? true : rowContainsTabset(child));
686
+ const normalizeLayout = (root) => {
687
+ let children = normalizeRowChildren(root.children);
688
+ let orientation = root.orientation;
689
+ let inner = children[0];
690
+ while (children.length === 1 && inner?.type === "row") {
691
+ children = inner.children;
692
+ orientation = inner.orientation;
693
+ inner = children[0];
694
+ }
695
+ return {
696
+ ...root,
697
+ children,
698
+ orientation
699
+ };
700
+ };
701
+ const normalize = (model) => {
702
+ const layout = normalizeLayout(model.layout);
703
+ const healedFloats = [];
704
+ for (const float of model.floats ?? []) {
705
+ const floatLayout = normalizeLayout(float.layout);
706
+ if (rowContainsTabset(floatLayout)) healedFloats.push({
707
+ ...float,
708
+ layout: floatLayout
709
+ });
710
+ }
711
+ const floats = healedFloats.length > 0 ? healedFloats : void 0;
712
+ const withRoots = {
713
+ ...model,
714
+ floats,
715
+ layout
716
+ };
717
+ const tabsetIds = new Set(collectTabsets(withRoots).map((tabset) => tabset.id));
718
+ const firstTabsetId = collectTabsets(withRoots)[0]?.id;
719
+ const activeTabsetId = model.activeTabsetId !== void 0 && tabsetIds.has(model.activeTabsetId) ? model.activeTabsetId : firstTabsetId;
720
+ const maximizedTabsetId = model.maximizedTabsetId !== void 0 && tabsetIds.has(model.maximizedTabsetId) ? model.maximizedTabsetId : void 0;
721
+ return {
722
+ ...withRoots,
723
+ activeTabsetId,
724
+ maximizedTabsetId
725
+ };
726
+ };
727
+
728
+ //#endregion
729
+ //#region src/state/surgery.ts
730
+ const removeTabsetReturning = (row, tabsetId) => {
731
+ const found = findTabsetParent(row, tabsetId);
732
+ if (!found) return;
733
+ const [removed] = found.parent.children.splice(found.index, 1);
734
+ return removed?.type === "tabset" ? removed : void 0;
735
+ };
736
+ const removeTabset = (row, tabsetId) => removeTabsetReturning(row, tabsetId) !== void 0;
737
+ const splitOrientation = (location) => location === "split-left" || location === "split-right" ? "row" : "column";
738
+ const splitsBefore = (location) => location === "split-left" || location === "split-top";
739
+ const placeBesideTarget = (draft, placed, targetId, location) => {
740
+ const targetTabset = findTabset(draft, targetId);
741
+ const found = findTabsetParent(findRootContaining(draft, targetId) ?? draft.layout, targetId);
742
+ if (!targetTabset || !found) return;
743
+ const orientation = splitOrientation(location);
744
+ const before = splitsBefore(location);
745
+ if (found.parent.orientation === orientation) {
746
+ const targetWeight = targetTabset.weight ?? 100;
747
+ targetTabset.weight = targetWeight / 2;
748
+ placed.weight = targetWeight / 2;
749
+ found.parent.children.splice(before ? found.index : found.index + 1, 0, placed);
750
+ } else {
751
+ const targetWeight = targetTabset.weight;
752
+ targetTabset.weight = 50;
753
+ placed.weight = 50;
754
+ const newRow = {
755
+ children: before ? [placed, targetTabset] : [targetTabset, placed],
756
+ id: createNodeId("row"),
757
+ orientation,
758
+ type: "row"
759
+ };
760
+ if (targetWeight !== void 0) newRow.weight = targetWeight;
761
+ found.parent.children.splice(found.index, 1, newRow);
762
+ }
763
+ draft.activeTabsetId = placed.id;
764
+ };
765
+ const insertTab = (draft, tab, target) => {
766
+ const { id: targetId, index, location } = target;
767
+ const targetTabset = findTabset(draft, targetId);
768
+ if (!targetTabset) return;
769
+ if (location === "center") {
770
+ const at = index ?? targetTabset.children.length;
771
+ targetTabset.children.splice(at, 0, tab);
772
+ targetTabset.selected = at;
773
+ return;
774
+ }
775
+ const newTabset = {
776
+ children: [tab],
777
+ id: createNodeId("tabset"),
778
+ selected: 0,
779
+ type: "tabset",
780
+ weight: 50
781
+ };
782
+ placeBesideTarget(draft, newTabset, targetId, location);
783
+ };
784
+ const mergeTabsInto = (target, tabs, selectedOffset) => {
785
+ const mergeStart = target.children.length;
786
+ target.children.push(...tabs);
787
+ target.selected = mergeStart + selectedOffset;
788
+ };
789
+ const wrapTabInLayout = (tab) => ({
790
+ children: [{
791
+ children: [tab],
792
+ id: createNodeId("tabset"),
793
+ selected: 0,
794
+ type: "tabset"
795
+ }],
796
+ id: createNodeId("row"),
797
+ orientation: "row",
798
+ type: "row"
799
+ });
800
+ const wrapTabsetInLayout = (tabset) => ({
801
+ children: [tabset],
802
+ id: createNodeId("row"),
803
+ orientation: "row",
804
+ type: "row"
805
+ });
806
+
807
+ //#endregion
808
+ //#region src/state/floats.ts
809
+ const DEFAULT_FLOAT_GEOMETRY = {
810
+ height: 360,
811
+ left: 80,
812
+ top: 80,
813
+ width: 480
814
+ };
815
+ const uniqueFloatName = (base, floats) => {
816
+ const used = new Set(floats.map((float) => float.name));
817
+ if (!used.has(base)) return base;
818
+ let index = 1;
819
+ while (used.has(`${base} ${index}`)) index += 1;
820
+ return `${base} ${index}`;
821
+ };
822
+ const pushFloat = (draft, layout, geometry, floatId) => {
823
+ const float = {
824
+ geometry: geometry ?? DEFAULT_FLOAT_GEOMETRY,
825
+ id: floatId ?? createNodeId("float"),
826
+ layout,
827
+ name: uniqueFloatName("Panel", draft.floats ?? []),
828
+ type: "float"
829
+ };
830
+ draft.floats = [...draft.floats ?? [], float];
831
+ const tabsets = [];
832
+ collectTabsetsInRow(layout, tabsets);
833
+ const first = tabsets.at(0);
834
+ if (first) draft.activeTabsetId = first.id;
835
+ };
836
+ const floatTabsetById = (draft, tabsetId, geometry, floatId) => {
837
+ const root = findRootContaining(draft, tabsetId);
838
+ if (!root) return;
839
+ const detached = removeTabsetReturning(root, tabsetId);
840
+ if (detached) pushFloat(draft, wrapTabsetInLayout(detached), geometry, floatId);
841
+ };
842
+ const resolveMainTarget = (draft, targetId) => {
843
+ const mainTabsets = [];
844
+ collectTabsetsInRow(draft.layout, mainTabsets);
845
+ if (targetId !== void 0) {
846
+ const explicit = mainTabsets.find((tabset) => tabset.id === targetId);
847
+ if (explicit) return explicit;
848
+ }
849
+ if (draft.activeTabsetId !== void 0) {
850
+ const active = mainTabsets.find((tabset) => tabset.id === draft.activeTabsetId);
851
+ if (active) return active;
852
+ }
853
+ return mainTabsets[0];
854
+ };
855
+ const dockFloat = (draft, floatId, targetId, location) => {
856
+ const floats = draft.floats ?? [];
857
+ const index = floats.findIndex((float) => float.id === floatId);
858
+ if (index === -1) return;
859
+ const float = floats.splice(index, 1).at(0);
860
+ draft.floats = floats;
861
+ if (!float) return;
862
+ const tabsets = [];
863
+ collectTabsetsInRow(float.layout, tabsets);
864
+ const tabs = tabsets.flatMap((tabset) => tabset.children);
865
+ if (tabs.length === 0) return;
866
+ const leadTabset = tabsets.at(0);
867
+ const selectedOffset = leadTabset ? clampSelected(leadTabset.children.length, leadTabset.selected) : 0;
868
+ const target = resolveMainTarget(draft, targetId);
869
+ if (!target) {
870
+ draft.layout = float.layout;
871
+ draft.activeTabsetId = leadTabset?.id;
872
+ return;
873
+ }
874
+ if (location === "center") {
875
+ mergeTabsInto(target, tabs, selectedOffset);
876
+ draft.activeTabsetId = target.id;
877
+ return;
878
+ }
879
+ const where = location ?? "split-right";
880
+ const placed = tabsets.length === 1 && leadTabset ? leadTabset : {
881
+ children: tabs,
882
+ id: createNodeId("tabset"),
883
+ selected: selectedOffset,
884
+ type: "tabset"
885
+ };
886
+ placed.weight = 50;
887
+ placeBesideTarget(draft, placed, target.id, where);
888
+ };
889
+
890
+ //#endregion
891
+ //#region src/state/reducer.ts
892
+ const assertNever = (value) => {
893
+ throw new Error(`Unhandled action: ${JSON.stringify(value)}`);
894
+ };
895
+ const applyAction = (draft, action) => {
896
+ switch (action.type) {
897
+ case "addNode":
898
+ insertTab(draft, action.tab, {
899
+ id: action.targetId,
900
+ index: action.index,
901
+ location: action.location
902
+ });
903
+ return;
904
+ case "adjustSplit": {
905
+ const row = findRow(findRootContaining(draft, action.rowId) ?? draft.layout, action.rowId);
906
+ if (!row) return;
907
+ for (let index = 0; index < action.weights.length; index++) {
908
+ const child = row.children.at(index);
909
+ const weight = action.weights[index];
910
+ if (!child || weight === void 0) continue;
911
+ child.weight = weight;
912
+ }
913
+ return;
914
+ }
915
+ case "deleteTab": {
916
+ const location = findTab(draft, action.tabId);
917
+ if (location) location.container.children.splice(location.index, 1);
918
+ return;
919
+ }
920
+ case "deleteTabset": {
921
+ const root = findRootContaining(draft, action.tabsetId);
922
+ if (root) removeTabset(root, action.tabsetId);
923
+ return;
924
+ }
925
+ case "dockFloat":
926
+ dockFloat(draft, action.floatId, action.targetId, action.location);
927
+ return;
928
+ case "floatTab": {
929
+ const location = findTab(draft, action.tabId);
930
+ if (!location) return;
931
+ const removed = location.container.children.splice(location.index, 1).at(0);
932
+ if (removed) pushFloat(draft, wrapTabInLayout(removed), action.geometry, action.floatId);
933
+ return;
934
+ }
935
+ case "floatTabset":
936
+ floatTabsetById(draft, action.tabsetId, action.geometry, action.floatId);
937
+ return;
938
+ case "moveFloat": {
939
+ const float = findFloat(draft, action.floatId);
940
+ if (float) float.geometry = action.geometry;
941
+ return;
942
+ }
943
+ case "moveNode": {
944
+ const source = findTab(draft, action.sourceId);
945
+ if (!source) return;
946
+ const removed = source.container.children.splice(source.index, 1).at(0);
947
+ if (!removed) return;
948
+ insertTab(draft, removed, {
949
+ id: action.targetId,
950
+ index: action.index,
951
+ location: action.location
952
+ });
953
+ return;
954
+ }
955
+ case "moveTabset": {
956
+ if (action.sourceId === action.targetId) return;
957
+ const source = findTabset(draft, action.sourceId);
958
+ if (!source) return;
959
+ if (action.location === "center") {
960
+ const target = findTabset(draft, action.targetId);
961
+ if (target) {
962
+ mergeTabsInto(target, source.children, clampSelected(source.children.length, source.selected));
963
+ const sourceRoot = findRootContaining(draft, action.sourceId);
964
+ if (sourceRoot) removeTabset(sourceRoot, action.sourceId);
965
+ }
966
+ return;
967
+ }
968
+ if (action.location.startsWith("split-")) {
969
+ const sourceRoot = findRootContaining(draft, action.sourceId);
970
+ const detached = sourceRoot ? removeTabsetReturning(sourceRoot, action.sourceId) : void 0;
971
+ if (detached) placeBesideTarget(draft, detached, action.targetId, action.location);
972
+ }
973
+ return;
974
+ }
975
+ case "renameFloat": {
976
+ const float = findFloat(draft, action.floatId);
977
+ if (float) {
978
+ const others = (draft.floats ?? []).filter((other) => other.id !== action.floatId);
979
+ float.name = uniqueFloatName(action.name, others);
980
+ }
981
+ return;
982
+ }
983
+ case "renameTab": {
984
+ const location = findTab(draft, action.tabId);
985
+ if (location) location.tab.name = action.name;
986
+ return;
987
+ }
988
+ case "selectTab": {
989
+ const tabset = findTabset(draft, action.tabsetId);
990
+ if (tabset) tabset.selected = action.index;
991
+ return;
992
+ }
993
+ case "setActiveTabset":
994
+ if (findTabset(draft, action.tabsetId)) draft.activeTabsetId = action.tabsetId;
995
+ return;
996
+ case "setFloatMinimized": {
997
+ const float = findFloat(draft, action.floatId);
998
+ if (float) float.minimized = action.minimized;
999
+ return;
1000
+ }
1001
+ case "setMaximizedTabset":
1002
+ draft.maximizedTabsetId = action.tabsetId ?? void 0;
1003
+ return;
1004
+ case "updateGlobalAttributes":
1005
+ Object.assign(draft.global, action.attrs);
1006
+ return;
1007
+ case "updateNodeAttributes": {
1008
+ const node = findAttributedNode(draft, action.nodeId);
1009
+ if (node) Object.assign(node, action.attrs);
1010
+ return;
1011
+ }
1012
+ default: assertNever(action);
1013
+ }
1014
+ };
1015
+ const reducer = (model, action) => {
1016
+ const draft = structuredClone(model);
1017
+ applyAction(draft, action);
1018
+ return normalize(draft);
1019
+ };
1020
+
1021
+ //#endregion
1022
+ //#region src/state/history.ts
1023
+ const HISTORY_LIMIT = 100;
1024
+ const createHistory = (present) => ({
1025
+ future: [],
1026
+ past: [],
1027
+ present
1028
+ });
1029
+ const canUndo = (history) => history.past.length > 0;
1030
+ const canRedo = (history) => history.future.length > 0;
1031
+ const dispatch = (history, action) => {
1032
+ const present = reducer(history.present, action);
1033
+ return {
1034
+ future: [],
1035
+ past: [...history.past, history.present].slice(-100),
1036
+ present
1037
+ };
1038
+ };
1039
+ const undo = (history) => {
1040
+ const previous = history.past.at(-1);
1041
+ if (previous === void 0) return history;
1042
+ return {
1043
+ future: [history.present, ...history.future],
1044
+ past: history.past.slice(0, -1),
1045
+ present: previous
1046
+ };
1047
+ };
1048
+ const redo = (history) => {
1049
+ const [next, ...rest] = history.future;
1050
+ if (next === void 0) return history;
1051
+ return {
1052
+ future: rest,
1053
+ past: [...history.past, history.present].slice(-100),
1054
+ present: next
1055
+ };
1056
+ };
1057
+
1058
+ //#endregion
1059
+ //#region src/machines/dashfoo-machine.ts
1060
+ const dashfooMachine = setup({ types: {
1061
+ context: {},
1062
+ events: {},
1063
+ input: {}
1064
+ } }).createMachine({
1065
+ context: ({ input }) => ({ history: createHistory(input.model) }),
1066
+ id: "dashfoo",
1067
+ initial: "ready",
1068
+ states: { ready: { on: {
1069
+ DISPATCH: { actions: assign({ history: ({ context, event }) => dispatch(context.history, event.action) }) },
1070
+ REDO: { actions: assign({ history: ({ context }) => redo(context.history) }) },
1071
+ SET_MODEL: { actions: assign({ history: ({ event }) => createHistory(event.model) }) },
1072
+ UNDO: { actions: assign({ history: ({ context }) => undo(context.history) }) }
1073
+ } } }
1074
+ });
1075
+
1076
+ //#endregion
1077
+ //#region src/machines/drag-dock-machine.ts
1078
+ const requireDrop = (context) => {
1079
+ if (context.subject === null || context.intent === null) throw new Error("drop requires a subject and a drop intent");
1080
+ return {
1081
+ intent: context.intent,
1082
+ subject: context.subject
1083
+ };
1084
+ };
1085
+ const dragDockMachine = setup({
1086
+ guards: { hasValidDrop: ({ context }) => context.subject !== null && context.intent !== null },
1087
+ types: {
1088
+ context: {},
1089
+ emitted: {},
1090
+ events: {}
1091
+ }
1092
+ }).createMachine({
1093
+ context: {
1094
+ intent: null,
1095
+ subject: null
1096
+ },
1097
+ id: "dragDock",
1098
+ initial: "idle",
1099
+ states: {
1100
+ dragging: { on: {
1101
+ CANCEL: { target: "idle" },
1102
+ DROP: [{
1103
+ actions: emit(({ context }) => {
1104
+ const { intent, subject } = requireDrop(context);
1105
+ return {
1106
+ action: subject.kind === "external" ? {
1107
+ index: intent.index,
1108
+ location: intent.location,
1109
+ tab: subject.tab,
1110
+ targetId: intent.targetId,
1111
+ type: "addNode"
1112
+ } : {
1113
+ index: intent.index,
1114
+ location: intent.location,
1115
+ sourceId: subject.id,
1116
+ targetId: intent.targetId,
1117
+ type: subject.kind === "tabset" ? "moveTabset" : "moveNode"
1118
+ },
1119
+ type: "COMMIT"
1120
+ };
1121
+ }),
1122
+ guard: "hasValidDrop",
1123
+ target: "idle"
1124
+ }, { target: "idle" }],
1125
+ OVER: { actions: assign({ intent: ({ event }) => event.intent }) }
1126
+ } },
1127
+ idle: {
1128
+ entry: assign({
1129
+ intent: null,
1130
+ subject: null
1131
+ }),
1132
+ on: { START: {
1133
+ actions: assign({ subject: ({ event }) => event.subject }),
1134
+ target: "dragging"
1135
+ } }
1136
+ }
1137
+ }
1138
+ });
1139
+
1140
+ //#endregion
1141
+ //#region src/model/serialize.ts
1142
+ const parseModel = (value) => {
1143
+ const model = normalize(dashfooSchema.parse(value));
1144
+ const duplicates = findDuplicateIds(model);
1145
+ if (duplicates.length > 0) console.warn(`[dashfoo] duplicate node ids in the loaded layout: ${duplicates.join(", ")}`);
1146
+ return model;
1147
+ };
1148
+ const toJSON = (model) => JSON.stringify(model);
1149
+ const fromJSON = (json) => parseModel(JSON.parse(json));
1150
+
1151
+ //#endregion
1152
+ //#region src/model/stack.ts
1153
+ const stackModel = (model, orientation = "column") => {
1154
+ const mainTabsets = [];
1155
+ collectTabsetsInRow(model.layout, mainTabsets);
1156
+ const children = [];
1157
+ for (const tabset of mainTabsets) children.push({
1158
+ ...tabset,
1159
+ weight: 1
1160
+ });
1161
+ return normalize({
1162
+ ...model,
1163
+ layout: {
1164
+ children,
1165
+ id: model.layout.id,
1166
+ orientation,
1167
+ type: "row"
1168
+ },
1169
+ maximizedTabsetId: void 0
1170
+ });
1171
+ };
1172
+
1173
+ //#endregion
1174
+ export { DEFAULT_SNAP_THRESHOLD, HISTORY_LIMIT, actionSchema, canRedo, canUndo, collectRoots, collectTabsets, collectTabsetsInRow, createHistory, createNodeId, createTabId, dashfooMachine, dashfooSchema, dimensionSchema, dispatch, dockLocationSchema, dockZonePolygons, dragDockMachine, edgeSchema, findAttributedNode, findDuplicateIds, findFloat, findRootContaining, findRow, findTab, findTabset, findTabsetParent, floatNode, floatNodeSchema, fromJSON, geometrySchema, getFirstTabset, globalAttributesSchema, jsonValueSchema, model, mutableNodeAttrsSchema, normalize, orientationSchema, parseModel, redo, reducer, resolveDockTarget, resolveSnapTargets, row, rowNodeSchema, snapEnabled, snapSchema, snapSizes, stackModel, tab, tabNodeSchema, tabset, tabsetNodeSchema, toJSON, undo, unitSchema, zoneRect };
2
1175
  //# sourceMappingURL=index.js.map