@dashfoo/react 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,9 +1,2585 @@
1
- "use client";import{canRedo as e,canUndo as t,dashfooMachine as n,dragDockMachine as r,findTabset as i,fromJSON as a,normalize as o,reducer as s,resolveDockTarget as c,snapEnabled as l,snapSizes as u,stackModel as d,tabNodeSchema as f,toJSON as p,zoneRect as m}from"@dashfoo/core";import{Fragment as h,createContext as g,forwardRef as _,useCallback as v,useContext as y,useEffect as b,useId as x,useImperativeHandle as S,useInsertionEffect as C,useLayoutEffect as w,useMemo as T,useRef as E,useState as D}from"react";import{Accessibility as O,DragDropManager as k,Draggable as A,Feedback as j,PointerSensor as ee}from"@dnd-kit/dom";import{createStore as M,useStore as N}from"zustand";import{useActorRef as te,useSelector as P}from"@xstate/react";import{Fragment as F,jsx as I,jsxs as L}from"react/jsx-runtime";import{Group as R,Panel as z,Separator as ne}from"react-resizable-panels";const B=(e,t)=>{let{target:n}=e;if(!(n instanceof Element))return!1;let r=n.closest(`
1
+ "use client";
2
+
3
+ import { canRedo, canUndo, dashfooMachine, dragDockMachine, findTabset, fromJSON, normalize, reducer, resolveDockTarget, snapEnabled, snapSizes, stackModel, tabNodeSchema, toJSON, zoneRect } from "@dashfoo/core";
4
+ import { Fragment, createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { Accessibility, DragDropManager, Draggable, Feedback, PointerSensor } from "@dnd-kit/dom";
6
+ import { createStore, useStore } from "zustand";
7
+ import { useActorRef, useSelector } from "@xstate/react";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
+ import { Group, Panel as Panel$1, Separator } from "react-resizable-panels";
10
+
11
+ //#region src/hooks/drag-hooks.tsx
12
+ const INTERACTIVE_SELECTOR = `
2
13
  input:not([disabled]),
3
14
  select:not([disabled]),
4
15
  textarea:not([disabled]),
5
16
  button:not([disabled]),
6
17
  a[href],
7
18
  [contenteditable]:not([contenteditable="false"])
8
- `);return r!==null&&r!==t.element},V=()=>new k({plugins:e=>e.filter(e=>e!==O&&e!==j),sensors:()=>[ee.configure({preventActivation:B})]}),re=g(null),H=g(null),ie=()=>M(()=>({intent:null,intentOwner:null,subject:null})),ae=(e,t)=>e===t||e!==null&&t!==null&&e.index===t.index&&e.location===t.location&&e.targetId===t.targetId,oe=(e,t)=>e===t||e!==null&&t!==null&&e.id===t.id&&e.kind===t.kind,se=ie(),U=g(null),ce=()=>{let e=E(null),t=E(null);return{draggableRef:t,elementRef:e,ref:v(n=>{e.current=n,t.current&&(t.current.element=n??void 0)},[])}},le=(e,t,n)=>{let r=y(re),i=y(H),a=r?.manager??i,{draggableRef:o,elementRef:s,ref:c}=ce(),{data:l,id:u}=e;return b(()=>{if(!a||t)return;let e=new A({data:{...l,label:``},id:u},a);return e.element=s.current??void 0,o.current=e,()=>{e.destroy(),o.current=null}},[l,t,o,s,u,a]),b(()=>{let e=o.current;e&&(e.data={...l,label:n})},[l,o,n]),{ref:c}},ue=(e,t=!1,n=``)=>le({data:T(()=>({type:`tab`}),[]),id:e},t,n),de=(e,t=!1,n=``)=>le({data:T(()=>({tabsetId:e,type:`tabset`}),[e]),id:`grip-${e}`},t,n),fe=e=>{let t=y(re);return{ref:v(n=>{t?.registerTabset(e,n)},[t,e])}},pe=({createTab:e,disabled:t=!1,label:n=``})=>{let r=x(),i=E(e);return b(()=>{i.current=e}),le({data:T(()=>({createTab:()=>i.current(),type:`external`}),[]),id:`external-${r}`},t,n)},me=()=>N(y(U)??se,e=>e.subject),he=()=>N(y(U)??se,e=>e.intent),ge=()=>{let e=new Map;return{getItem:t=>e.get(t)??null,removeItem:t=>{e.delete(t)},setItem:(t,n)=>{e.set(t,n)}}},_e={getItem:e=>{if(typeof window>`u`)return null;try{return window.localStorage.getItem(e)}catch{return null}},removeItem:e=>{if(!(typeof window>`u`))try{window.localStorage.removeItem(e)}catch{}},setItem:(e,t)=>{if(!(typeof window>`u`))try{window.localStorage.setItem(e,t)}catch(e){console.warn(`[dashfoo] failed to persist layout`,e)}}},ve=(e,t)=>{let n=E(e);b(()=>{n.current=e});let[r]=D(()=>{if(e===null||t===void 0)return t;let n=e.storage.getItem(e.key);if(n===null)return t;try{return a(n)}catch{return t}});b(()=>{let e=n.current;if(e===null)return;let t=e.storage.getItem(e.key);if(t!==null)try{a(t)}catch(t){console.warn(`[dashfoo] discarding unreadable persisted layout`,t),e.storage.removeItem(e.key)}},[]);let i=E(e?.key);b(()=>{e!==null&&e.key!==i.current&&(console.warn(`[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key — remount (key={persistKey}) to load the new key`),i.current=e.key)},[e]);let o=E(null),s=E(null),c=v(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),s.current!==null&&(s.current.storage.setItem(s.current.key,s.current.value),s.current=null)},[]),l=v(e=>{let t=n.current;t!==null&&(s.current={key:t.key,storage:t.storage,value:p(e)},o.current!==null&&clearTimeout(o.current),o.current=setTimeout(c,t.debounceMs))},[c]);b(()=>{let e=()=>{c()},t=()=>{document.visibilityState===`hidden`&&c()};return window.addEventListener(`pagehide`,e),document.addEventListener(`visibilitychange`,t),()=>{window.removeEventListener(`pagehide`,e),document.removeEventListener(`visibilitychange`,t),c()}},[c]);let u=v(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),s.current=null;let e=n.current;e!==null&&e.storage.removeItem(e.key)},[]);return T(()=>({clear:u,initialModel:r,save:l}),[u,r,l])},ye=()=>typeof window<`u`&&typeof window.matchMedia==`function`,be=(e,t)=>{let{query:n}=e;return n?`maxWidth`in n?t<=n.maxWidth:ye()&&window.matchMedia(n.media).matches:!0},xe=(e,t)=>{let n=e.at(-1);if(!n)throw Error(`useResponsiveModel requires at least one breakpoint.`);return e.find(e=>be(e,t))??n},Se=()=>{let[e,t]=D(1/0),n=E(null),r=v(e=>{if(n.current?.disconnect(),!e)return;let r=new ResizeObserver(e=>{let n=e[0];n&&t(n.contentRect.width)});r.observe(e),n.current=r},[]);return b(()=>()=>{n.current?.disconnect()},[]),[r,e]},Ce=({breakpoints:e})=>{let[t,n]=Se(),[,r]=D(0);b(()=>{if(!ye())return;let t=e.flatMap(e=>e.query&&`media`in e.query?[window.matchMedia(e.query.media)]:[]);if(t.length===0)return;let n=()=>{r(e=>e+1)};for(let e of t)e.addEventListener(`change`,n);return()=>{for(let e of t)e.removeEventListener(`change`,n)}},[e]);let i=xe(e,n),a=i.compact===!0;return{breakpoint:i.id,containerRef:t,draggableTabs:!a,draggableTabsets:!a,isCompact:a,model:i.model,resizableSplits:!a}},we=r=>{let{defaultModel:i,model:a,onAction:c,onActiveTabsetChange:l,onMaximizedTabsetChange:u,onModelChange:d}=r,f=a??i;if(f===void 0)throw Error("useDashfooStore requires either a `model` or a `defaultModel`.");let p=te(n,{input:{model:o(f)}}),m=P(p,e=>e.context.history);b(()=>{a!==void 0&&p.send({model:o(a),type:`SET_MODEL`})},[p,a]);let h=a??m.present,g=v((e,t,n)=>{Object.is(e,t)||(d?.(t,n),t.activeTabsetId!==e.activeTabsetId&&l?.(t.activeTabsetId),t.maximizedTabsetId!==e.maximizedTabsetId&&u?.(t.maximizedTabsetId))},[d,l,u]),_=v(e=>{let t=c?c(e):e;if(!t)return;let n=(()=>{if(a===void 0){let e=p.getSnapshot().context.history.present;return p.send({action:t,type:`DISPATCH`}),{after:p.getSnapshot().context.history.present,before:e}}return{after:s(a,t),before:o(a)}})();g(n.before,n.after,t)},[p,a,g,c]),y=v(()=>{if(a!==void 0)return;let e=p.getSnapshot().context.history.present;p.send({type:`UNDO`});let t=p.getSnapshot().context.history.present;g(e,t)},[p,a,g]),x=v(()=>{if(a!==void 0)return;let e=p.getSnapshot().context.history.present;p.send({type:`REDO`});let t=p.getSnapshot().context.history.present;g(e,t)},[p,a,g]),S=v(e=>{p.send({model:o(e),type:`SET_MODEL`})},[p]);return T(()=>({canRedo:()=>e(p.getSnapshot().context.history),canUndo:()=>t(p.getSnapshot().context.history),dispatch:_,model:h,redo:x,setModel:S,undo:y}),[p,_,h,x,S,y])},Te=({children:e})=>{let[t]=D(V);C(()=>()=>t.destroy(),[t]);let[n]=D(ie);return I(H.Provider,{value:t,children:I(U.Provider,{value:n,children:e})})},Ee=e=>M(()=>e),De=g(null),W=e=>{let t=y(De);if(t===null)throw Error(`dashfoo components must be rendered inside <DashfooLayout> or <Layout.Root>.`);return N(t,e)},Oe=(e,t)=>e.x>=t.x&&e.x<=t.x+t.width&&e.y>=t.y&&e.y<=t.y+t.height,ke=(e,t)=>{let n=e.findIndex(e=>t<e.x+e.width/2);return n===-1?e.length:n},Ae=(e,t,n)=>{let r=t[n],i=t.at(-1),a=(r?.x??(i?i.x+i.width:e.x))-4/2;return{height:e.height,width:4,x:Math.max(e.x,Math.min(a,e.x+e.width-4)),y:e.y}},je=(e,t,n)=>!(e===`grip-${t}`||n.length===1&&n[0]===e),Me=new Set,G=(e,t)=>{Me.has(e)||(Me.add(e),console.warn(`[dashfoo] ${t}`))},Ne={boxSizing:`border-box`,pointerEvents:`none`,position:`fixed`,transition:`var(--dashfoo-dock-transition, left 60ms, top 60ms, width 60ms, height 60ms)`,zIndex:9999},Pe=e=>({...Ne,background:`var(--dashfoo-dock-fill, oklch(0.556 0 0 / 0.18))`,border:`var(--dashfoo-dock-border-width, 1px) solid var(--dashfoo-dock-border, oklch(0.708 0 0 / 0.75))`,borderRadius:`var(--dashfoo-dock-radius, 6px)`,height:e.height,left:e.x,top:e.y,width:e.width}),Fe=e=>({...Pe(e),borderRadius:`var(--dashfoo-dock-line-radius, 2px)`}),Ie=(e,t)=>[...e.querySelectorAll(`[data-dashfoo="tab-item"]`)].flatMap(e=>e.querySelector(`[data-dashfoo="tab"]`)?.dataset.tabId===t?[]:[e.getBoundingClientRect()]),Le=({actorRef:e,getTabsetElement:t})=>{let n=P(e,e=>e.context.intent),r=P(e,e=>e.context.subject?.id);if(!n)return null;let i=t(n.targetId);if(!i)return null;if(n.location===`center`){let e=i.querySelector(`[data-dashfoo="tabstrip"]`);if(e)return I(`div`,{"data-dashfoo":`dock-indicator`,style:Fe(Ae(e.getBoundingClientRect(),Ie(e,r),n.index??0))})}return I(`div`,{"data-dashfoo":`dock-indicator`,style:Pe(m(i.getBoundingClientRect(),n.location))})},Re={left:0,position:`fixed`,top:0,zIndex:9999},ze=({overlayRef:e,preview:t})=>t?I(`div`,{"data-dashfoo":`drag-preview`,ref:e,style:{...Re,transform:`translate(${t.x}px, ${t.y}px)`},children:t.label}):null,Be=(e,t)=>[...e.querySelectorAll(`[data-dashfoo="tab"]`)].flatMap(e=>e.dataset.tabId===t?[]:[e.getBoundingClientRect()]),Ve=(e,t,n,r)=>{let i=t.querySelector(`[data-dashfoo="tabstrip"]`);if(i&&Oe(n,i.getBoundingClientRect()))return{index:ke(Be(i,r),n.x),location:`center`,targetId:e};let a=c(n,t.getBoundingClientRect()),o=a.kind===`tab`?`center`:`split-${a.edge}`;return o===`center`&&i?{index:Be(i,r).length,location:o,targetId:e}:{location:o,targetId:e}},K={x:12,y:8},He=e=>{let t=e?.data?.label;return typeof t==`string`?t:``},Ue=e=>typeof e==`function`,We=e=>{let t=e.data;if(t?.type===`tabset`)return{id:String(t.tabsetId),kind:`tabset`};if(t?.type===`external`){if(!Ue(t.createTab))return console.warn(`[dashfoo] external drag source is missing its createTab function`),null;let n;try{n=t.createTab()}catch(e){return console.warn(`[dashfoo] external drag source createTab threw`,e),null}let r=f.safeParse(n);return r.success?{id:String(e.id),kind:`external`,tab:r.data}:(console.warn(`[dashfoo] external drag source returned an invalid tab`,r.error),null)}return{id:String(e.id),kind:`tab`}},Ge=({children:e,onCommit:t,splitDock:n})=>{let i=te(r),a=y(De),[o]=D(()=>new Map),s=E(null),[c,l]=D(null),u=y(H),[d]=D(()=>u?null:V());C(()=>()=>d?.destroy(),[d]);let f=u??d;if(f===null)throw Error(`[dashfoo] DashfooDragProvider unmounted while its layout is still mounted; keep the provider above the layout or remount the layout`);let p=y(U),[m]=D(()=>p??ie()),h=x();b(()=>{let e=i.subscribe(e=>{let{intent:t,subject:n}=e.context,r=m.getState();t===null?r.intent!==null&&r.intentOwner===h&&m.setState({intent:null,intentOwner:null}):(!ae(r.intent,t)||r.intentOwner!==h)&&m.setState({intent:t,intentOwner:h}),oe(m.getState().subject,n)||m.setState({subject:n})});return()=>{e.unsubscribe()}},[i,h,m]);let g=E(t),_=E(n);b(()=>{g.current=t,_.current=n}),b(()=>{let e=i.on(`COMMIT`,e=>{if(a?.getState().editable===!1)return;let t=g.current??a?.getState().dispatch;if(!t){G(`drag-layer-no-commit`,`Layout.DragLayer has neither an onCommit prop nor a <Layout.Root> above it; drops are ignored`);return}t(e.action)});return()=>{e.unsubscribe()}},[i,a]);let S=v((e,t)=>{t?o.set(e,t):o.delete(e)},[o]),w=v(e=>o.get(e),[o]),O=v(e=>{s.current=e},[]);b(()=>{let e=e=>{let t=document.elementFromPoint(e.x,e.y);if(t){for(let[e,n]of o)if(n.contains(t))return{element:n,id:e}}},t=(e,t,n,r)=>{if(a?.getState().editable===!1||!je(r,e,[...t.querySelectorAll(`[data-dashfoo="tab"]`)].map(e=>e.dataset.tabId??``)))return null;let i=Ve(e,t,n,r);return!(_.current??a?.getState().splitDock??!0)&&i.location.startsWith(`split-`)?{location:`center`,targetId:e}:i},n=e=>{let t=s.current;t&&(t.style.transform=`translate(${e.x+K.x}px, ${e.y+K.y}px)`)},r=e=>{let t=e.operation.source;if(!t)return;let n=We(t);if(!n)return;let r=e.operation.position.current;l({label:He(t),x:r.x+K.x,y:r.y+K.y}),i.send({subject:n,type:`START`})},c=r=>{let a=r.operation,o=a.position.current;n(o);let s=a.source?String(a.source.id):void 0,c=e(o);i.send({intent:c?t(c.id,c.element,o,s):null,type:`OVER`})},u=n=>{if(l(null),n.canceled){i.send({type:`CANCEL`});return}let r=n.operation,a=r.position.current,o=r.source?String(r.source.id):void 0,s=e(a);s&&i.send({intent:t(s.id,s.element,a,o),type:`OVER`}),i.send({type:`DROP`})},d=f.monitor.addEventListener(`dragstart`,r),p=f.monitor.addEventListener(`dragmove`,c),m=f.monitor.addEventListener(`dragend`,u);return()=>{d(),p(),m()}},[i,a,f,o]);let k=T(()=>({manager:f,registerTabset:S}),[f,S]);return I(re.Provider,{value:k,children:L(U.Provider,{value:m,children:[e,I(Le,{actorRef:i,getTabsetElement:w}),I(ze,{overlayRef:O,preview:c})]})})},Ke=g(!1),qe=()=>y(Ke),Je=(e,t,n=e)=>({height:e.height,left:Math.max(0,Math.min(e.left,Math.max(0,t.width-n.width))),top:Math.max(0,Math.min(e.top,Math.max(0,t.height-n.height))),width:e.width}),Ye=e=>{let t=(e.closest(`[data-dashfoo="tabset"]`)??e).getBoundingClientRect();return Je({height:Math.max(120,Math.min(Math.round(t.height),340)),left:Math.round(t.left)+16,top:Math.round(t.top)+16,width:Math.max(200,Math.min(Math.round(t.width),480))},{height:window.innerHeight,width:window.innerWidth})},Xe=(e,t,n,r)=>{let{height:i,left:a,top:o,width:s}=e;return t.x===1?s=Math.max(200,e.width+n):t.x===-1&&(s=Math.max(200,e.width-n),a=Math.max(0,e.left+(e.width-s))),t.y===1?i=Math.max(120,e.height+r):t.y===-1&&(i=Math.max(120,e.height-r),o=Math.max(0,e.top+(e.height-i))),{height:i,left:a,top:o,width:s}},Ze={display:`flex`,height:`100%`,position:`relative`,width:`100%`},Qe=({children:e,closableTabs:t=!0,dispatch:n,draggableTabs:r=!0,draggableTabsets:i=!0,editable:a=!0,floatable:o=!1,keepMounted:s=!1,maximizable:c=!0,model:l,renamableTabs:u=!0,renderTab:d,renderTabLabel:f,renderTabsetToolbar:p,resizableSplits:m=!0,rootRef:h,snap:g,style:_,...v})=>{let y=l.global,b={closableTabs:a&&t&&y.tabEnableClose!==!1,dispatch:n,draggableTabs:a&&r&&y.tabEnableDrag!==!1,draggableTabsets:a&&i,editable:a,floatable:a&&o,keepMounted:s,maximizable:c&&y.tabSetEnableMaximize!==!1,maximizedTabsetId:l.maximizedTabsetId,renamableTabs:a&&u&&y.tabEnableRename!==!1,renderTab:d,renderTabLabel:f,renderTabsetToolbar:p,resizableSplits:a&&m&&y.enableSplitResize!==!1,snap:g??y.snap??null,splitDock:y.enableSplitDock!==!1,tabLocation:y.tabLocation??`top`,tabsetMinSize:y.tabSetMinSize??320,tabStripEnabled:y.tabSetEnableTabStrip!==!1},[x]=D(()=>Ee(b));w(()=>{x.setState(b)});let S=y.splitterSize===void 0?{...Ze,..._}:{...Ze,"--dashfoo-splitter-size":`${y.splitterSize}px`,..._};return I(De.Provider,{value:x,children:I(`div`,{...v,"data-dashfoo":`layout`,ref:h,style:S,children:e})})},q=(e,t)=>`dashfoo-tab-${e}-${t}`,$e=e=>`dashfoo-panel-${e}`,et=(e,t,n)=>e?.querySelector(`#${CSS.escape(q(t,n))}`)??null,tt=e=>M((t,n)=>({...e,cancelRename:()=>t({editingTabId:null}),closeTab:e=>{t({restoreFocus:!0}),n().dispatch({tabId:e,type:`deleteTab`})},commitRename:(e,r)=>{t({editingTabId:null});let{node:i,renameTab:a}=n(),o=r.trim(),s=i.children.find(t=>t.id===e)?.name;o&&o!==s&&a(e,o)},editingTabId:null,overflowItems:[],registerRenameInput:()=>(t(e=>({renameInputCount:e.renameInputCount+1})),()=>t(e=>({renameInputCount:e.renameInputCount-1}))),registerTablist:e=>{if(e){let r=n().tablistElement;r&&r!==e&&G(`duplicate-tablist-${n().node.id}`,`more than one <Tabset.Tablist> under the same <Tabset.Root>; the last one wins for keyboard focus and overflow`),t({tablistElement:e});return}t({tablistElement:null})},renameInputCount:0,renameTab:(e,t)=>n().dispatch({name:t,tabId:e,type:`renameTab`}),restoreFocus:!1,selectOverflowTab:e=>{let{dispatch:t,node:r,tablistElement:i}=n(),a=r.children.findIndex(t=>t.id===e);a!==-1&&(t({index:a,tabsetId:r.id,type:`selectTab`}),et(i,r.id,e)?.scrollIntoView({block:`nearest`,inline:`nearest`}))},selectTab:(e,t)=>{let{dispatch:r,node:i,tablistElement:a}=n();if(r({index:e,tabsetId:i.id,type:`selectTab`}),t?.focus){let t=i.children[e]?.id;t&&et(a,i.id,t)?.focus()}},startEditing:e=>{let{node:r,renameInputCount:i,tabsRenamable:a}=n(),o=r.children.find(t=>t.id===e);if(!(!a||o?.enableRename===!1)){if(i===0){G(`missing-rename-input-${r.id}`,`startEditing needs a <Tabset.RenameInput> in the composition; ignoring`);return}t({editingTabId:e})}},tablistElement:null,toggleMaximize:()=>{let{dispatch:e,isMaximized:t,node:r}=n();e({tabsetId:t?null:r.id,type:`setMaximizedTabset`})}})),nt=g(null),J=e=>{let t=y(nt);if(t===null)throw Error(`dashfoo Tabset parts must be rendered inside <Tabset.Root>.`);return N(t,e)},rt=g(null),Y=()=>{let e=y(rt);if(e===null)throw Error(`dashfoo tab parts must be rendered inside <Tabset.Tab>.`);return e},it={flex:1,minHeight:0,overflow:`auto`},at=({render:e,tab:t})=>{let n=W(e=>e.renderTab);return e?e(t):n(t)},ot=({children:e,style:t,...n})=>{let r=J(e=>e.node),i=J(e=>e.visualSelected),a=W(e=>e.keepMounted),o={...it,...t};if(a&&r.children.length>0)return r.children.map((t,a)=>I(`div`,{...n,"aria-labelledby":q(r.id,t.id),"data-dashfoo":`tabcontent`,hidden:a!==i||void 0,id:a===i?$e(r.id):void 0,role:a===i?`tabpanel`:void 0,style:o,tabIndex:a===i?0:void 0,children:I(at,{render:e,tab:t})},t.id));let s=r.children[i];return s?I(`div`,{...n,"aria-labelledby":q(r.id,s.id),"data-dashfoo":`tabcontent`,id:$e(r.id),role:`tabpanel`,style:o,tabIndex:0,children:I(at,{render:e,tab:s})}):I(`div`,{...n,"data-dashfoo":`tabcontent`,style:o})},st=()=>L(`svg`,{"aria-hidden":`true`,fill:`currentColor`,height:`10`,viewBox:`0 0 10 10`,width:`10`,children:[I(`circle`,{cx:`2`,cy:`5`,r:`1`}),I(`circle`,{cx:`5`,cy:`5`,r:`1`}),I(`circle`,{cx:`8`,cy:`5`,r:`1`})]}),ct={position:`absolute`,right:0,top:`100%`,zIndex:10},lt=({items:e,onSelect:t})=>{let[n,r]=D(!1),[i,a]=D(0),o=E(null),s=E(null),c=E([]);b(()=>{if(!n)return;let e=e=>{e.target instanceof Node&&!o.current?.contains(e.target)&&r(!1)};return document.addEventListener(`pointerdown`,e),()=>{document.removeEventListener(`pointerdown`,e)}},[n]),b(()=>{n&&c.current[0]?.focus()},[n]);let l=()=>{r(!1),s.current?.focus()},u=e=>{a(e),c.current[e]?.focus()},d=()=>{a(0),r(e=>!e)},f=e=>{t(e),l()};return L(`div`,{"data-dashfoo":`tab-overflow-root`,ref:o,style:{position:`relative`},children:[I(`button`,{"aria-expanded":n,"aria-haspopup":`menu`,"aria-label":`More tabs`,"data-dashfoo":`tab-overflow`,onClick:d,ref:s,type:`button`,children:I(st,{})}),n?I(`div`,{"data-dashfoo":`tab-overflow-menu`,onBlur:e=>{e.relatedTarget instanceof Node&&o.current?.contains(e.relatedTarget)||r(!1)},onKeyDown:t=>{let n=e.length-1;switch(t.key){case`ArrowDown`:t.preventDefault(),u(i>=n?0:i+1);break;case`ArrowUp`:t.preventDefault(),u(i<=0?n:i-1);break;case`Home`:t.preventDefault(),u(0);break;case`End`:t.preventDefault(),u(n);break;case`Escape`:t.preventDefault(),l();break;default:break}},role:`menu`,style:ct,tabIndex:-1,children:e.map((e,t)=>I(`button`,{"data-dashfoo":`tab-overflow-item`,onClick:()=>f(e.id),ref:e=>{c.current[t]=e},role:`menuitem`,tabIndex:t===i?0:-1,type:`button`,children:e.name},e.id))}):null]})},ut=()=>{let e=J(e=>e.overflowItems),t=J(e=>e.selectOverflowTab);return e.length===0?null:I(lt,{items:e,onSelect:t})},dt=e=>{let t=e.getBoundingClientRect();return[...e.querySelectorAll(`[data-dashfoo="tab"]`)].flatMap(e=>{let n=e.getBoundingClientRect();return(n.right>t.right+1||n.left<t.left-1)&&e.dataset.tabId?[e.dataset.tabId]:[]})},ft=(e,t)=>{let[n,r]=D([]);return b(()=>{let t=()=>{r(t=>{let n=e?dt(e):[];return n.length===0&&t.length===0?t:n})};if(t(),!e)return;let n=new ResizeObserver(t);return n.observe(e),e.addEventListener(`scroll`,t,{passive:!0}),()=>{n.disconnect(),e.removeEventListener(`scroll`,t)}},[e,t]),n},pt=(e,t)=>t+1<e?t+1:t-1,mt={display:`flex`,flexDirection:`column`,height:`100%`,minHeight:0,minWidth:0,width:`100%`},ht=({children:e,node:t,ref:n,style:r,...i})=>{let a=W(e=>e.closableTabs),o=W(e=>e.dispatch),s=W(e=>e.maximizable),c=W(e=>e.maximizedTabsetId),l=W(e=>e.renamableTabs),u=W(e=>e.tabLocation),{ref:d}=fe(t.id),f=me(),p=E(null),m=c===t.id,h=s&&t.enableMaximize!==!1,g=a&&t.enableClose!==!1,_=l,v=f?.kind===`tab`?t.children.findIndex(e=>e.id===f.id):-1,y=v!==-1&&v===t.selected?pt(t.children.length,v):t.selected,x=t.children[y],S=f?.kind===`tabset`&&f.id===t.id,C={activeTab:x,dispatch:o,isMaximized:m,node:t,showMaximize:h,tabsClosable:g,tabsRenamable:_,visualSelected:y},[O]=D(()=>tt(C));w(()=>{O.setState(C)});let k=ft(N(O,e=>e.tablistElement),t.children.map(e=>`${e.id}:${e.name}`).join(`|`)),A=T(()=>{let e=new Map(t.children.map(e=>[e.id,e.name]));return k.map(t=>({id:t,name:e.get(t)??t}))},[k,t.children]);return w(()=>{O.setState({overflowItems:A})},[A,O]),b(()=>{if(!O.getState().restoreFocus)return;O.setState({restoreFocus:!1});let e=t.children[t.selected]?.id;e?p.current?.querySelector(`#${CSS.escape(q(t.id,e))}`)?.focus():p.current?.focus()},[t.children,t.selected,t.id,O]),I(nt.Provider,{value:O,children:I(`div`,{...i,"data-dashfoo":`tabset`,"data-dragging-source":S||void 0,"data-tab-location":u,ref:e=>{d(e),p.current=e,typeof n==`function`?n(e):n&&(n.current=e)},style:{...mt,...r},tabIndex:-1,children:e})})},gt=(e,t)=>{typeof e==`function`?e(t):e&&(e.current=t)},X=(...e)=>t=>{for(let n of e)gt(n,t)},_t=({children:e,ref:t,tab:n,...r})=>{let i=y(nt),a=J(e=>e.node),o=J(e=>e.editingTabId===n.id),s=me(),c=s?.kind===`tab`&&s.id===n.id,l=E(null),u=E(!1),d=a.children.findIndex(e=>e.id===n.id);b(()=>{i?.getState().node.children.some(e=>e.id===n.id)===!1&&G(`orphan-tab-${n.id}`,`<Tabset.Tab> for "${n.id}" is not a child of its Tabset.Root's node; it renders but can never be selected`)},[d===-1,i,n.id]),b(()=>{u.current&&!o&&l.current?.querySelector(`[data-dashfoo="tab"]`)?.focus(),u.current=o},[o]);let f=T(()=>({index:d,tab:n}),[d,n]),p=T(()=>X(l,t),[t]);return I(rt.Provider,{value:f,children:I(`span`,{...r,"data-dashfoo":`tab-item`,"data-dragging":c||void 0,ref:p,children:e})})},vt=({children:e,onClick:t,onDoubleClick:n,ref:r,...i})=>{let{index:a,tab:o}=Y(),s=J(e=>e.node),c=J(e=>e.editingTabId===o.id),l=J(e=>e.selectTab),u=J(e=>e.startEditing),d=J(e=>e.tabsRenamable),f=J(e=>e.visualSelected),p=W(e=>e.draggableTabs),m=W(e=>e.renderTabLabel),{ref:h}=ue(o.id,!p||o.enableDrag===!1,o.name),g=T(()=>X(h,r),[h,r]);if(c)return null;let _=a===f,v=d&&o.enableRename!==!1,y=e=>{t?.(e),l(a)},b=e=>{n?.(e),v&&u(o.id)};return I(`button`,{"aria-label":m?o.name:void 0,...i,"aria-controls":$e(s.id),"aria-selected":_,"data-dashfoo":`tab`,"data-tab-id":o.id,id:q(s.id,o.id),onClick:y,onDoubleClick:b,ref:g,role:`tab`,tabIndex:_?0:-1,type:`button`,children:e??(m?m(o):o.name)})},yt={pointerEvents:`none`},Z=({children:e})=>I(`svg`,{"aria-hidden":`true`,fill:`none`,height:`12`,stroke:`currentColor`,strokeLinecap:`round`,strokeLinejoin:`round`,strokeWidth:`2`,style:yt,viewBox:`0 0 24 24`,width:`12`,children:e}),bt=()=>L(Z,{children:[I(`path`,{d:`M18 6 6 18`}),I(`path`,{d:`m6 6 12 12`})]}),xt=()=>L(Z,{children:[I(`circle`,{cx:`12`,cy:`5`,r:`1`}),I(`circle`,{cx:`19`,cy:`5`,r:`1`}),I(`circle`,{cx:`5`,cy:`5`,r:`1`}),I(`circle`,{cx:`12`,cy:`12`,r:`1`}),I(`circle`,{cx:`19`,cy:`12`,r:`1`}),I(`circle`,{cx:`5`,cy:`12`,r:`1`}),I(`circle`,{cx:`12`,cy:`19`,r:`1`}),I(`circle`,{cx:`19`,cy:`19`,r:`1`}),I(`circle`,{cx:`5`,cy:`19`,r:`1`})]}),St=({maximized:e})=>e?L(Z,{children:[I(`path`,{d:`m14 10 7-7`}),I(`path`,{d:`M20 10h-6V4`}),I(`path`,{d:`m3 21 7-7`}),I(`path`,{d:`M4 14h6v6`})]}):L(Z,{children:[I(`path`,{d:`M15 3h6v6`}),I(`path`,{d:`m21 3-7 7`}),I(`path`,{d:`m3 21 7-7`}),I(`path`,{d:`M9 21H3v-6`})]}),Ct=()=>L(Z,{children:[I(`rect`,{height:`14`,rx:`2`,width:`18`,x:`3`,y:`6`}),I(`path`,{d:`M3 10h18`})]}),wt=()=>I(Z,{children:I(`path`,{d:`M5 12h14`})}),Tt=()=>L(Z,{children:[I(`path`,{d:`M3 7V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2`}),I(`path`,{d:`M11 8 7 12l4 4`}),I(`path`,{d:`M7 12h10`})]}),Et=({defaultValue:e,onBlur:t,onKeyDown:n,ref:r,...i})=>{let{tab:a}=Y(),o=J(e=>e.cancelRename),s=J(e=>e.commitRename),c=E(null),l=E(!1),u=T(()=>X(c,r),[r]);b(()=>{c.current?.focus(),c.current?.select()},[]);let d=e=>{n?.(e),e.key===`Enter`?(l.current=!0,s(a.id,e.currentTarget.value)):e.key===`Escape`&&(l.current=!0,o())},f=e=>{t?.(e),!l.current&&(l.current=!0,s(a.id,e.currentTarget.value))};return I(`input`,{"aria-label":`Rename ${a.name}`,...i,"data-dashfoo":`tab-rename`,defaultValue:e??a.name,onBlur:f,onKeyDown:d,ref:u,type:`text`})},Dt=e=>{let{tab:t}=Y(),n=J(e=>e.editingTabId===t.id),r=J(e=>e.registerRenameInput);return b(()=>r(),[r]),n?I(Et,{...e}):null},Ot=({children:e,onClick:t,...n})=>{let{index:r,tab:i}=Y(),a=J(e=>e.closeTab),o=J(e=>e.editingTabId===i.id),s=J(e=>e.tabsClosable),c=J(e=>e.visualSelected);if(!(s&&i.enableClose!==!1)||o)return null;let l=e=>{t?.(e),e.stopPropagation(),a(i.id)};return I(`button`,{"aria-label":`Close ${i.name}`,...n,"data-dashfoo":`tab-close`,onClick:l,tabIndex:r===c?0:-1,type:`button`,children:e??I(bt,{})})},kt={display:`flex`,flexShrink:0},At={display:`flex`,minWidth:0,overflowX:`auto`},jt=({style:e,...t})=>I(`div`,{...t,"data-dashfoo":`tabstrip`,style:{...kt,...e}}),Mt=({onKeyDown:e,ref:t,style:n,...r})=>{let i=J(e=>e.node),a=J(e=>e.registerTablist),o=J(e=>e.selectTab),s=t=>{if(e?.(t),t.defaultPrevented||!(t.target instanceof HTMLElement)||!t.target.closest(`[role="tab"]`))return;let n=i.children.length;if(n===0)return;let r={ArrowLeft:(i.selected-1+n)%n,ArrowRight:(i.selected+1)%n,End:n-1,Home:0}[t.key];r!==void 0&&(t.preventDefault(),o(r,{focus:!0}))},c=T(()=>X(a,t),[a,t]);return I(`div`,{"aria-label":i.name??`Tabs`,...r,"aria-orientation":`horizontal`,"data-dashfoo":`tablist`,onKeyDown:s,ref:c,role:`tablist`,style:{...At,...n},tabIndex:-1})},Nt={alignItems:`center`,display:`flex`,flexShrink:0,marginInlineStart:`auto`},Pt=({style:e,...t})=>I(`div`,{...t,"data-dashfoo":`tabset-toolbar`,style:{...Nt,...e}}),Ft=({children:e,ref:t,...n})=>{let r=J(e=>e.activeTab?.name??``),i=J(e=>e.isMaximized),a=J(e=>e.node),o=!W(e=>e.draggableTabsets)||i,{ref:s}=de(a.id,o,r),c=T(()=>X(s,t),[s,t]);return o?null:I(`button`,{"aria-label":`Move tabset`,...n,"data-dashfoo":`tabset-grip`,ref:c,type:`button`,children:e??I(xt,{})})},It=({children:e,onClick:t,...n})=>{let r=J(e=>e.isMaximized),i=J(e=>e.showMaximize),a=J(e=>e.toggleMaximize);if(!i)return null;let o=e=>{t?.(e),a()};return I(`button`,{"aria-label":r?`Restore`:`Maximize`,...n,"aria-pressed":r,"data-dashfoo":`tabset-maximize`,onClick:o,type:`button`,children:e??I(St,{maximized:r})})},Lt=({children:e,onClick:t,...n})=>{let r=W(e=>e.floatable),i=W(e=>e.dispatch),a=J(e=>e.isMaximized),o=J(e=>e.node),s=qe();if(!r||a)return null;if(!s)return G(`float-no-layer`,`Tabset.FloatButton needs a <Layout.FloatLayer> around the layout (DashfooLayout adds one); the float control is hidden`),null;let c=e=>{t?.(e),i({geometry:Ye(e.currentTarget),tabsetId:o.id,type:`floatTabset`})};return I(`button`,{"aria-label":`Float panel`,...n,"data-dashfoo":`tabset-float`,onClick:c,type:`button`,children:e??I(Ct,{})})},Rt=I(ot,{}),zt=()=>{let e=W(e=>e.draggableTabsets),t=W(e=>e.floatable),n=W(e=>e.renderTabsetToolbar),r=W(e=>e.tabLocation),i=W(e=>e.tabStripEnabled),a=J(e=>e.isMaximized),o=J(e=>e.node),s=J(e=>e.overflowItems.length),c=J(e=>e.showMaximize)||n!==void 0||s>0||t&&!a||e&&!a,l=i?L(jt,{children:[I(Mt,{children:o.children.map(e=>L(_t,{tab:e,children:[I(vt,{}),I(Dt,{}),I(Ot,{})]},e.id))}),c?L(Pt,{children:[I(ut,{}),I(Ft,{}),n?.(o),I(Lt,{}),I(It,{})]}):null]}):null;return r===`bottom`?L(F,{children:[Rt,l]}):L(F,{children:[l,Rt]})},Bt=({node:e})=>I(ht,{node:e,children:I(zt,{})}),Vt=e=>`${e.value}${e.unit}`,Ht=e=>e?.unit===`px`?e.value:void 0,Ut={display:`flex`,flex:1,minHeight:0,minWidth:0},Wt=.01,Gt=(e,t)=>e.reduce((e,n)=>(e[n.id]=(n.weight??1)/t*100,e),{}),Kt=(e,t)=>t.map(t=>e[t.id]??0),qt=(e,t)=>t.reduce((t,n,r)=>(t[n.id]=e[r]??0,t),{}),Jt=(e,t)=>{let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?r.every(n=>Math.abs((e[n]??NaN)-t[n])<=Wt):!1},Yt=(e,t,n)=>{let r=Ht(e.min);if(r!==void 0)return r;if(e.min!==void 0)return;if(e.type===`tabset`)return n;let i=[];for(let r of e.children){let e=Yt(r,t,n);if(e===void 0)return;i.push(e)}if(i.length!==0)return e.orientation===t?i.reduce((e,t)=>e+t,0):Math.max(...i)},Xt=({node:e,renderTabset:t})=>{let n=W(e=>e.dispatch),r=W(e=>e.resizableSplits),i=W(e=>e.tabsetMinSize),a=W(e=>e.snap),o=e.snap??a,s=l(o),c=e.orientation===`row`?`horizontal`:`vertical`,d=e.children.reduce((e,t)=>e+(t.weight??1),0),f=T(()=>Gt(e.children,d),[e.children,d]),p=E(!1),m=E(null),g=E(null),_=E(!1),v=E(null),y=E(!1),b=E(null),[x,S]=D(null),C=e=>{e!==b.current&&(b.current=e,S(e))},O=e=>{let t=g.current;t&&(e?t.dataset.dashfooSnapping=`true`:delete t.dataset.dashfooSnapping)};return w(()=>{let e=m.current;if(!e)return;let t=e.getLayout();if(!(Object.keys(t).length===Object.keys(f).length&&Object.keys(f).every(e=>e in t))||Jt(t,f))return;_.current=!0,e.setLayout(f);let n=requestAnimationFrame(()=>{_.current=!1});return()=>{cancelAnimationFrame(n),_.current=!1}},[f]),I(R,{"data-dashfoo":`row`,disabled:!r,elementRef:g,groupRef:m,onLayoutChange:s?t=>{if(y.current||_.current||!r||o===null||!s)return;let n=v.current;if(n===null)return;let{sizes:i,snapped:a}=u(Kt(t,e.children),n,o);if(C(a?n:null),!a){O(!1);return}O(!0);let c=m.current,l=qt(i,e.children);!c||Jt(t,l)||(y.current=!0,c.setLayout(l),y.current=!1)}:void 0,onLayoutChanged:t=>{if(!p.current){p.current=!0;return}if(_.current)return;let i=v.current;if(v.current=null,C(null),O(!1),!r)return;let a=e.children.map(e=>t[e.id]??e.weight??1);i!==null&&o!==null&&s&&(a=u(a,i,o).sizes),a.some((t,n)=>Math.abs(t-(e.children[n]?.weight??1))>Wt)&&n({rowId:e.id,type:`adjustSplit`,weights:a})},orientation:c,style:Ut,children:e.children.map((n,a)=>{let o=(n.weight??1)/d*100,c=n.min?Vt(n.min):void 0;if(c===void 0){let t=Yt(n,e.orientation,i);c=t===void 0?void 0:`${t}px`}let l=n.max?Vt(n.max):void 0;return L(h,{children:[a>0?I(ne,{"data-dashfoo":`splitter`,"data-dashfoo-snapped":x===a-1?`true`:void 0,disabled:!r,onPointerDown:s&&r?()=>{v.current=a-1}:void 0}):null,I(z,{defaultSize:`${o}%`,id:n.id,maxSize:l,minSize:c,children:n.type===`row`?I(Xt,{node:n,renderTabset:t}):t?.(n)??I(Bt,{node:n})})]},n.id)})},e.id)},Q=e=>e.name??`Panel`,Zt={alignItems:`center`,display:`flex`,flexShrink:0,gap:`0.375rem`,touchAction:`none`},Qt={flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`},$t={alignItems:`center`,cursor:`pointer`,display:`inline-flex`,flexShrink:0},en={flex:1,minHeight:0,position:`relative`},tn=[{edges:{x:0,y:-1},key:`n`,style:{cursor:`ns-resize`,height:6,insetInline:10,top:-3}},{edges:{x:0,y:1},key:`s`,style:{bottom:-3,cursor:`ns-resize`,height:6,insetInline:10}},{edges:{x:1,y:0},key:`e`,style:{cursor:`ew-resize`,insetBlock:10,right:-3,width:6}},{edges:{x:-1,y:0},key:`w`,style:{cursor:`ew-resize`,insetBlock:10,left:-3,width:6}},{edges:{x:1,y:-1},key:`ne`,style:{cursor:`nesw-resize`,height:14,right:-4,top:-4,width:14}},{edges:{x:-1,y:-1},key:`nw`,style:{cursor:`nwse-resize`,height:14,left:-4,top:-4,width:14}},{edges:{x:1,y:1},key:`se`,style:{bottom:-4,cursor:`nwse-resize`,height:14,right:-4,width:14}},{edges:{x:-1,y:1},key:`sw`,style:{bottom:-4,cursor:`nesw-resize`,height:14,left:-4,width:14}}],nn=new Map(tn.map(e=>[e.key,e.edges])),rn={height:34,width:168},an=({dispatch:e,node:t,onDone:n})=>{let r=E(null),i=E(!1);b(()=>{r.current?.focus(),r.current?.select()},[]);let a=r=>{let i=r.trim();i&&i!==Q(t)&&e({floatId:t.id,name:i,type:`renameFloat`}),n()};return I(`input`,{"aria-label":`Rename ${Q(t)}`,"data-dashfoo":`float-rename`,defaultValue:Q(t),onBlur:e=>{i.current||(i.current=!0,a(e.currentTarget.value))},onKeyDown:e=>{e.key===`Enter`?(i.current=!0,a(e.currentTarget.value)):e.key===`Escape`&&(i.current=!0,n())},onPointerDown:e=>e.stopPropagation(),ref:r,style:Qt,type:`text`})},on=({dispatch:e,node:t,renamable:n})=>{let[r,i]=D(!1);return n&&r?I(an,{dispatch:e,node:t,onDone:()=>i(!1)}):I(`span`,{"data-dashfoo":`float-title`,onDoubleClick:n?()=>i(!0):void 0,style:Qt,title:n?`Double-click to rename`:void 0,children:Q(t)})},sn=({global:e,node:t,onFocus:n,zIndex:r})=>{let i=W(e=>e.dispatch),a=W(e=>e.closableTabs),o=W(e=>e.draggableTabs),s=W(e=>e.editable),c=W(e=>e.renamableTabs),l=W(e=>e.resizableSplits),u=W(e=>e.keepMounted),d=W(e=>e.renderTab),f=W(e=>e.renderTabLabel),p=W(e=>e.renderTabsetToolbar),m=W(e=>e.snap),h=E(null),g=E(null),_=E(t.geometry),v=E(!1),y=e=>{h.current=e},b=()=>{s&&i({floatId:t.id,minimized:!1,type:`setFloatMinimized`})},x=e=>{let n=h.current;if(!n||!s)return;v.current=!1;let r=e.currentTarget.dataset.edge,i=n.offsetParent;g.current={bounds:{height:i?.clientHeight??0,width:i?.clientWidth??0},edges:r?nn.get(r)??null:null,pointerId:e.pointerId,start:t.geometry,startX:e.clientX,startY:e.clientY},_.current=t.geometry},S=e=>{let n=g.current,r=h.current;if(!n||!r||e.pointerId!==n.pointerId)return;let i=e.clientX-n.startX,a=e.clientY-n.startY;if(!v.current){if(Math.hypot(i,a)<=4)return;v.current=!0,r.setPointerCapture(n.pointerId)}let o=n.edges?Xe(n.start,n.edges,i,a):Je({...n.start,left:n.start.left+i,top:n.start.top+a},n.bounds,t.minimized?rn:void 0);_.current=o,r.style.left=`${o.left}px`,r.style.top=`${o.top}px`,t.minimized||(r.style.width=`${o.width}px`,r.style.height=`${o.height}px`)},C=e=>{let n=g.current;if(!(!n||e.pointerId!==n.pointerId)){if(g.current=null,!v.current){t.minimized&&b();return}h.current?.releasePointerCapture(e.pointerId),i({floatId:t.id,geometry:_.current,type:`moveFloat`})}};if(t.minimized)return L(`button`,{"aria-label":`Restore ${Q(t)} panel`,"data-dashfoo":`float-chip`,onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),b())},onPointerDown:x,onPointerDownCapture:n,onPointerMove:S,onPointerUp:C,ref:y,style:{left:t.geometry.left,pointerEvents:`auto`,position:`absolute`,top:t.geometry.top,zIndex:r},title:Q(t),type:`button`,children:[I(Ct,{}),I(`span`,{"data-dashfoo":`float-chip-label`,children:Q(t)})]});let w={global:e,layout:t.layout,version:1};return L(`div`,{"data-dashfoo":`float`,onPointerDownCapture:n,onPointerMove:S,onPointerUp:C,ref:y,style:{height:t.geometry.height,left:t.geometry.left,pointerEvents:`auto`,position:`absolute`,top:t.geometry.top,width:t.geometry.width,zIndex:r},children:[L(`div`,{"data-dashfoo":`float-titlebar`,onPointerDown:x,style:s?Zt:{...Zt,cursor:`default`},children:[I(`span`,{"aria-hidden":`true`,"data-dashfoo":`float-grip`,children:I(xt,{})}),I(on,{dispatch:i,node:t,renamable:s}),s&&L(F,{children:[I(`button`,{"aria-label":`Minimize panel`,"data-dashfoo":`float-minimize`,onClick:()=>i({floatId:t.id,minimized:!0,type:`setFloatMinimized`}),onPointerDown:e=>e.stopPropagation(),style:$t,type:`button`,children:I(wt,{})}),I(`button`,{"aria-label":`Dock panel back into the main layout`,"data-dashfoo":`float-dock`,onClick:()=>i({floatId:t.id,type:`dockFloat`}),onPointerDown:e=>e.stopPropagation(),style:$t,type:`button`,children:I(Tt,{})})]})]}),I(`div`,{"data-dashfoo":`float-body`,style:en,children:I(Qe,{closableTabs:a,dispatch:i,draggableTabs:o,draggableTabsets:!1,editable:s,floatable:!1,keepMounted:u,maximizable:!1,model:w,renamableTabs:c,renderTab:d,renderTabLabel:f,renderTabsetToolbar:p,resizableSplits:l,snap:m??void 0,children:I(Ge,{children:I(Xt,{node:t.layout})})})}),s&&tn.map(e=>I(`div`,{"data-dashfoo":`float-resize`,"data-edge":e.key,onPointerDown:x,style:{position:`absolute`,touchAction:`none`,zIndex:1,...e.style}},e.key))]})},cn={inset:0,pointerEvents:`none`,position:`fixed`},ln=({floats:e,global:t})=>{let[n,r]=D(null);return e.length===0?null:I(`div`,{"data-dashfoo":`float-overlay`,style:cn,children:e.map(e=>I(sn,{global:t,node:e,onFocus:()=>r(e.id),zIndex:e.id===n?2:1},e.id))})},$={DragLayer:Ge,FloatLayer:({children:e,floats:t,global:n})=>L(Ke.Provider,{value:!0,children:[e,I(ln,{floats:t,global:n})]}),Root:Qe,Rows:Xt,Tabset:Bt},un=(e,t)=>e===void 0||!t?null:typeof e==`string`?{debounceMs:300,key:e,storage:_e}:{debounceMs:e.debounceMs??300,key:e.key,storage:e.storage??_e},dn=new Set,fn=_((e,t)=>{let{closableTabs:n=!0,components:r,defaultModel:a,draggableTabs:o=!0,draggableTabsets:s=!0,editable:c=!0,factory:l,floatable:u=!1,keepMounted:f=!1,maximizable:p=!0,model:m,onAction:h,onActiveTabsetChange:g,onMaximizedTabsetChange:_,onModelChange:b,persist:x,renamableTabs:C=!0,renderTabLabel:w,renderTabsetToolbar:D,resizableSplits:O=!0,responsive:k,snap:A}=e,j=ve(T(()=>un(x,a!==void 0),[x,a]),a),ee=v((e,t)=>{j.save(e),b?.(e,t)},[b,j]),M=we({defaultModel:j.initialModel??a,model:m,onAction:h,onActiveTabsetChange:g,onMaximizedTabsetChange:_,onModelChange:ee}),N=y(H)!==null;S(t,()=>({addTab:(e,t)=>M.dispatch({index:t.index,location:t.location??`center`,tab:e,targetId:t.targetId,type:`addNode`}),canRedo:M.canRedo,canUndo:M.canUndo,closeTab:e=>M.dispatch({tabId:e,type:`deleteTab`}),dispatch:M.dispatch,dockFloat:e=>M.dispatch({floatId:e,type:`dockFloat`}),floatTab:e=>M.dispatch({tabId:e,type:`floatTab`}),getModel:()=>M.model,maximizeTabset:e=>M.dispatch({tabsetId:e,type:`setMaximizedTabset`}),redo:M.redo,renameTab:(e,t)=>M.dispatch({name:t,tabId:e,type:`renameTab`}),resetLayout:()=>{j.clear(),a!==void 0&&M.setModel(a)},selectTab:(e,t)=>M.dispatch({index:t,tabsetId:e,type:`selectTab`}),undo:M.undo}),[a,j,M]);let te=v(e=>{if(l)return l(e);let t=r?.[e.component];return t?I(t,{node:e}):(r&&!dn.has(e.component)&&(dn.add(e.component),console.warn(`[dashfoo] no component registered for "${e.component}"`)),null)},[r,l]),[P,F]=Se(),L=k!==void 0&&F<=k.maxWidth,R=T(()=>L?d(M.model,k?.orientation):M.model,[L,k?.orientation,M.model]),z=E(L);z.current=L;let ne=v(e=>{z.current&&e.type===`adjustSplit`||M.dispatch(e)},[M]),B=R.maximizedTabsetId?i(R,R.maximizedTabsetId):void 0,V=I($.FloatLayer,{floats:M.model.floats??[],global:M.model.global,children:I($.DragLayer,{children:B?I($.Tabset,{node:B}):I($.Rows,{node:R.layout})})});return I($.Root,{closableTabs:n,dispatch:ne,draggableTabs:L?!1:o,draggableTabsets:L?!1:s,editable:c,floatable:L?!1:u,keepMounted:f,maximizable:p,model:R,renamableTabs:C,renderTab:te,renderTabLabel:w,renderTabsetToolbar:D,resizableSplits:L?!1:O,rootRef:P,snap:A,children:N?V:I(Te,{children:V})})});fn.displayName=`DashfooLayout`;const pn={Badge:e=>I(`span`,{...e,"data-dashfoo":`panel-badge`}),Body:e=>I(`div`,{...e,"data-dashfoo":`panel-body`}),Header:e=>I(`div`,{...e,"data-dashfoo":`panel-header`}),Icon:e=>I(`span`,{...e,"data-dashfoo":`panel-icon`}),Root:e=>I(`div`,{...e,"data-dashfoo":`panel`}),Title:e=>I(`span`,{...e,"data-dashfoo":`panel-title`})},mn={CloseButton:Ot,Content:ot,FloatButton:Lt,Grip:Ft,MaximizeButton:It,OverflowMenu:ut,RenameInput:Dt,Root:ht,Tab:_t,Tablist:Mt,TabStrip:jt,Toolbar:Pt,Trigger:vt};export{Te as DashfooDragProvider,fn as DashfooLayout,$ as Layout,pn as Panel,mn as Tabset,xe as activeBreakpoint,_e as localStorageAdapter,be as matchBreakpoint,ge as memoryStorageAdapter,Se as useContainerWidth,we as useDashfooStore,me as useDragSubject,he as useDropIntent,pe as useExternalTabSource,W as useLayout,ve as usePersistence,Ce as useResponsiveModel,Y as useTab,J as useTabset};
19
+ `;
20
+ const preventActivation = (event, source) => {
21
+ const { target } = event;
22
+ if (!(target instanceof Element)) return false;
23
+ const interactive = target.closest(INTERACTIVE_SELECTOR);
24
+ return interactive !== null && interactive !== source.element;
25
+ };
26
+ const createDragManager = () => new DragDropManager({
27
+ plugins: (defaults) => defaults.filter((plugin) => plugin !== Accessibility && plugin !== Feedback),
28
+ sensors: () => [PointerSensor.configure({ preventActivation })]
29
+ });
30
+ const DragContext = createContext(null);
31
+ const SharedDragManagerContext = createContext(null);
32
+ const createDragSubjectStore = () => createStore(() => ({
33
+ intent: null,
34
+ intentOwner: null,
35
+ subject: null
36
+ }));
37
+ const sameDropIntent = (a, b) => a === b || a !== null && b !== null && a.index === b.index && a.location === b.location && a.targetId === b.targetId;
38
+ const sameDragSubject = (a, b) => a === b || a !== null && b !== null && a.id === b.id && a.kind === b.kind;
39
+ const nullSubjectStore = createDragSubjectStore();
40
+ const DragSubjectStoreContext = createContext(null);
41
+ const useDraggableHandle = () => {
42
+ const elementRef = useRef(null);
43
+ const draggableRef = useRef(null);
44
+ return {
45
+ draggableRef,
46
+ elementRef,
47
+ ref: useCallback((element) => {
48
+ elementRef.current = element;
49
+ if (draggableRef.current) draggableRef.current.element = element ?? void 0;
50
+ }, [])
51
+ };
52
+ };
53
+ const useDraggableEntity = (descriptor, disabled, label) => {
54
+ const context = useContext(DragContext);
55
+ const sharedManager = useContext(SharedDragManagerContext);
56
+ const manager = context?.manager ?? sharedManager;
57
+ const { draggableRef, elementRef, ref } = useDraggableHandle();
58
+ const { data, id } = descriptor;
59
+ useEffect(() => {
60
+ if (!manager || disabled) return;
61
+ const draggable = new Draggable({
62
+ data: {
63
+ ...data,
64
+ label: ""
65
+ },
66
+ id
67
+ }, manager);
68
+ draggable.element = elementRef.current ?? void 0;
69
+ draggableRef.current = draggable;
70
+ return () => {
71
+ draggable.destroy();
72
+ draggableRef.current = null;
73
+ };
74
+ }, [
75
+ data,
76
+ disabled,
77
+ draggableRef,
78
+ elementRef,
79
+ id,
80
+ manager
81
+ ]);
82
+ useEffect(() => {
83
+ const draggable = draggableRef.current;
84
+ if (draggable) draggable.data = {
85
+ ...data,
86
+ label
87
+ };
88
+ }, [
89
+ data,
90
+ draggableRef,
91
+ label
92
+ ]);
93
+ return { ref };
94
+ };
95
+ const useTabDraggable = (tabId, disabled = false, label = "") => {
96
+ return useDraggableEntity({
97
+ data: useMemo(() => ({ type: "tab" }), []),
98
+ id: tabId
99
+ }, disabled, label);
100
+ };
101
+ const useTabsetDraggable = (tabsetId, disabled = false, label = "") => {
102
+ return useDraggableEntity({
103
+ data: useMemo(() => ({
104
+ tabsetId,
105
+ type: "tabset"
106
+ }), [tabsetId]),
107
+ id: `grip-${tabsetId}`
108
+ }, disabled, label);
109
+ };
110
+ const useTabsetDroppable = (tabsetId) => {
111
+ const context = useContext(DragContext);
112
+ return { ref: useCallback((element) => {
113
+ context?.registerTabset(tabsetId, element);
114
+ }, [context, tabsetId]) };
115
+ };
116
+ const useExternalTabSource = ({ createTab, disabled = false, label = "" }) => {
117
+ const id = useId();
118
+ const createTabRef = useRef(createTab);
119
+ useEffect(() => {
120
+ createTabRef.current = createTab;
121
+ });
122
+ return useDraggableEntity({
123
+ data: useMemo(() => ({
124
+ createTab: () => createTabRef.current(),
125
+ type: "external"
126
+ }), []),
127
+ id: `external-${id}`
128
+ }, disabled, label);
129
+ };
130
+ const useDragSubject = () => {
131
+ return useStore(useContext(DragSubjectStoreContext) ?? nullSubjectStore, (state) => state.subject);
132
+ };
133
+ const useDropIntent = () => {
134
+ return useStore(useContext(DragSubjectStoreContext) ?? nullSubjectStore, (state) => state.intent);
135
+ };
136
+
137
+ //#endregion
138
+ //#region src/hooks/persistence.ts
139
+ const memoryStorageAdapter = () => {
140
+ const store = /* @__PURE__ */ new Map();
141
+ return {
142
+ getItem: (key) => store.get(key) ?? null,
143
+ removeItem: (key) => {
144
+ store.delete(key);
145
+ },
146
+ setItem: (key, value) => {
147
+ store.set(key, value);
148
+ }
149
+ };
150
+ };
151
+ const localStorageAdapter = {
152
+ getItem: (key) => {
153
+ if (typeof window === "undefined") return null;
154
+ try {
155
+ return window.localStorage.getItem(key);
156
+ } catch {
157
+ return null;
158
+ }
159
+ },
160
+ removeItem: (key) => {
161
+ if (typeof window === "undefined") return;
162
+ try {
163
+ window.localStorage.removeItem(key);
164
+ } catch {}
165
+ },
166
+ setItem: (key, value) => {
167
+ if (typeof window === "undefined") return;
168
+ try {
169
+ window.localStorage.setItem(key, value);
170
+ } catch (error) {
171
+ console.warn("[dashfoo] failed to persist layout", error);
172
+ }
173
+ }
174
+ };
175
+ const usePersistence = (config, defaultModel) => {
176
+ const configRef = useRef(config);
177
+ useEffect(() => {
178
+ configRef.current = config;
179
+ });
180
+ const [initialModel] = useState(() => {
181
+ if (config === null || defaultModel === void 0) return defaultModel;
182
+ const raw = config.storage.getItem(config.key);
183
+ if (raw === null) return defaultModel;
184
+ try {
185
+ return fromJSON(raw);
186
+ } catch {
187
+ return defaultModel;
188
+ }
189
+ });
190
+ useEffect(() => {
191
+ const current = configRef.current;
192
+ if (current === null) return;
193
+ const raw = current.storage.getItem(current.key);
194
+ if (raw === null) return;
195
+ try {
196
+ fromJSON(raw);
197
+ } catch (error) {
198
+ console.warn("[dashfoo] discarding unreadable persisted layout", error);
199
+ current.storage.removeItem(current.key);
200
+ }
201
+ }, []);
202
+ const loadedKey = useRef(config?.key);
203
+ useEffect(() => {
204
+ if (config !== null && config.key !== loadedKey.current) {
205
+ console.warn("[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key — remount (key={persistKey}) to load the new key");
206
+ loadedKey.current = config.key;
207
+ }
208
+ }, [config]);
209
+ const timer = useRef(null);
210
+ const pending = useRef(null);
211
+ const flush = useCallback(() => {
212
+ if (timer.current !== null) {
213
+ clearTimeout(timer.current);
214
+ timer.current = null;
215
+ }
216
+ if (pending.current !== null) {
217
+ pending.current.storage.setItem(pending.current.key, pending.current.value);
218
+ pending.current = null;
219
+ }
220
+ }, []);
221
+ const save = useCallback((next) => {
222
+ const current = configRef.current;
223
+ if (current === null) return;
224
+ pending.current = {
225
+ key: current.key,
226
+ storage: current.storage,
227
+ value: toJSON(next)
228
+ };
229
+ if (timer.current !== null) clearTimeout(timer.current);
230
+ timer.current = setTimeout(flush, current.debounceMs);
231
+ }, [flush]);
232
+ useEffect(() => {
233
+ const handlePageHide = () => {
234
+ flush();
235
+ };
236
+ const handleVisibilityChange = () => {
237
+ if (document.visibilityState === "hidden") flush();
238
+ };
239
+ window.addEventListener("pagehide", handlePageHide);
240
+ document.addEventListener("visibilitychange", handleVisibilityChange);
241
+ return () => {
242
+ window.removeEventListener("pagehide", handlePageHide);
243
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
244
+ flush();
245
+ };
246
+ }, [flush]);
247
+ const clear = useCallback(() => {
248
+ if (timer.current !== null) {
249
+ clearTimeout(timer.current);
250
+ timer.current = null;
251
+ }
252
+ pending.current = null;
253
+ const current = configRef.current;
254
+ if (current !== null) current.storage.removeItem(current.key);
255
+ }, []);
256
+ return useMemo(() => ({
257
+ clear,
258
+ initialModel,
259
+ save
260
+ }), [
261
+ clear,
262
+ initialModel,
263
+ save
264
+ ]);
265
+ };
266
+
267
+ //#endregion
268
+ //#region src/hooks/responsive.ts
269
+ const hasMatchMedia = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
270
+ const matchBreakpoint = (breakpoint, width) => {
271
+ const { query } = breakpoint;
272
+ if (!query) return true;
273
+ if ("maxWidth" in query) return width <= query.maxWidth;
274
+ return hasMatchMedia() && window.matchMedia(query.media).matches;
275
+ };
276
+ const activeBreakpoint = (breakpoints, width) => {
277
+ const fallback = breakpoints.at(-1);
278
+ if (!fallback) throw new Error("useResponsiveModel requires at least one breakpoint.");
279
+ return breakpoints.find((breakpoint) => matchBreakpoint(breakpoint, width)) ?? fallback;
280
+ };
281
+ const useContainerWidth = () => {
282
+ const [width, setWidth] = useState(Number.POSITIVE_INFINITY);
283
+ const observerRef = useRef(null);
284
+ const containerRef = useCallback((element) => {
285
+ observerRef.current?.disconnect();
286
+ if (!element) return;
287
+ const observer = new ResizeObserver((entries) => {
288
+ const entry = entries[0];
289
+ if (entry) setWidth(entry.contentRect.width);
290
+ });
291
+ observer.observe(element);
292
+ observerRef.current = observer;
293
+ }, []);
294
+ useEffect(() => () => {
295
+ observerRef.current?.disconnect();
296
+ }, []);
297
+ return [containerRef, width];
298
+ };
299
+ const useResponsiveModel = ({ breakpoints }) => {
300
+ const [containerRef, width] = useContainerWidth();
301
+ const [, setMediaTick] = useState(0);
302
+ useEffect(() => {
303
+ if (!hasMatchMedia()) return;
304
+ const lists = breakpoints.flatMap((breakpoint) => breakpoint.query && "media" in breakpoint.query ? [window.matchMedia(breakpoint.query.media)] : []);
305
+ if (lists.length === 0) return;
306
+ const handleChange = () => {
307
+ setMediaTick((tick) => tick + 1);
308
+ };
309
+ for (const list of lists) list.addEventListener("change", handleChange);
310
+ return () => {
311
+ for (const list of lists) list.removeEventListener("change", handleChange);
312
+ };
313
+ }, [breakpoints]);
314
+ const active = activeBreakpoint(breakpoints, width);
315
+ const isCompact = active.compact === true;
316
+ return {
317
+ breakpoint: active.id,
318
+ containerRef,
319
+ draggableTabs: !isCompact,
320
+ draggableTabsets: !isCompact,
321
+ isCompact,
322
+ model: active.model,
323
+ resizableSplits: !isCompact
324
+ };
325
+ };
326
+
327
+ //#endregion
328
+ //#region src/hooks/store.ts
329
+ const useDashfooStore = (options) => {
330
+ const { defaultModel, model: controlledModel, onAction, onActiveTabsetChange, onMaximizedTabsetChange, onModelChange } = options;
331
+ const initialModel = controlledModel ?? defaultModel;
332
+ if (initialModel === void 0) throw new Error("useDashfooStore requires either a `model` or a `defaultModel`.");
333
+ const actorRef = useActorRef(dashfooMachine, { input: { model: normalize(initialModel) } });
334
+ const history = useSelector(actorRef, (snapshot) => snapshot.context.history);
335
+ useEffect(() => {
336
+ if (controlledModel !== void 0) actorRef.send({
337
+ model: normalize(controlledModel),
338
+ type: "SET_MODEL"
339
+ });
340
+ }, [actorRef, controlledModel]);
341
+ const model = controlledModel ?? history.present;
342
+ const notify = useCallback((before, after, action) => {
343
+ if (Object.is(before, after)) return;
344
+ onModelChange?.(after, action);
345
+ if (after.activeTabsetId !== before.activeTabsetId) onActiveTabsetChange?.(after.activeTabsetId);
346
+ if (after.maximizedTabsetId !== before.maximizedTabsetId) onMaximizedTabsetChange?.(after.maximizedTabsetId);
347
+ }, [
348
+ onModelChange,
349
+ onActiveTabsetChange,
350
+ onMaximizedTabsetChange
351
+ ]);
352
+ const dispatch = useCallback((action) => {
353
+ const resolved = onAction ? onAction(action) : action;
354
+ if (!resolved) return;
355
+ const transition = (() => {
356
+ if (controlledModel === void 0) {
357
+ const before = actorRef.getSnapshot().context.history.present;
358
+ actorRef.send({
359
+ action: resolved,
360
+ type: "DISPATCH"
361
+ });
362
+ return {
363
+ after: actorRef.getSnapshot().context.history.present,
364
+ before
365
+ };
366
+ }
367
+ return {
368
+ after: reducer(controlledModel, resolved),
369
+ before: normalize(controlledModel)
370
+ };
371
+ })();
372
+ notify(transition.before, transition.after, resolved);
373
+ }, [
374
+ actorRef,
375
+ controlledModel,
376
+ notify,
377
+ onAction
378
+ ]);
379
+ const undo = useCallback(() => {
380
+ if (controlledModel !== void 0) return;
381
+ const before = actorRef.getSnapshot().context.history.present;
382
+ actorRef.send({ type: "UNDO" });
383
+ const after = actorRef.getSnapshot().context.history.present;
384
+ notify(before, after);
385
+ }, [
386
+ actorRef,
387
+ controlledModel,
388
+ notify
389
+ ]);
390
+ const redo = useCallback(() => {
391
+ if (controlledModel !== void 0) return;
392
+ const before = actorRef.getSnapshot().context.history.present;
393
+ actorRef.send({ type: "REDO" });
394
+ const after = actorRef.getSnapshot().context.history.present;
395
+ notify(before, after);
396
+ }, [
397
+ actorRef,
398
+ controlledModel,
399
+ notify
400
+ ]);
401
+ const setModel = useCallback((next) => {
402
+ actorRef.send({
403
+ model: normalize(next),
404
+ type: "SET_MODEL"
405
+ });
406
+ }, [actorRef]);
407
+ return useMemo(() => ({
408
+ canRedo: () => canRedo(actorRef.getSnapshot().context.history),
409
+ canUndo: () => canUndo(actorRef.getSnapshot().context.history),
410
+ dispatch,
411
+ model,
412
+ redo,
413
+ setModel,
414
+ undo
415
+ }), [
416
+ actorRef,
417
+ dispatch,
418
+ model,
419
+ redo,
420
+ setModel,
421
+ undo
422
+ ]);
423
+ };
424
+
425
+ //#endregion
426
+ //#region src/components/drag-root.tsx
427
+ const DashfooDragProvider = ({ children }) => {
428
+ const [manager] = useState(createDragManager);
429
+ useInsertionEffect(() => () => manager.destroy(), [manager]);
430
+ const [subjectStore] = useState(createDragSubjectStore);
431
+ return /* @__PURE__ */ jsx(SharedDragManagerContext.Provider, {
432
+ value: manager,
433
+ children: /* @__PURE__ */ jsx(DragSubjectStoreContext.Provider, {
434
+ value: subjectStore,
435
+ children
436
+ })
437
+ });
438
+ };
439
+
440
+ //#endregion
441
+ //#region src/hooks/layout-store.ts
442
+ const createLayoutStore = (initial) => createStore(() => initial);
443
+ const LayoutStoreContext = createContext(null);
444
+ const useLayout = (selector) => {
445
+ const store = useContext(LayoutStoreContext);
446
+ if (store === null) throw new Error("dashfoo components must be rendered inside <DashfooLayout> or <Layout.Root>.");
447
+ return useStore(store, selector);
448
+ };
449
+
450
+ //#endregion
451
+ //#region src/lib/tab-insertion.ts
452
+ const pointInRect = (point, rect) => point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
453
+ const insertionIndex = (rects, pointerX) => {
454
+ const found = rects.findIndex((rect) => pointerX < rect.x + rect.width / 2);
455
+ return found === -1 ? rects.length : found;
456
+ };
457
+ const INSERTION_LINE_WIDTH = 4;
458
+ const insertionLineRect = (stripRect, itemRects, index) => {
459
+ const at = itemRects[index];
460
+ const last = itemRects.at(-1);
461
+ const left = (at?.x ?? (last ? last.x + last.width : stripRect.x)) - INSERTION_LINE_WIDTH / 2;
462
+ return {
463
+ height: stripRect.height,
464
+ width: INSERTION_LINE_WIDTH,
465
+ x: Math.max(stripRect.x, Math.min(left, stripRect.x + stripRect.width - INSERTION_LINE_WIDTH)),
466
+ y: stripRect.y
467
+ };
468
+ };
469
+ const shouldAllowDrop = (draggedId, targetId, tabIds) => {
470
+ if (draggedId === `grip-${targetId}`) return false;
471
+ if (tabIds.length === 1 && tabIds[0] === draggedId) return false;
472
+ return true;
473
+ };
474
+
475
+ //#endregion
476
+ //#region src/lib/warn-once.ts
477
+ const warned = /* @__PURE__ */ new Set();
478
+ const warnOnce = (key, message) => {
479
+ if (warned.has(key)) return;
480
+ warned.add(key);
481
+ console.warn(`[dashfoo] ${message}`);
482
+ };
483
+
484
+ //#endregion
485
+ //#region src/components/drag-overlays.tsx
486
+ const overlayBase = {
487
+ boxSizing: "border-box",
488
+ pointerEvents: "none",
489
+ position: "fixed",
490
+ transition: "var(--dashfoo-dock-transition, left 60ms, top 60ms, width 60ms, height 60ms)",
491
+ zIndex: 9999
492
+ };
493
+ const paneStyle = (zone) => ({
494
+ ...overlayBase,
495
+ background: "var(--dashfoo-dock-fill, oklch(0.556 0 0 / 0.18))",
496
+ border: "var(--dashfoo-dock-border-width, 1px) solid var(--dashfoo-dock-border, oklch(0.708 0 0 / 0.75))",
497
+ borderRadius: "var(--dashfoo-dock-radius, 6px)",
498
+ height: zone.height,
499
+ left: zone.x,
500
+ top: zone.y,
501
+ width: zone.width
502
+ });
503
+ const lineStyle = (zone) => ({
504
+ ...paneStyle(zone),
505
+ borderRadius: "var(--dashfoo-dock-line-radius, 2px)"
506
+ });
507
+ const tabItemRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab-item\"]")].flatMap((item) => item.querySelector("[data-dashfoo=\"tab\"]")?.dataset.tabId === excludeId ? [] : [item.getBoundingClientRect()]);
508
+ const DockIndicator = ({ actorRef, getTabsetElement }) => {
509
+ const intent = useSelector(actorRef, (snapshot) => snapshot.context.intent);
510
+ const draggedId = useSelector(actorRef, (snapshot) => snapshot.context.subject?.id);
511
+ if (!intent) return null;
512
+ const element = getTabsetElement(intent.targetId);
513
+ if (!element) return null;
514
+ const zIndex = element.closest("[data-dashfoo=\"float\"]") ? overlayBase.zIndex : "auto";
515
+ if (intent.location === "center") {
516
+ const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
517
+ if (strip) return /* @__PURE__ */ jsx("div", {
518
+ "data-dashfoo": "dock-indicator",
519
+ style: {
520
+ ...lineStyle(insertionLineRect(strip.getBoundingClientRect(), tabItemRects(strip, draggedId), intent.index ?? 0)),
521
+ zIndex
522
+ }
523
+ });
524
+ }
525
+ return /* @__PURE__ */ jsx("div", {
526
+ "data-dashfoo": "dock-indicator",
527
+ style: {
528
+ ...paneStyle(zoneRect(element.getBoundingClientRect(), intent.location)),
529
+ zIndex
530
+ }
531
+ });
532
+ };
533
+ const previewStyle = {
534
+ left: 0,
535
+ position: "fixed",
536
+ top: 0,
537
+ zIndex: 9999
538
+ };
539
+ const DragPreview = ({ overlayRef, preview }) => {
540
+ if (!preview) return null;
541
+ return /* @__PURE__ */ jsx("div", {
542
+ "data-dashfoo": "drag-preview",
543
+ ref: overlayRef,
544
+ style: {
545
+ ...previewStyle,
546
+ transform: `translate(${preview.x}px, ${preview.y}px)`
547
+ },
548
+ children: preview.label
549
+ });
550
+ };
551
+
552
+ //#endregion
553
+ //#region src/components/drag-adapter.tsx
554
+ const tabRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()]);
555
+ const intentForTabset = (id, element, point, draggedId) => {
556
+ const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
557
+ if (strip && pointInRect(point, strip.getBoundingClientRect())) return {
558
+ index: insertionIndex(tabRects(strip, draggedId), point.x),
559
+ location: "center",
560
+ targetId: id
561
+ };
562
+ const target = resolveDockTarget(point, element.getBoundingClientRect());
563
+ const location = target.kind === "tab" ? "center" : `split-${target.edge}`;
564
+ if (location === "center" && strip) return {
565
+ index: tabRects(strip, draggedId).length,
566
+ location,
567
+ targetId: id
568
+ };
569
+ return {
570
+ location,
571
+ targetId: id
572
+ };
573
+ };
574
+ const PREVIEW_OFFSET = {
575
+ x: 12,
576
+ y: 8
577
+ };
578
+ const labelOf = (source) => {
579
+ const raw = source?.data?.label;
580
+ return typeof raw === "string" ? raw : "";
581
+ };
582
+ const isTabFactory = (value) => typeof value === "function";
583
+ const subjectFor = (source) => {
584
+ const data = source.data;
585
+ if (data?.type === "tabset") return {
586
+ id: String(data.tabsetId),
587
+ kind: "tabset"
588
+ };
589
+ if (data?.type === "external") {
590
+ if (!isTabFactory(data.createTab)) {
591
+ console.warn("[dashfoo] external drag source is missing its createTab function");
592
+ return null;
593
+ }
594
+ let candidate;
595
+ try {
596
+ candidate = data.createTab();
597
+ } catch (error) {
598
+ console.warn("[dashfoo] external drag source createTab threw", error);
599
+ return null;
600
+ }
601
+ const parsed = tabNodeSchema.safeParse(candidate);
602
+ if (!parsed.success) {
603
+ console.warn("[dashfoo] external drag source returned an invalid tab", parsed.error);
604
+ return null;
605
+ }
606
+ return {
607
+ id: String(source.id),
608
+ kind: "external",
609
+ tab: parsed.data
610
+ };
611
+ }
612
+ return {
613
+ id: String(source.id),
614
+ kind: "tab"
615
+ };
616
+ };
617
+ const DragProvider = ({ children, onCommit, splitDock }) => {
618
+ const actorRef = useActorRef(dragDockMachine);
619
+ const layoutStore = useContext(LayoutStoreContext);
620
+ const [tabsets] = useState(() => /* @__PURE__ */ new Map());
621
+ const overlayRef = useRef(null);
622
+ const [preview, setPreview] = useState(null);
623
+ const sharedManager = useContext(SharedDragManagerContext);
624
+ const [ownManager] = useState(() => sharedManager ? null : createDragManager());
625
+ useInsertionEffect(() => () => ownManager?.destroy(), [ownManager]);
626
+ const manager = sharedManager ?? ownManager;
627
+ if (manager === null) throw new Error("[dashfoo] DashfooDragProvider unmounted while its layout is still mounted; keep the provider above the layout or remount the layout");
628
+ const inheritedStore = useContext(DragSubjectStoreContext);
629
+ const [subjectStore] = useState(() => inheritedStore ?? createDragSubjectStore());
630
+ const ownerId = useId();
631
+ useEffect(() => {
632
+ const subscription = actorRef.subscribe((snapshot) => {
633
+ const { intent, subject } = snapshot.context;
634
+ const state = subjectStore.getState();
635
+ if (intent !== null) {
636
+ if (!sameDropIntent(state.intent, intent) || state.intentOwner !== ownerId) subjectStore.setState({
637
+ intent,
638
+ intentOwner: ownerId
639
+ });
640
+ } else if (state.intent !== null && state.intentOwner === ownerId) subjectStore.setState({
641
+ intent: null,
642
+ intentOwner: null
643
+ });
644
+ if (!sameDragSubject(subjectStore.getState().subject, subject)) subjectStore.setState({ subject });
645
+ });
646
+ return () => {
647
+ subscription.unsubscribe();
648
+ };
649
+ }, [
650
+ actorRef,
651
+ ownerId,
652
+ subjectStore
653
+ ]);
654
+ const onCommitRef = useRef(onCommit);
655
+ const splitDockRef = useRef(splitDock);
656
+ useEffect(() => {
657
+ onCommitRef.current = onCommit;
658
+ splitDockRef.current = splitDock;
659
+ });
660
+ useEffect(() => {
661
+ const subscription = actorRef.on("COMMIT", (emitted) => {
662
+ if (layoutStore?.getState().editable === false) return;
663
+ const commit = onCommitRef.current ?? layoutStore?.getState().dispatch;
664
+ if (!commit) {
665
+ warnOnce("drag-layer-no-commit", "Layout.DragLayer has neither an onCommit prop nor a <Layout.Root> above it; drops are ignored");
666
+ return;
667
+ }
668
+ commit(emitted.action);
669
+ });
670
+ return () => {
671
+ subscription.unsubscribe();
672
+ };
673
+ }, [actorRef, layoutStore]);
674
+ const registerTabset = useCallback((id, element) => {
675
+ if (element) tabsets.set(id, element);
676
+ else tabsets.delete(id);
677
+ }, [tabsets]);
678
+ const getTabsetElement = useCallback((id) => tabsets.get(id), [tabsets]);
679
+ const attachOverlay = useCallback((element) => {
680
+ overlayRef.current = element;
681
+ }, []);
682
+ useEffect(() => {
683
+ const tabsetAt = (point) => {
684
+ const top = document.elementFromPoint(point.x, point.y);
685
+ if (!top) return;
686
+ for (const [id, element] of tabsets) if (element.contains(top)) return {
687
+ element,
688
+ id
689
+ };
690
+ };
691
+ const resolveIntent = (targetId, element, point, draggedId) => {
692
+ if (layoutStore?.getState().editable === false) return null;
693
+ if (!shouldAllowDrop(draggedId, targetId, [...element.querySelectorAll("[data-dashfoo=\"tab\"]")].map((tab) => tab.dataset.tabId ?? ""))) return null;
694
+ const intent = intentForTabset(targetId, element, point, draggedId);
695
+ if (!(splitDockRef.current ?? layoutStore?.getState().splitDock ?? true) && intent.location.startsWith("split-")) return {
696
+ location: "center",
697
+ targetId
698
+ };
699
+ return intent;
700
+ };
701
+ const positionOverlay = (point) => {
702
+ const element = overlayRef.current;
703
+ if (element) element.style.transform = `translate(${point.x + PREVIEW_OFFSET.x}px, ${point.y + PREVIEW_OFFSET.y}px)`;
704
+ };
705
+ const handleStart = (event) => {
706
+ const source = event.operation.source;
707
+ if (!source) return;
708
+ const subject = subjectFor(source);
709
+ if (!subject) return;
710
+ const point = event.operation.position.current;
711
+ setPreview({
712
+ label: labelOf(source),
713
+ x: point.x + PREVIEW_OFFSET.x,
714
+ y: point.y + PREVIEW_OFFSET.y
715
+ });
716
+ actorRef.send({
717
+ subject,
718
+ type: "START"
719
+ });
720
+ };
721
+ const handleMove = (event) => {
722
+ const op = event.operation;
723
+ const point = op.position.current;
724
+ positionOverlay(point);
725
+ const draggedId = op.source ? String(op.source.id) : void 0;
726
+ const hit = tabsetAt(point);
727
+ actorRef.send({
728
+ intent: hit ? resolveIntent(hit.id, hit.element, point, draggedId) : null,
729
+ type: "OVER"
730
+ });
731
+ };
732
+ const handleEnd = (event) => {
733
+ setPreview(null);
734
+ if (event.canceled) {
735
+ actorRef.send({ type: "CANCEL" });
736
+ return;
737
+ }
738
+ const op = event.operation;
739
+ const point = op.position.current;
740
+ const draggedId = op.source ? String(op.source.id) : void 0;
741
+ const hit = tabsetAt(point);
742
+ if (hit) actorRef.send({
743
+ intent: resolveIntent(hit.id, hit.element, point, draggedId),
744
+ type: "OVER"
745
+ });
746
+ actorRef.send({ type: "DROP" });
747
+ };
748
+ const offStart = manager.monitor.addEventListener("dragstart", handleStart);
749
+ const offMove = manager.monitor.addEventListener("dragmove", handleMove);
750
+ const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
751
+ return () => {
752
+ offStart();
753
+ offMove();
754
+ offEnd();
755
+ };
756
+ }, [
757
+ actorRef,
758
+ layoutStore,
759
+ manager,
760
+ tabsets
761
+ ]);
762
+ const contextValue = useMemo(() => ({
763
+ manager,
764
+ registerTabset
765
+ }), [manager, registerTabset]);
766
+ return /* @__PURE__ */ jsx(DragContext.Provider, {
767
+ value: contextValue,
768
+ children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
769
+ value: subjectStore,
770
+ children: [
771
+ children,
772
+ /* @__PURE__ */ jsx(DockIndicator, {
773
+ actorRef,
774
+ getTabsetElement
775
+ }),
776
+ /* @__PURE__ */ jsx(DragPreview, {
777
+ overlayRef: attachOverlay,
778
+ preview
779
+ })
780
+ ]
781
+ })
782
+ });
783
+ };
784
+
785
+ //#endregion
786
+ //#region src/components/float-context.ts
787
+ const FloatLayerContext = createContext(false);
788
+ const useHasFloatLayer = () => useContext(FloatLayerContext);
789
+
790
+ //#endregion
791
+ //#region src/lib/float-geometry.ts
792
+ const MIN_WIDTH = 200;
793
+ const MIN_HEIGHT = 120;
794
+ const DEFAULT_WIDTH = 480;
795
+ const DEFAULT_HEIGHT = 340;
796
+ const LIFT_OFFSET = 16;
797
+ const clampToBounds = (rect, bounds, size = rect) => ({
798
+ height: rect.height,
799
+ left: Math.max(0, Math.min(rect.left, Math.max(0, bounds.width - size.width))),
800
+ top: Math.max(0, Math.min(rect.top, Math.max(0, bounds.height - size.height))),
801
+ width: rect.width
802
+ });
803
+ const measureFloatRect = (element) => {
804
+ const rect = (element.closest("[data-dashfoo=\"tabset\"]") ?? element).getBoundingClientRect();
805
+ return clampToBounds({
806
+ height: Math.max(MIN_HEIGHT, Math.min(Math.round(rect.height), DEFAULT_HEIGHT)),
807
+ left: Math.round(rect.left) + LIFT_OFFSET,
808
+ top: Math.round(rect.top) + LIFT_OFFSET,
809
+ width: Math.max(MIN_WIDTH, Math.min(Math.round(rect.width), DEFAULT_WIDTH))
810
+ }, {
811
+ height: window.innerHeight,
812
+ width: window.innerWidth
813
+ });
814
+ };
815
+ const resizeRect = (start, edges, dx, dy) => {
816
+ let { height, left, top, width } = start;
817
+ if (edges.x === 1) width = Math.max(MIN_WIDTH, start.width + dx);
818
+ else if (edges.x === -1) {
819
+ width = Math.max(MIN_WIDTH, start.width - dx);
820
+ left = Math.max(0, start.left + (start.width - width));
821
+ }
822
+ if (edges.y === 1) height = Math.max(MIN_HEIGHT, start.height + dy);
823
+ else if (edges.y === -1) {
824
+ height = Math.max(MIN_HEIGHT, start.height - dy);
825
+ top = Math.max(0, start.top + (start.height - height));
826
+ }
827
+ return {
828
+ height,
829
+ left,
830
+ top,
831
+ width
832
+ };
833
+ };
834
+
835
+ //#endregion
836
+ //#region src/components/float-resize-handles.ts
837
+ const EDGE_THICKNESS = 12;
838
+ const EDGE_OFFSET = -6;
839
+ const CORNER_SIZE = 20;
840
+ const CORNER_OFFSET = -8;
841
+ const EDGE_INSET = 12;
842
+ const RESIZE_HANDLES = [
843
+ {
844
+ edges: {
845
+ x: 0,
846
+ y: -1
847
+ },
848
+ key: "n",
849
+ style: {
850
+ cursor: "ns-resize",
851
+ height: EDGE_THICKNESS,
852
+ insetInline: EDGE_INSET,
853
+ top: EDGE_OFFSET
854
+ }
855
+ },
856
+ {
857
+ edges: {
858
+ x: 0,
859
+ y: 1
860
+ },
861
+ key: "s",
862
+ style: {
863
+ bottom: EDGE_OFFSET,
864
+ cursor: "ns-resize",
865
+ height: EDGE_THICKNESS,
866
+ insetInline: EDGE_INSET
867
+ }
868
+ },
869
+ {
870
+ edges: {
871
+ x: 1,
872
+ y: 0
873
+ },
874
+ key: "e",
875
+ style: {
876
+ cursor: "ew-resize",
877
+ insetBlock: EDGE_INSET,
878
+ right: EDGE_OFFSET,
879
+ width: EDGE_THICKNESS
880
+ }
881
+ },
882
+ {
883
+ edges: {
884
+ x: -1,
885
+ y: 0
886
+ },
887
+ key: "w",
888
+ style: {
889
+ cursor: "ew-resize",
890
+ insetBlock: EDGE_INSET,
891
+ left: EDGE_OFFSET,
892
+ width: EDGE_THICKNESS
893
+ }
894
+ },
895
+ {
896
+ edges: {
897
+ x: 1,
898
+ y: -1
899
+ },
900
+ key: "ne",
901
+ style: {
902
+ cursor: "nesw-resize",
903
+ height: CORNER_SIZE,
904
+ right: CORNER_OFFSET,
905
+ top: CORNER_OFFSET,
906
+ width: CORNER_SIZE
907
+ }
908
+ },
909
+ {
910
+ edges: {
911
+ x: -1,
912
+ y: -1
913
+ },
914
+ key: "nw",
915
+ style: {
916
+ cursor: "nwse-resize",
917
+ height: CORNER_SIZE,
918
+ left: CORNER_OFFSET,
919
+ top: CORNER_OFFSET,
920
+ width: CORNER_SIZE
921
+ }
922
+ },
923
+ {
924
+ edges: {
925
+ x: 1,
926
+ y: 1
927
+ },
928
+ key: "se",
929
+ style: {
930
+ bottom: CORNER_OFFSET,
931
+ cursor: "nwse-resize",
932
+ height: CORNER_SIZE,
933
+ right: CORNER_OFFSET,
934
+ width: CORNER_SIZE
935
+ }
936
+ },
937
+ {
938
+ edges: {
939
+ x: -1,
940
+ y: 1
941
+ },
942
+ key: "sw",
943
+ style: {
944
+ bottom: CORNER_OFFSET,
945
+ cursor: "nesw-resize",
946
+ height: CORNER_SIZE,
947
+ left: CORNER_OFFSET,
948
+ width: CORNER_SIZE
949
+ }
950
+ }
951
+ ];
952
+ const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
953
+
954
+ //#endregion
955
+ //#region src/components/layout-root.tsx
956
+ const DEFAULT_TABSET_MIN_SIZE = 320;
957
+ const rootStyle = {
958
+ display: "flex",
959
+ height: "100%",
960
+ position: "relative",
961
+ width: "100%"
962
+ };
963
+ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = true, draggableTabsets = true, editable = true, floatable = false, keepMounted = false, maximizable = true, model, renamableTabs = true, renderTab, renderTabLabel, renderTabsetToolbar, resizableSplits = true, rootRef, snap, style, ...props }) => {
964
+ const global = model.global;
965
+ const snapshot = {
966
+ closableTabs: editable && closableTabs && global.tabEnableClose !== false,
967
+ dispatch,
968
+ draggableTabs: editable && draggableTabs && global.tabEnableDrag !== false,
969
+ draggableTabsets: editable && draggableTabsets,
970
+ editable,
971
+ floatable: editable && floatable,
972
+ keepMounted,
973
+ maximizable: maximizable && global.tabSetEnableMaximize !== false,
974
+ maximizedTabsetId: model.maximizedTabsetId,
975
+ renamableTabs: editable && renamableTabs && global.tabEnableRename !== false,
976
+ renderTab,
977
+ renderTabLabel,
978
+ renderTabsetToolbar,
979
+ resizableSplits: editable && resizableSplits && global.enableSplitResize !== false,
980
+ snap: snap ?? global.snap ?? null,
981
+ splitDock: global.enableSplitDock !== false,
982
+ tabLocation: global.tabLocation ?? "top",
983
+ tabsetMinSize: global.tabSetMinSize ?? DEFAULT_TABSET_MIN_SIZE,
984
+ tabStripEnabled: global.tabSetEnableTabStrip !== false
985
+ };
986
+ const [store] = useState(() => createLayoutStore(snapshot));
987
+ useLayoutEffect(() => {
988
+ store.setState(snapshot);
989
+ });
990
+ const layoutStyle = global.splitterSize === void 0 ? {
991
+ ...rootStyle,
992
+ ...style
993
+ } : {
994
+ ...rootStyle,
995
+ "--dashfoo-splitter-size": `${global.splitterSize}px`,
996
+ ...style
997
+ };
998
+ return /* @__PURE__ */ jsx(LayoutStoreContext.Provider, {
999
+ value: store,
1000
+ children: /* @__PURE__ */ jsx("div", {
1001
+ ...props,
1002
+ "data-dashfoo": "layout",
1003
+ ref: rootRef,
1004
+ style: layoutStyle,
1005
+ children
1006
+ })
1007
+ });
1008
+ };
1009
+
1010
+ //#endregion
1011
+ //#region src/lib/tab-ids.ts
1012
+ const tabDomId = (tabsetId, tabId) => `dashfoo-tab-${tabsetId}-${tabId}`;
1013
+ const panelDomId = (tabsetId) => `dashfoo-panel-${tabsetId}`;
1014
+
1015
+ //#endregion
1016
+ //#region src/components/tabset/tabset-store.ts
1017
+ const tabButtonIn = (scope, tabsetId, tabId) => scope?.querySelector(`#${CSS.escape(tabDomId(tabsetId, tabId))}`) ?? null;
1018
+ const createTabsetStore = (initial) => createStore((set, get) => ({
1019
+ ...initial,
1020
+ cancelRename: () => set({ editingTabId: null }),
1021
+ closeTab: (tabId) => {
1022
+ set({ restoreFocus: true });
1023
+ get().dispatch({
1024
+ tabId,
1025
+ type: "deleteTab"
1026
+ });
1027
+ },
1028
+ commitRename: (tabId, value) => {
1029
+ set({ editingTabId: null });
1030
+ const { node, renameTab } = get();
1031
+ const trimmed = value.trim();
1032
+ const current = node.children.find((tab) => tab.id === tabId)?.name;
1033
+ if (trimmed && trimmed !== current) renameTab(tabId, trimmed);
1034
+ },
1035
+ editingTabId: null,
1036
+ overflowItems: [],
1037
+ registerRenameInput: () => {
1038
+ set((state) => ({ renameInputCount: state.renameInputCount + 1 }));
1039
+ return () => set((state) => ({ renameInputCount: state.renameInputCount - 1 }));
1040
+ },
1041
+ registerTablist: (element) => {
1042
+ if (element) {
1043
+ const current = get().tablistElement;
1044
+ if (current && current !== element) warnOnce(`duplicate-tablist-${get().node.id}`, "more than one <Tabset.Tablist> under the same <Tabset.Root>; the last one wins for keyboard focus and overflow");
1045
+ set({ tablistElement: element });
1046
+ return;
1047
+ }
1048
+ set({ tablistElement: null });
1049
+ },
1050
+ renameInputCount: 0,
1051
+ renameTab: (tabId, name) => get().dispatch({
1052
+ name,
1053
+ tabId,
1054
+ type: "renameTab"
1055
+ }),
1056
+ restoreFocus: false,
1057
+ selectOverflowTab: (tabId) => {
1058
+ const { dispatch, node, tablistElement } = get();
1059
+ const index = node.children.findIndex((tab) => tab.id === tabId);
1060
+ if (index === -1) return;
1061
+ dispatch({
1062
+ index,
1063
+ tabsetId: node.id,
1064
+ type: "selectTab"
1065
+ });
1066
+ tabButtonIn(tablistElement, node.id, tabId)?.scrollIntoView({
1067
+ block: "nearest",
1068
+ inline: "nearest"
1069
+ });
1070
+ },
1071
+ selectTab: (index, options) => {
1072
+ const { dispatch, node, tablistElement } = get();
1073
+ dispatch({
1074
+ index,
1075
+ tabsetId: node.id,
1076
+ type: "selectTab"
1077
+ });
1078
+ if (options?.focus) {
1079
+ const tabId = node.children[index]?.id;
1080
+ if (tabId) tabButtonIn(tablistElement, node.id, tabId)?.focus();
1081
+ }
1082
+ },
1083
+ startEditing: (tabId) => {
1084
+ const { node, renameInputCount, tabsRenamable } = get();
1085
+ const tab = node.children.find((child) => child.id === tabId);
1086
+ if (!tabsRenamable || tab?.enableRename === false) return;
1087
+ if (renameInputCount === 0) {
1088
+ warnOnce(`missing-rename-input-${node.id}`, "startEditing needs a <Tabset.RenameInput> in the composition; ignoring");
1089
+ return;
1090
+ }
1091
+ set({ editingTabId: tabId });
1092
+ },
1093
+ tablistElement: null,
1094
+ toggleMaximize: () => {
1095
+ const { dispatch, isMaximized, node } = get();
1096
+ dispatch({
1097
+ tabsetId: isMaximized ? null : node.id,
1098
+ type: "setMaximizedTabset"
1099
+ });
1100
+ }
1101
+ }));
1102
+ const TabsetStoreContext = createContext(null);
1103
+ const useTabset = (selector) => {
1104
+ const store = useContext(TabsetStoreContext);
1105
+ if (store === null) throw new Error("dashfoo Tabset parts must be rendered inside <Tabset.Root>.");
1106
+ return useStore(store, selector);
1107
+ };
1108
+ const TabContext = createContext(null);
1109
+ const useTab = () => {
1110
+ const value = useContext(TabContext);
1111
+ if (value === null) throw new Error("dashfoo tab parts must be rendered inside <Tabset.Tab>.");
1112
+ return value;
1113
+ };
1114
+
1115
+ //#endregion
1116
+ //#region src/components/tabset/tabset-content.tsx
1117
+ const contentStyle = {
1118
+ flex: 1,
1119
+ minHeight: 0,
1120
+ overflow: "auto"
1121
+ };
1122
+ const TabPanel = ({ render, tab }) => {
1123
+ const renderTab = useLayout((state) => state.renderTab);
1124
+ return render ? render(tab) : renderTab(tab);
1125
+ };
1126
+ const TabsetContent = ({ children, style, ...props }) => {
1127
+ const node = useTabset((state) => state.node);
1128
+ const visualSelected = useTabset((state) => state.visualSelected);
1129
+ const keepMounted = useLayout((state) => state.keepMounted);
1130
+ const mergedStyle = {
1131
+ ...contentStyle,
1132
+ ...style
1133
+ };
1134
+ if (keepMounted && node.children.length > 0) return node.children.map((tab, index) => /* @__PURE__ */ jsx("div", {
1135
+ ...props,
1136
+ "aria-labelledby": tabDomId(node.id, tab.id),
1137
+ "data-dashfoo": "tabcontent",
1138
+ hidden: index !== visualSelected || void 0,
1139
+ id: index === visualSelected ? panelDomId(node.id) : void 0,
1140
+ role: index === visualSelected ? "tabpanel" : void 0,
1141
+ style: mergedStyle,
1142
+ tabIndex: index === visualSelected ? 0 : void 0,
1143
+ children: /* @__PURE__ */ jsx(TabPanel, {
1144
+ render: children,
1145
+ tab
1146
+ })
1147
+ }, tab.id));
1148
+ const active = node.children[visualSelected];
1149
+ if (active) return /* @__PURE__ */ jsx("div", {
1150
+ ...props,
1151
+ "aria-labelledby": tabDomId(node.id, active.id),
1152
+ "data-dashfoo": "tabcontent",
1153
+ id: panelDomId(node.id),
1154
+ role: "tabpanel",
1155
+ style: mergedStyle,
1156
+ tabIndex: 0,
1157
+ children: /* @__PURE__ */ jsx(TabPanel, {
1158
+ render: children,
1159
+ tab: active
1160
+ })
1161
+ });
1162
+ return /* @__PURE__ */ jsx("div", {
1163
+ ...props,
1164
+ "data-dashfoo": "tabcontent",
1165
+ style: mergedStyle
1166
+ });
1167
+ };
1168
+
1169
+ //#endregion
1170
+ //#region src/components/tab-overflow.tsx
1171
+ const OverflowIcon = () => /* @__PURE__ */ jsxs("svg", {
1172
+ "aria-hidden": "true",
1173
+ fill: "currentColor",
1174
+ height: "10",
1175
+ viewBox: "0 0 10 10",
1176
+ width: "10",
1177
+ children: [
1178
+ /* @__PURE__ */ jsx("circle", {
1179
+ cx: "2",
1180
+ cy: "5",
1181
+ r: "1"
1182
+ }),
1183
+ /* @__PURE__ */ jsx("circle", {
1184
+ cx: "5",
1185
+ cy: "5",
1186
+ r: "1"
1187
+ }),
1188
+ /* @__PURE__ */ jsx("circle", {
1189
+ cx: "8",
1190
+ cy: "5",
1191
+ r: "1"
1192
+ })
1193
+ ]
1194
+ });
1195
+ const menuStyle = {
1196
+ position: "absolute",
1197
+ right: 0,
1198
+ top: "100%",
1199
+ zIndex: 10
1200
+ };
1201
+ const TabOverflowMenu = ({ items, onSelect }) => {
1202
+ const [open, setOpen] = useState(false);
1203
+ const [activeIndex, setActiveIndex] = useState(0);
1204
+ const rootRef = useRef(null);
1205
+ const triggerRef = useRef(null);
1206
+ const itemRefs = useRef([]);
1207
+ useEffect(() => {
1208
+ if (!open) return;
1209
+ const handlePointerDown = (event) => {
1210
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false);
1211
+ };
1212
+ document.addEventListener("pointerdown", handlePointerDown);
1213
+ return () => {
1214
+ document.removeEventListener("pointerdown", handlePointerDown);
1215
+ };
1216
+ }, [open]);
1217
+ useEffect(() => {
1218
+ if (open) itemRefs.current[0]?.focus();
1219
+ }, [open]);
1220
+ const closeAndReturnFocus = () => {
1221
+ setOpen(false);
1222
+ triggerRef.current?.focus();
1223
+ };
1224
+ const focusItem = (index) => {
1225
+ setActiveIndex(index);
1226
+ itemRefs.current[index]?.focus();
1227
+ };
1228
+ const handleToggle = () => {
1229
+ setActiveIndex(0);
1230
+ setOpen((value) => !value);
1231
+ };
1232
+ const handleSelect = (id) => {
1233
+ onSelect(id);
1234
+ closeAndReturnFocus();
1235
+ };
1236
+ const handleMenuKeyDown = (event) => {
1237
+ const last = items.length - 1;
1238
+ switch (event.key) {
1239
+ case "ArrowDown":
1240
+ event.preventDefault();
1241
+ focusItem(activeIndex >= last ? 0 : activeIndex + 1);
1242
+ break;
1243
+ case "ArrowUp":
1244
+ event.preventDefault();
1245
+ focusItem(activeIndex <= 0 ? last : activeIndex - 1);
1246
+ break;
1247
+ case "Home":
1248
+ event.preventDefault();
1249
+ focusItem(0);
1250
+ break;
1251
+ case "End":
1252
+ event.preventDefault();
1253
+ focusItem(last);
1254
+ break;
1255
+ case "Escape":
1256
+ event.preventDefault();
1257
+ closeAndReturnFocus();
1258
+ break;
1259
+ default: break;
1260
+ }
1261
+ };
1262
+ const handleFocusOut = (event) => {
1263
+ if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
1264
+ setOpen(false);
1265
+ };
1266
+ return /* @__PURE__ */ jsxs("div", {
1267
+ "data-dashfoo": "tab-overflow-root",
1268
+ ref: rootRef,
1269
+ style: { position: "relative" },
1270
+ children: [/* @__PURE__ */ jsx("button", {
1271
+ "aria-expanded": open,
1272
+ "aria-haspopup": "menu",
1273
+ "aria-label": "More tabs",
1274
+ "data-dashfoo": "tab-overflow",
1275
+ onClick: handleToggle,
1276
+ ref: triggerRef,
1277
+ title: "More tabs",
1278
+ type: "button",
1279
+ children: /* @__PURE__ */ jsx(OverflowIcon, {})
1280
+ }), open ? /* @__PURE__ */ jsx("div", {
1281
+ "data-dashfoo": "tab-overflow-menu",
1282
+ onBlur: handleFocusOut,
1283
+ onKeyDown: handleMenuKeyDown,
1284
+ role: "menu",
1285
+ style: menuStyle,
1286
+ tabIndex: -1,
1287
+ children: items.map((item, index) => /* @__PURE__ */ jsx("button", {
1288
+ "data-dashfoo": "tab-overflow-item",
1289
+ onClick: () => handleSelect(item.id),
1290
+ ref: (node) => {
1291
+ itemRefs.current[index] = node;
1292
+ },
1293
+ role: "menuitem",
1294
+ tabIndex: index === activeIndex ? 0 : -1,
1295
+ type: "button",
1296
+ children: item.name
1297
+ }, item.id))
1298
+ }) : null]
1299
+ });
1300
+ };
1301
+
1302
+ //#endregion
1303
+ //#region src/components/tabset/tabset-overflow.tsx
1304
+ const TabsetOverflowMenu = () => {
1305
+ const overflowItems = useTabset((state) => state.overflowItems);
1306
+ const selectOverflowTab = useTabset((state) => state.selectOverflowTab);
1307
+ if (overflowItems.length === 0) return null;
1308
+ return /* @__PURE__ */ jsx(TabOverflowMenu, {
1309
+ items: overflowItems,
1310
+ onSelect: selectOverflowTab
1311
+ });
1312
+ };
1313
+
1314
+ //#endregion
1315
+ //#region src/hooks/use-tab-overflow.ts
1316
+ const overflowingIds = (tablist) => {
1317
+ const box = tablist.getBoundingClientRect();
1318
+ return [...tablist.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => {
1319
+ const rect = tab.getBoundingClientRect();
1320
+ return (rect.right > box.right + 1 || rect.left < box.left - 1) && tab.dataset.tabId ? [tab.dataset.tabId] : [];
1321
+ });
1322
+ };
1323
+ const useTabOverflow = (tablist, signature) => {
1324
+ const [overflow, setOverflow] = useState([]);
1325
+ useEffect(() => {
1326
+ const recompute = () => {
1327
+ setOverflow((previous) => {
1328
+ const next = tablist ? overflowingIds(tablist) : [];
1329
+ return next.length === 0 && previous.length === 0 ? previous : next;
1330
+ });
1331
+ };
1332
+ recompute();
1333
+ if (!tablist) return;
1334
+ const observer = new ResizeObserver(recompute);
1335
+ observer.observe(tablist);
1336
+ tablist.addEventListener("scroll", recompute, { passive: true });
1337
+ return () => {
1338
+ observer.disconnect();
1339
+ tablist.removeEventListener("scroll", recompute);
1340
+ };
1341
+ }, [tablist, signature]);
1342
+ return overflow;
1343
+ };
1344
+
1345
+ //#endregion
1346
+ //#region src/lib/tab-selection.ts
1347
+ const fallbackSelectedIndex = (count, removedIndex) => removedIndex + 1 < count ? removedIndex + 1 : removedIndex - 1;
1348
+
1349
+ //#endregion
1350
+ //#region src/components/tabset/tabset-root.tsx
1351
+ const tabsetStyle = {
1352
+ display: "flex",
1353
+ flexDirection: "column",
1354
+ height: "100%",
1355
+ minHeight: 0,
1356
+ minWidth: 0,
1357
+ width: "100%"
1358
+ };
1359
+ const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
1360
+ const closableTabs = useLayout((state) => state.closableTabs);
1361
+ const dispatch = useLayout((state) => state.dispatch);
1362
+ const maximizable = useLayout((state) => state.maximizable);
1363
+ const maximizedTabsetId = useLayout((state) => state.maximizedTabsetId);
1364
+ const renamableTabs = useLayout((state) => state.renamableTabs);
1365
+ const tabLocation = useLayout((state) => state.tabLocation);
1366
+ const { ref } = useTabsetDroppable(node.id);
1367
+ const dragSubject = useDragSubject();
1368
+ const tabsetRef = useRef(null);
1369
+ const isMaximized = maximizedTabsetId === node.id;
1370
+ const showMaximize = maximizable && node.enableMaximize !== false;
1371
+ const tabsClosable = closableTabs && node.enableClose !== false;
1372
+ const tabsRenamable = renamableTabs;
1373
+ const draggingTabIndex = dragSubject?.kind === "tab" ? node.children.findIndex((tab) => tab.id === dragSubject.id) : -1;
1374
+ const visualSelected = draggingTabIndex !== -1 && draggingTabIndex === node.selected ? fallbackSelectedIndex(node.children.length, draggingTabIndex) : node.selected;
1375
+ const activeTab = node.children[visualSelected];
1376
+ const isDragSource = dragSubject?.kind === "tabset" && dragSubject.id === node.id;
1377
+ const snapshot = {
1378
+ activeTab,
1379
+ dispatch,
1380
+ isMaximized,
1381
+ node,
1382
+ showMaximize,
1383
+ tabsClosable,
1384
+ tabsRenamable,
1385
+ visualSelected
1386
+ };
1387
+ const [store] = useState(() => createTabsetStore(snapshot));
1388
+ useLayoutEffect(() => {
1389
+ store.setState(snapshot);
1390
+ });
1391
+ const overflow = useTabOverflow(useStore(store, (state) => state.tablistElement), node.children.map((tab) => `${tab.id}:${tab.name}`).join("|"));
1392
+ const overflowItems = useMemo(() => {
1393
+ const names = new Map(node.children.map((tab) => [tab.id, tab.name]));
1394
+ return overflow.map((id) => ({
1395
+ id,
1396
+ name: names.get(id) ?? id
1397
+ }));
1398
+ }, [overflow, node.children]);
1399
+ useLayoutEffect(() => {
1400
+ store.setState({ overflowItems });
1401
+ }, [overflowItems, store]);
1402
+ useEffect(() => {
1403
+ if (!store.getState().restoreFocus) return;
1404
+ store.setState({ restoreFocus: false });
1405
+ const activeTabId = node.children[node.selected]?.id;
1406
+ if (activeTabId) tabsetRef.current?.querySelector(`#${CSS.escape(tabDomId(node.id, activeTabId))}`)?.focus();
1407
+ else tabsetRef.current?.focus();
1408
+ }, [
1409
+ node.children,
1410
+ node.selected,
1411
+ node.id,
1412
+ store
1413
+ ]);
1414
+ return /* @__PURE__ */ jsx(TabsetStoreContext.Provider, {
1415
+ value: store,
1416
+ children: /* @__PURE__ */ jsx("div", {
1417
+ ...props,
1418
+ "data-dashfoo": "tabset",
1419
+ "data-dragging-source": isDragSource || void 0,
1420
+ "data-tab-location": tabLocation,
1421
+ ref: (element) => {
1422
+ ref(element);
1423
+ tabsetRef.current = element;
1424
+ if (typeof userRef === "function") userRef(element);
1425
+ else if (userRef) userRef.current = element;
1426
+ },
1427
+ style: {
1428
+ ...tabsetStyle,
1429
+ ...style
1430
+ },
1431
+ tabIndex: -1,
1432
+ children
1433
+ })
1434
+ });
1435
+ };
1436
+
1437
+ //#endregion
1438
+ //#region src/lib/merge-refs.ts
1439
+ const applyRef = (ref, value) => {
1440
+ if (typeof ref === "function") ref(value);
1441
+ else if (ref) ref.current = value;
1442
+ };
1443
+ const mergeRefs = (...refs) => (value) => {
1444
+ for (const ref of refs) applyRef(ref, value);
1445
+ };
1446
+
1447
+ //#endregion
1448
+ //#region src/components/tabset/tabset-tab.tsx
1449
+ const TabsetTab = ({ children, ref: userRef, tab, ...props }) => {
1450
+ const store = useContext(TabsetStoreContext);
1451
+ const node = useTabset((state) => state.node);
1452
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1453
+ const dragSubject = useDragSubject();
1454
+ const isDragging = dragSubject?.kind === "tab" && dragSubject.id === tab.id;
1455
+ const itemRef = useRef(null);
1456
+ const wasEditing = useRef(false);
1457
+ const index = node.children.findIndex((child) => child.id === tab.id);
1458
+ useEffect(() => {
1459
+ if (store?.getState().node.children.some((child) => child.id === tab.id) === false) warnOnce(`orphan-tab-${tab.id}`, `<Tabset.Tab> for "${tab.id}" is not a child of its Tabset.Root's node; it renders but can never be selected`);
1460
+ }, [
1461
+ index === -1,
1462
+ store,
1463
+ tab.id
1464
+ ]);
1465
+ useEffect(() => {
1466
+ if (wasEditing.current && !editing) itemRef.current?.querySelector("[data-dashfoo=\"tab\"]")?.focus();
1467
+ wasEditing.current = editing;
1468
+ }, [editing]);
1469
+ const contextValue = useMemo(() => ({
1470
+ index,
1471
+ tab
1472
+ }), [index, tab]);
1473
+ const refCallback = useMemo(() => mergeRefs(itemRef, userRef), [userRef]);
1474
+ return /* @__PURE__ */ jsx(TabContext.Provider, {
1475
+ value: contextValue,
1476
+ children: /* @__PURE__ */ jsx("span", {
1477
+ ...props,
1478
+ "data-dashfoo": "tab-item",
1479
+ "data-dragging": isDragging || void 0,
1480
+ ref: refCallback,
1481
+ children
1482
+ })
1483
+ });
1484
+ };
1485
+ const TabsetTrigger = ({ children, onClick, onDoubleClick, ref: userRef, ...props }) => {
1486
+ const { index, tab } = useTab();
1487
+ const node = useTabset((state) => state.node);
1488
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1489
+ const selectTab = useTabset((state) => state.selectTab);
1490
+ const startEditing = useTabset((state) => state.startEditing);
1491
+ const tabsRenamable = useTabset((state) => state.tabsRenamable);
1492
+ const visualSelected = useTabset((state) => state.visualSelected);
1493
+ const draggableTabs = useLayout((state) => state.draggableTabs);
1494
+ const renderTabLabel = useLayout((state) => state.renderTabLabel);
1495
+ const { ref } = useTabDraggable(tab.id, !draggableTabs || tab.enableDrag === false, tab.name);
1496
+ const refCallback = useMemo(() => mergeRefs(ref, userRef), [ref, userRef]);
1497
+ if (editing) return null;
1498
+ const selected = index === visualSelected;
1499
+ const renamable = tabsRenamable && tab.enableRename !== false;
1500
+ const handleClick = (event) => {
1501
+ onClick?.(event);
1502
+ selectTab(index);
1503
+ };
1504
+ const handleDoubleClick = (event) => {
1505
+ onDoubleClick?.(event);
1506
+ if (renamable) startEditing(tab.id);
1507
+ };
1508
+ return /* @__PURE__ */ jsx("button", {
1509
+ "aria-label": renderTabLabel ? tab.name : void 0,
1510
+ ...props,
1511
+ "aria-controls": panelDomId(node.id),
1512
+ "aria-selected": selected,
1513
+ "data-dashfoo": "tab",
1514
+ "data-tab-id": tab.id,
1515
+ id: tabDomId(node.id, tab.id),
1516
+ onClick: handleClick,
1517
+ onDoubleClick: handleDoubleClick,
1518
+ ref: refCallback,
1519
+ role: "tab",
1520
+ tabIndex: selected ? 0 : -1,
1521
+ type: "button",
1522
+ children: children ?? (renderTabLabel ? renderTabLabel(tab) : tab.name)
1523
+ });
1524
+ };
1525
+
1526
+ //#endregion
1527
+ //#region src/components/tabset-icons.tsx
1528
+ const iconStyle = { pointerEvents: "none" };
1529
+ const Icon = ({ children }) => /* @__PURE__ */ jsx("svg", {
1530
+ "aria-hidden": "true",
1531
+ fill: "none",
1532
+ height: "12",
1533
+ stroke: "currentColor",
1534
+ strokeLinecap: "round",
1535
+ strokeLinejoin: "round",
1536
+ strokeWidth: "2",
1537
+ style: iconStyle,
1538
+ viewBox: "0 0 24 24",
1539
+ width: "12",
1540
+ children
1541
+ });
1542
+ const CloseIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] });
1543
+ const GripIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
1544
+ /* @__PURE__ */ jsx("circle", {
1545
+ cx: "12",
1546
+ cy: "5",
1547
+ r: "1"
1548
+ }),
1549
+ /* @__PURE__ */ jsx("circle", {
1550
+ cx: "19",
1551
+ cy: "5",
1552
+ r: "1"
1553
+ }),
1554
+ /* @__PURE__ */ jsx("circle", {
1555
+ cx: "5",
1556
+ cy: "5",
1557
+ r: "1"
1558
+ }),
1559
+ /* @__PURE__ */ jsx("circle", {
1560
+ cx: "12",
1561
+ cy: "12",
1562
+ r: "1"
1563
+ }),
1564
+ /* @__PURE__ */ jsx("circle", {
1565
+ cx: "19",
1566
+ cy: "12",
1567
+ r: "1"
1568
+ }),
1569
+ /* @__PURE__ */ jsx("circle", {
1570
+ cx: "5",
1571
+ cy: "12",
1572
+ r: "1"
1573
+ }),
1574
+ /* @__PURE__ */ jsx("circle", {
1575
+ cx: "12",
1576
+ cy: "19",
1577
+ r: "1"
1578
+ }),
1579
+ /* @__PURE__ */ jsx("circle", {
1580
+ cx: "19",
1581
+ cy: "19",
1582
+ r: "1"
1583
+ }),
1584
+ /* @__PURE__ */ jsx("circle", {
1585
+ cx: "5",
1586
+ cy: "19",
1587
+ r: "1"
1588
+ })
1589
+ ] });
1590
+ const MaximizeIcon = ({ maximized }) => maximized ? /* @__PURE__ */ jsxs(Icon, { children: [
1591
+ /* @__PURE__ */ jsx("path", { d: "m14 10 7-7" }),
1592
+ /* @__PURE__ */ jsx("path", { d: "M20 10h-6V4" }),
1593
+ /* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
1594
+ /* @__PURE__ */ jsx("path", { d: "M4 14h6v6" })
1595
+ ] }) : /* @__PURE__ */ jsxs(Icon, { children: [
1596
+ /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
1597
+ /* @__PURE__ */ jsx("path", { d: "m21 3-7 7" }),
1598
+ /* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
1599
+ /* @__PURE__ */ jsx("path", { d: "M9 21H3v-6" })
1600
+ ] });
1601
+ const FloatIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("rect", {
1602
+ height: "14",
1603
+ rx: "2",
1604
+ width: "18",
1605
+ x: "3",
1606
+ y: "6"
1607
+ }), /* @__PURE__ */ jsx("path", { d: "M3 10h18" })] });
1608
+ const MinimizeIcon = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
1609
+ const DockIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
1610
+ /* @__PURE__ */ jsx("path", { d: "M3 7V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2" }),
1611
+ /* @__PURE__ */ jsx("path", { d: "M11 8 7 12l4 4" }),
1612
+ /* @__PURE__ */ jsx("path", { d: "M7 12h10" })
1613
+ ] });
1614
+
1615
+ //#endregion
1616
+ //#region src/components/tabset/tabset-tab-controls.tsx
1617
+ const RenameEditor = ({ defaultValue, onBlur, onKeyDown, ref: userRef, ...props }) => {
1618
+ const { tab } = useTab();
1619
+ const cancelRename = useTabset((state) => state.cancelRename);
1620
+ const commitRename = useTabset((state) => state.commitRename);
1621
+ const ref = useRef(null);
1622
+ const done = useRef(false);
1623
+ const refCallback = useMemo(() => mergeRefs(ref, userRef), [userRef]);
1624
+ useEffect(() => {
1625
+ ref.current?.focus();
1626
+ ref.current?.select();
1627
+ }, []);
1628
+ const handleKeyDown = (event) => {
1629
+ onKeyDown?.(event);
1630
+ if (event.key === "Enter") {
1631
+ done.current = true;
1632
+ commitRename(tab.id, event.currentTarget.value);
1633
+ } else if (event.key === "Escape") {
1634
+ done.current = true;
1635
+ cancelRename();
1636
+ }
1637
+ };
1638
+ const handleCommitOnBlur = (event) => {
1639
+ onBlur?.(event);
1640
+ if (done.current) return;
1641
+ done.current = true;
1642
+ commitRename(tab.id, event.currentTarget.value);
1643
+ };
1644
+ return /* @__PURE__ */ jsx("input", {
1645
+ "aria-label": `Rename ${tab.name}`,
1646
+ ...props,
1647
+ "data-dashfoo": "tab-rename",
1648
+ defaultValue: defaultValue ?? tab.name,
1649
+ onBlur: handleCommitOnBlur,
1650
+ onKeyDown: handleKeyDown,
1651
+ ref: refCallback,
1652
+ type: "text"
1653
+ });
1654
+ };
1655
+ const TabsetRenameInput = (props) => {
1656
+ const { tab } = useTab();
1657
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1658
+ const registerRenameInput = useTabset((state) => state.registerRenameInput);
1659
+ useEffect(() => registerRenameInput(), [registerRenameInput]);
1660
+ if (!editing) return null;
1661
+ return /* @__PURE__ */ jsx(RenameEditor, { ...props });
1662
+ };
1663
+ const TabsetCloseButton = ({ children, onClick, ...props }) => {
1664
+ const { index, tab } = useTab();
1665
+ const closeTab = useTabset((state) => state.closeTab);
1666
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1667
+ const tabsClosable = useTabset((state) => state.tabsClosable);
1668
+ const visualSelected = useTabset((state) => state.visualSelected);
1669
+ if (!(tabsClosable && tab.enableClose !== false) || editing) return null;
1670
+ const handleClick = (event) => {
1671
+ onClick?.(event);
1672
+ event.stopPropagation();
1673
+ closeTab(tab.id);
1674
+ };
1675
+ return /* @__PURE__ */ jsx("button", {
1676
+ "aria-label": `Close ${tab.name}`,
1677
+ title: `Close ${tab.name}`,
1678
+ ...props,
1679
+ "data-dashfoo": "tab-close",
1680
+ onClick: handleClick,
1681
+ tabIndex: index === visualSelected ? 0 : -1,
1682
+ type: "button",
1683
+ children: children ?? /* @__PURE__ */ jsx(CloseIcon, {})
1684
+ });
1685
+ };
1686
+
1687
+ //#endregion
1688
+ //#region src/components/tabset/tabset-tablist.tsx
1689
+ const stripStyle = {
1690
+ display: "flex",
1691
+ flexShrink: 0
1692
+ };
1693
+ const tablistStyle = {
1694
+ display: "flex",
1695
+ minWidth: 0,
1696
+ overflowX: "auto"
1697
+ };
1698
+ const TabsetTabStrip = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
1699
+ ...props,
1700
+ "data-dashfoo": "tabstrip",
1701
+ style: {
1702
+ ...stripStyle,
1703
+ ...style
1704
+ }
1705
+ });
1706
+ const TabsetTablist = ({ onKeyDown, ref: userRef, style, ...props }) => {
1707
+ const node = useTabset((state) => state.node);
1708
+ const registerTablist = useTabset((state) => state.registerTablist);
1709
+ const selectTab = useTabset((state) => state.selectTab);
1710
+ const handleKeyDown = (event) => {
1711
+ onKeyDown?.(event);
1712
+ if (event.defaultPrevented) return;
1713
+ if (!(event.target instanceof HTMLElement) || !event.target.closest("[role=\"tab\"]")) return;
1714
+ const count = node.children.length;
1715
+ if (count === 0) return;
1716
+ const next = {
1717
+ ArrowLeft: (node.selected - 1 + count) % count,
1718
+ ArrowRight: (node.selected + 1) % count,
1719
+ End: count - 1,
1720
+ Home: 0
1721
+ }[event.key];
1722
+ if (next === void 0) return;
1723
+ event.preventDefault();
1724
+ selectTab(next, { focus: true });
1725
+ };
1726
+ const refCallback = useMemo(() => mergeRefs(registerTablist, userRef), [registerTablist, userRef]);
1727
+ return /* @__PURE__ */ jsx("div", {
1728
+ "aria-label": node.name ?? "Tabs",
1729
+ ...props,
1730
+ "aria-orientation": "horizontal",
1731
+ "data-dashfoo": "tablist",
1732
+ onKeyDown: handleKeyDown,
1733
+ ref: refCallback,
1734
+ role: "tablist",
1735
+ style: {
1736
+ ...tablistStyle,
1737
+ ...style
1738
+ },
1739
+ tabIndex: -1
1740
+ });
1741
+ };
1742
+
1743
+ //#endregion
1744
+ //#region src/components/tabset/tabset-toolbar.tsx
1745
+ const toolbarStyle = {
1746
+ alignItems: "center",
1747
+ display: "flex",
1748
+ flexShrink: 0,
1749
+ marginInlineStart: "auto"
1750
+ };
1751
+ const TabsetToolbar = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
1752
+ ...props,
1753
+ "data-dashfoo": "tabset-toolbar",
1754
+ style: {
1755
+ ...toolbarStyle,
1756
+ ...style
1757
+ }
1758
+ });
1759
+ const TabsetGrip = ({ children, ref: userRef, ...props }) => {
1760
+ const activeTabName = useTabset((state) => state.activeTab?.name ?? "");
1761
+ const isMaximized = useTabset((state) => state.isMaximized);
1762
+ const node = useTabset((state) => state.node);
1763
+ const hidden = !useLayout((state) => state.draggableTabsets) || isMaximized;
1764
+ const { ref } = useTabsetDraggable(node.id, hidden, activeTabName);
1765
+ const refCallback = useMemo(() => mergeRefs(ref, userRef), [ref, userRef]);
1766
+ if (hidden) return null;
1767
+ return /* @__PURE__ */ jsx("button", {
1768
+ "aria-label": "Move tabset",
1769
+ title: "Move tabset",
1770
+ ...props,
1771
+ "data-dashfoo": "tabset-grip",
1772
+ ref: refCallback,
1773
+ type: "button",
1774
+ children: children ?? /* @__PURE__ */ jsx(GripIcon, {})
1775
+ });
1776
+ };
1777
+ const TabsetMaximizeButton = ({ children, onClick, ...props }) => {
1778
+ const isMaximized = useTabset((state) => state.isMaximized);
1779
+ const showMaximize = useTabset((state) => state.showMaximize);
1780
+ const toggleMaximize = useTabset((state) => state.toggleMaximize);
1781
+ if (!showMaximize) return null;
1782
+ const handleClick = (event) => {
1783
+ onClick?.(event);
1784
+ toggleMaximize();
1785
+ };
1786
+ return /* @__PURE__ */ jsx("button", {
1787
+ "aria-label": isMaximized ? "Restore" : "Maximize",
1788
+ title: isMaximized ? "Restore tabset" : "Maximize tabset",
1789
+ ...props,
1790
+ "aria-pressed": isMaximized,
1791
+ "data-dashfoo": "tabset-maximize",
1792
+ onClick: handleClick,
1793
+ type: "button",
1794
+ children: children ?? /* @__PURE__ */ jsx(MaximizeIcon, { maximized: isMaximized })
1795
+ });
1796
+ };
1797
+ const TabsetFloatButton = ({ children, onClick, ...props }) => {
1798
+ const floatable = useLayout((state) => state.floatable);
1799
+ const dispatch = useLayout((state) => state.dispatch);
1800
+ const isMaximized = useTabset((state) => state.isMaximized);
1801
+ const node = useTabset((state) => state.node);
1802
+ const hasFloatLayer = useHasFloatLayer();
1803
+ if (!floatable || isMaximized) return null;
1804
+ if (!hasFloatLayer) {
1805
+ warnOnce("float-no-layer", "Tabset.FloatButton needs a <Layout.FloatLayer> around the layout (DashfooLayout adds one); the float control is hidden");
1806
+ return null;
1807
+ }
1808
+ const handleClick = (event) => {
1809
+ onClick?.(event);
1810
+ dispatch({
1811
+ geometry: measureFloatRect(event.currentTarget),
1812
+ tabsetId: node.id,
1813
+ type: "floatTabset"
1814
+ });
1815
+ };
1816
+ return /* @__PURE__ */ jsx("button", {
1817
+ "aria-label": "Float panel",
1818
+ title: "Float panel",
1819
+ ...props,
1820
+ "data-dashfoo": "tabset-float",
1821
+ onClick: handleClick,
1822
+ type: "button",
1823
+ children: children ?? /* @__PURE__ */ jsx(FloatIcon, {})
1824
+ });
1825
+ };
1826
+
1827
+ //#endregion
1828
+ //#region src/components/tabset/tabset-view.tsx
1829
+ const tabsetContent = /* @__PURE__ */ jsx(TabsetContent, {});
1830
+ const DefaultTabsetLayout = () => {
1831
+ const draggableTabsets = useLayout((state) => state.draggableTabsets);
1832
+ const floatable = useLayout((state) => state.floatable);
1833
+ const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
1834
+ const tabLocation = useLayout((state) => state.tabLocation);
1835
+ const tabStripEnabled = useLayout((state) => state.tabStripEnabled);
1836
+ const isMaximized = useTabset((state) => state.isMaximized);
1837
+ const node = useTabset((state) => state.node);
1838
+ const overflowCount = useTabset((state) => state.overflowItems.length);
1839
+ const showToolbar = useTabset((state) => state.showMaximize) || renderTabsetToolbar !== void 0 || overflowCount > 0 || floatable && !isMaximized || draggableTabsets && !isMaximized;
1840
+ const strip = tabStripEnabled ? /* @__PURE__ */ jsxs(TabsetTabStrip, { children: [/* @__PURE__ */ jsx(TabsetTablist, { children: node.children.map((tab) => /* @__PURE__ */ jsxs(TabsetTab, {
1841
+ tab,
1842
+ children: [
1843
+ /* @__PURE__ */ jsx(TabsetTrigger, {}),
1844
+ /* @__PURE__ */ jsx(TabsetRenameInput, {}),
1845
+ /* @__PURE__ */ jsx(TabsetCloseButton, {})
1846
+ ]
1847
+ }, tab.id)) }), showToolbar ? /* @__PURE__ */ jsxs(TabsetToolbar, { children: [
1848
+ /* @__PURE__ */ jsx(TabsetOverflowMenu, {}),
1849
+ /* @__PURE__ */ jsx(TabsetGrip, {}),
1850
+ renderTabsetToolbar?.(node),
1851
+ /* @__PURE__ */ jsx(TabsetFloatButton, {}),
1852
+ /* @__PURE__ */ jsx(TabsetMaximizeButton, {})
1853
+ ] }) : null] }) : null;
1854
+ return tabLocation === "bottom" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [tabsetContent, strip] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [strip, tabsetContent] });
1855
+ };
1856
+ const TabsetView = ({ node }) => /* @__PURE__ */ jsx(TabsetRoot, {
1857
+ node,
1858
+ children: /* @__PURE__ */ jsx(DefaultTabsetLayout, {})
1859
+ });
1860
+
1861
+ //#endregion
1862
+ //#region src/components/row-view.tsx
1863
+ const dimensionToSize = (dimension) => `${dimension.value}${dimension.unit}`;
1864
+ const dimensionToPixels = (dimension) => dimension?.unit === "px" ? dimension.value : void 0;
1865
+ const groupStyle = {
1866
+ display: "flex",
1867
+ flex: 1,
1868
+ minHeight: 0,
1869
+ minWidth: 0
1870
+ };
1871
+ const WEIGHT_EPSILON = .01;
1872
+ const layoutFromWeights = (children, total) => children.reduce((layout, child) => {
1873
+ layout[child.id] = (child.weight ?? 1) / total * 100;
1874
+ return layout;
1875
+ }, {});
1876
+ const orderedFromLayout = (layout, children) => children.map((child) => layout[child.id] ?? 0);
1877
+ const layoutFromOrdered = (sizes, children) => children.reduce((layout, child, index) => {
1878
+ layout[child.id] = sizes[index] ?? 0;
1879
+ return layout;
1880
+ }, {});
1881
+ const layoutsMatch = (current, expected) => {
1882
+ const currentIds = Object.keys(current);
1883
+ const expectedIds = Object.keys(expected);
1884
+ if (currentIds.length !== expectedIds.length) return false;
1885
+ return expectedIds.every((id) => Math.abs((current[id] ?? NaN) - expected[id]) <= WEIGHT_EPSILON);
1886
+ };
1887
+ const descendantMinSize = (child, axis, tabsetMinSize) => {
1888
+ const explicit = dimensionToPixels(child.min);
1889
+ if (explicit !== void 0) return explicit;
1890
+ if (child.min !== void 0) return;
1891
+ if (child.type === "tabset") return tabsetMinSize;
1892
+ const childMinimums = [];
1893
+ for (const grandchild of child.children) {
1894
+ const minimum = descendantMinSize(grandchild, axis, tabsetMinSize);
1895
+ if (minimum === void 0) return;
1896
+ childMinimums.push(minimum);
1897
+ }
1898
+ if (childMinimums.length === 0) return;
1899
+ return child.orientation === axis ? childMinimums.reduce((sum, value) => sum + value, 0) : Math.max(...childMinimums);
1900
+ };
1901
+ const RowView = ({ node, renderTabset }) => {
1902
+ const dispatch = useLayout((state) => state.dispatch);
1903
+ const resizableSplits = useLayout((state) => state.resizableSplits);
1904
+ const tabsetMinSize = useLayout((state) => state.tabsetMinSize);
1905
+ const globalSnap = useLayout((state) => state.snap);
1906
+ const effectiveSnap = node.snap ?? globalSnap;
1907
+ const snapActive = snapEnabled(effectiveSnap);
1908
+ const orientation = node.orientation === "row" ? "horizontal" : "vertical";
1909
+ const total = node.children.reduce((sum, child) => sum + (child.weight ?? 1), 0);
1910
+ const desiredLayout = useMemo(() => layoutFromWeights(node.children, total), [node.children, total]);
1911
+ const measured = useRef(false);
1912
+ const groupRef = useRef(null);
1913
+ const groupElement = useRef(null);
1914
+ const syncing = useRef(false);
1915
+ const activeBoundary = useRef(null);
1916
+ const snapping = useRef(false);
1917
+ const snappedBoundaryRef = useRef(null);
1918
+ const [snappedBoundary, setSnappedBoundary] = useState(null);
1919
+ const setHighlight = (boundary) => {
1920
+ if (boundary !== snappedBoundaryRef.current) {
1921
+ snappedBoundaryRef.current = boundary;
1922
+ setSnappedBoundary(boundary);
1923
+ }
1924
+ };
1925
+ const setSnapTransition = (active) => {
1926
+ const element = groupElement.current;
1927
+ if (!element) return;
1928
+ if (active) element.dataset.dashfooSnapping = "true";
1929
+ else delete element.dataset.dashfooSnapping;
1930
+ };
1931
+ useLayoutEffect(() => {
1932
+ const group = groupRef.current;
1933
+ if (!group) return;
1934
+ const current = group.getLayout();
1935
+ if (!(Object.keys(current).length === Object.keys(desiredLayout).length && Object.keys(desiredLayout).every((id) => id in current)) || layoutsMatch(current, desiredLayout)) return;
1936
+ syncing.current = true;
1937
+ group.setLayout(desiredLayout);
1938
+ const frame = requestAnimationFrame(() => {
1939
+ syncing.current = false;
1940
+ });
1941
+ return () => {
1942
+ cancelAnimationFrame(frame);
1943
+ syncing.current = false;
1944
+ };
1945
+ }, [desiredLayout]);
1946
+ const handleLayoutChange = (layout) => {
1947
+ if (snapping.current || syncing.current || !resizableSplits) return;
1948
+ if (effectiveSnap === null || !snapActive) return;
1949
+ const boundary = activeBoundary.current;
1950
+ if (boundary === null) return;
1951
+ const { sizes, snapped } = snapSizes(orderedFromLayout(layout, node.children), boundary, effectiveSnap);
1952
+ setHighlight(snapped ? boundary : null);
1953
+ if (!snapped) {
1954
+ setSnapTransition(false);
1955
+ return;
1956
+ }
1957
+ setSnapTransition(true);
1958
+ const group = groupRef.current;
1959
+ const target = layoutFromOrdered(sizes, node.children);
1960
+ if (!group || layoutsMatch(layout, target)) return;
1961
+ snapping.current = true;
1962
+ group.setLayout(target);
1963
+ snapping.current = false;
1964
+ };
1965
+ const handleLayoutChanged = (layout) => {
1966
+ if (!measured.current) {
1967
+ measured.current = true;
1968
+ return;
1969
+ }
1970
+ if (syncing.current) return;
1971
+ const boundary = activeBoundary.current;
1972
+ activeBoundary.current = null;
1973
+ setHighlight(null);
1974
+ setSnapTransition(false);
1975
+ if (!resizableSplits) return;
1976
+ let weights = node.children.map((child) => layout[child.id] ?? child.weight ?? 1);
1977
+ if (boundary !== null && effectiveSnap !== null && snapActive) weights = snapSizes(weights, boundary, effectiveSnap).sizes;
1978
+ if (!weights.some((weight, index) => Math.abs(weight - (node.children[index]?.weight ?? 1)) > WEIGHT_EPSILON)) return;
1979
+ dispatch({
1980
+ rowId: node.id,
1981
+ type: "adjustSplit",
1982
+ weights
1983
+ });
1984
+ };
1985
+ return /* @__PURE__ */ jsx(Group, {
1986
+ "data-dashfoo": "row",
1987
+ disabled: !resizableSplits,
1988
+ elementRef: groupElement,
1989
+ groupRef,
1990
+ onLayoutChange: snapActive ? handleLayoutChange : void 0,
1991
+ onLayoutChanged: handleLayoutChanged,
1992
+ orientation,
1993
+ style: groupStyle,
1994
+ children: node.children.map((child, index) => {
1995
+ const percent = (child.weight ?? 1) / total * 100;
1996
+ let min = child.min ? dimensionToSize(child.min) : void 0;
1997
+ if (min === void 0) {
1998
+ const minimum = descendantMinSize(child, node.orientation, tabsetMinSize);
1999
+ min = minimum === void 0 ? void 0 : `${minimum}px`;
2000
+ }
2001
+ const max = child.max ? dimensionToSize(child.max) : void 0;
2002
+ return /* @__PURE__ */ jsxs(Fragment, { children: [index > 0 ? /* @__PURE__ */ jsx(Separator, {
2003
+ "data-dashfoo": "splitter",
2004
+ "data-dashfoo-snapped": snappedBoundary === index - 1 ? "true" : void 0,
2005
+ disabled: !resizableSplits,
2006
+ onPointerDown: snapActive && resizableSplits ? () => {
2007
+ activeBoundary.current = index - 1;
2008
+ } : void 0
2009
+ }) : null, /* @__PURE__ */ jsx(Panel$1, {
2010
+ defaultSize: `${percent}%`,
2011
+ id: child.id,
2012
+ maxSize: max,
2013
+ minSize: min,
2014
+ children: child.type === "row" ? /* @__PURE__ */ jsx(RowView, {
2015
+ node: child,
2016
+ renderTabset
2017
+ }) : renderTabset?.(child) ?? /* @__PURE__ */ jsx(TabsetView, { node: child })
2018
+ })] }, child.id);
2019
+ })
2020
+ }, node.id);
2021
+ };
2022
+
2023
+ //#endregion
2024
+ //#region src/components/float-panel.tsx
2025
+ const floatTitle = (node) => node.name ?? "Panel";
2026
+ const titleBarStyle = {
2027
+ alignItems: "center",
2028
+ display: "flex",
2029
+ flexShrink: 0,
2030
+ gap: "0.375rem",
2031
+ touchAction: "none"
2032
+ };
2033
+ const titleStyle = {
2034
+ flex: 1,
2035
+ minWidth: 0,
2036
+ overflow: "hidden",
2037
+ textOverflow: "ellipsis",
2038
+ whiteSpace: "nowrap"
2039
+ };
2040
+ const dockButtonStyle = {
2041
+ alignItems: "center",
2042
+ cursor: "pointer",
2043
+ display: "inline-flex",
2044
+ flexShrink: 0
2045
+ };
2046
+ const bodyStyle = {
2047
+ flex: 1,
2048
+ minHeight: 0,
2049
+ position: "relative"
2050
+ };
2051
+ const CHIP_SIZE = {
2052
+ height: 34,
2053
+ width: 168
2054
+ };
2055
+ const TAP_SLOP = 4;
2056
+ const FloatTitleEditor = ({ dispatch, node, onDone }) => {
2057
+ const ref = useRef(null);
2058
+ const done = useRef(false);
2059
+ useEffect(() => {
2060
+ ref.current?.focus();
2061
+ ref.current?.select();
2062
+ }, []);
2063
+ const commit = (value) => {
2064
+ const trimmed = value.trim();
2065
+ if (trimmed && trimmed !== floatTitle(node)) dispatch({
2066
+ floatId: node.id,
2067
+ name: trimmed,
2068
+ type: "renameFloat"
2069
+ });
2070
+ onDone();
2071
+ };
2072
+ const handleKeyDown = (event) => {
2073
+ if (event.key === "Enter") {
2074
+ done.current = true;
2075
+ commit(event.currentTarget.value);
2076
+ } else if (event.key === "Escape") {
2077
+ done.current = true;
2078
+ onDone();
2079
+ }
2080
+ };
2081
+ const handleBlur = (event) => {
2082
+ if (done.current) return;
2083
+ done.current = true;
2084
+ commit(event.currentTarget.value);
2085
+ };
2086
+ return /* @__PURE__ */ jsx("input", {
2087
+ "aria-label": `Rename ${floatTitle(node)}`,
2088
+ "data-dashfoo": "float-rename",
2089
+ defaultValue: floatTitle(node),
2090
+ onBlur: handleBlur,
2091
+ onKeyDown: handleKeyDown,
2092
+ onPointerDown: (event) => event.stopPropagation(),
2093
+ ref,
2094
+ style: titleStyle,
2095
+ type: "text"
2096
+ });
2097
+ };
2098
+ const FloatTitle = ({ dispatch, node, renamable }) => {
2099
+ const [editing, setEditing] = useState(false);
2100
+ if (renamable && editing) return /* @__PURE__ */ jsx(FloatTitleEditor, {
2101
+ dispatch,
2102
+ node,
2103
+ onDone: () => setEditing(false)
2104
+ });
2105
+ return /* @__PURE__ */ jsx("span", {
2106
+ "data-dashfoo": "float-title",
2107
+ onDoubleClick: renamable ? () => setEditing(true) : void 0,
2108
+ style: titleStyle,
2109
+ title: renamable ? "Double-click to rename" : void 0,
2110
+ children: floatTitle(node)
2111
+ });
2112
+ };
2113
+ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2114
+ const dispatch = useLayout((state) => state.dispatch);
2115
+ const closableTabs = useLayout((state) => state.closableTabs);
2116
+ const draggableTabs = useLayout((state) => state.draggableTabs);
2117
+ const editable = useLayout((state) => state.editable);
2118
+ const renamableTabs = useLayout((state) => state.renamableTabs);
2119
+ const resizableSplits = useLayout((state) => state.resizableSplits);
2120
+ const keepMounted = useLayout((state) => state.keepMounted);
2121
+ const renderTab = useLayout((state) => state.renderTab);
2122
+ const renderTabLabel = useLayout((state) => state.renderTabLabel);
2123
+ const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
2124
+ const snap = useLayout((state) => state.snap);
2125
+ const panelRef = useRef(null);
2126
+ const gestureRef = useRef(null);
2127
+ const latestRef = useRef(node.geometry);
2128
+ const movedRef = useRef(false);
2129
+ const setPanel = (element) => {
2130
+ panelRef.current = element;
2131
+ };
2132
+ const restore = () => {
2133
+ if (!editable) return;
2134
+ dispatch({
2135
+ floatId: node.id,
2136
+ minimized: false,
2137
+ type: "setFloatMinimized"
2138
+ });
2139
+ };
2140
+ const handlePointerDown = (event) => {
2141
+ const panel = panelRef.current;
2142
+ if (!panel || !editable) return;
2143
+ const prior = gestureRef.current;
2144
+ if (prior && panel.hasPointerCapture?.(prior.pointerId)) panel.releasePointerCapture(prior.pointerId);
2145
+ movedRef.current = false;
2146
+ const edgeKey = event.currentTarget.dataset.edge;
2147
+ const parent = panel.offsetParent;
2148
+ const gesture = {
2149
+ bounds: {
2150
+ height: parent?.clientHeight ?? 0,
2151
+ width: parent?.clientWidth ?? 0
2152
+ },
2153
+ edges: edgeKey ? EDGE_BY_KEY.get(edgeKey) ?? null : null,
2154
+ pointerId: event.pointerId,
2155
+ start: node.geometry,
2156
+ startX: event.clientX,
2157
+ startY: event.clientY
2158
+ };
2159
+ gestureRef.current = gesture;
2160
+ latestRef.current = node.geometry;
2161
+ if (gesture.edges) panel.setPointerCapture(event.pointerId);
2162
+ };
2163
+ const handlePointerUp = (event) => {
2164
+ const gesture = gestureRef.current;
2165
+ if (!gesture || event.pointerId !== gesture.pointerId) return;
2166
+ gestureRef.current = null;
2167
+ const panel = panelRef.current;
2168
+ if (panel?.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
2169
+ if (!movedRef.current) {
2170
+ if (node.minimized) restore();
2171
+ return;
2172
+ }
2173
+ dispatch({
2174
+ floatId: node.id,
2175
+ geometry: latestRef.current,
2176
+ type: "moveFloat"
2177
+ });
2178
+ };
2179
+ const handlePointerMove = (event) => {
2180
+ const gesture = gestureRef.current;
2181
+ const panel = panelRef.current;
2182
+ if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2183
+ if (event.buttons === 0) {
2184
+ handlePointerUp(event);
2185
+ return;
2186
+ }
2187
+ const dx = event.clientX - gesture.startX;
2188
+ const dy = event.clientY - gesture.startY;
2189
+ if (!movedRef.current) {
2190
+ if (Math.hypot(dx, dy) <= TAP_SLOP) return;
2191
+ movedRef.current = true;
2192
+ if (!gesture.edges) panel.setPointerCapture(gesture.pointerId);
2193
+ }
2194
+ const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
2195
+ ...gesture.start,
2196
+ left: gesture.start.left + dx,
2197
+ top: gesture.start.top + dy
2198
+ }, gesture.bounds, node.minimized ? CHIP_SIZE : void 0);
2199
+ latestRef.current = next;
2200
+ panel.style.left = `${next.left}px`;
2201
+ panel.style.top = `${next.top}px`;
2202
+ if (!node.minimized) {
2203
+ panel.style.width = `${next.width}px`;
2204
+ panel.style.height = `${next.height}px`;
2205
+ }
2206
+ };
2207
+ const handlePointerCancel = (event) => {
2208
+ const gesture = gestureRef.current;
2209
+ const panel = panelRef.current;
2210
+ if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2211
+ gestureRef.current = null;
2212
+ if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
2213
+ if (!movedRef.current) return;
2214
+ panel.style.left = `${gesture.start.left}px`;
2215
+ panel.style.top = `${gesture.start.top}px`;
2216
+ if (!node.minimized) {
2217
+ panel.style.width = `${gesture.start.width}px`;
2218
+ panel.style.height = `${gesture.start.height}px`;
2219
+ }
2220
+ };
2221
+ if (node.minimized) return /* @__PURE__ */ jsxs("button", {
2222
+ "aria-label": `Restore ${floatTitle(node)} panel`,
2223
+ "data-dashfoo": "float-chip",
2224
+ onKeyDown: (event) => {
2225
+ if (event.key === "Enter" || event.key === " ") {
2226
+ event.preventDefault();
2227
+ restore();
2228
+ }
2229
+ },
2230
+ onPointerCancel: handlePointerCancel,
2231
+ onPointerDown: handlePointerDown,
2232
+ onPointerDownCapture: onFocus,
2233
+ onPointerMove: handlePointerMove,
2234
+ onPointerUp: handlePointerUp,
2235
+ ref: setPanel,
2236
+ style: {
2237
+ left: node.geometry.left,
2238
+ pointerEvents: "auto",
2239
+ position: "absolute",
2240
+ top: node.geometry.top,
2241
+ zIndex
2242
+ },
2243
+ title: floatTitle(node),
2244
+ type: "button",
2245
+ children: [/* @__PURE__ */ jsx(FloatIcon, {}), /* @__PURE__ */ jsx("span", {
2246
+ "data-dashfoo": "float-chip-label",
2247
+ children: floatTitle(node)
2248
+ })]
2249
+ });
2250
+ const floatModel = {
2251
+ global,
2252
+ layout: node.layout,
2253
+ version: 1
2254
+ };
2255
+ return /* @__PURE__ */ jsxs("div", {
2256
+ "data-dashfoo": "float",
2257
+ onPointerCancel: handlePointerCancel,
2258
+ onPointerDownCapture: onFocus,
2259
+ onPointerMove: handlePointerMove,
2260
+ onPointerUp: handlePointerUp,
2261
+ ref: setPanel,
2262
+ style: {
2263
+ height: node.geometry.height,
2264
+ left: node.geometry.left,
2265
+ pointerEvents: "auto",
2266
+ position: "absolute",
2267
+ top: node.geometry.top,
2268
+ width: node.geometry.width,
2269
+ zIndex
2270
+ },
2271
+ children: [
2272
+ /* @__PURE__ */ jsxs("div", {
2273
+ "data-dashfoo": "float-titlebar",
2274
+ onPointerDown: handlePointerDown,
2275
+ style: editable ? titleBarStyle : {
2276
+ ...titleBarStyle,
2277
+ cursor: "default"
2278
+ },
2279
+ children: [
2280
+ /* @__PURE__ */ jsx("span", {
2281
+ "aria-hidden": "true",
2282
+ "data-dashfoo": "float-grip",
2283
+ children: /* @__PURE__ */ jsx(GripIcon, {})
2284
+ }),
2285
+ /* @__PURE__ */ jsx(FloatTitle, {
2286
+ dispatch,
2287
+ node,
2288
+ renamable: editable
2289
+ }),
2290
+ editable && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
2291
+ "aria-label": "Minimize panel",
2292
+ "data-dashfoo": "float-minimize",
2293
+ onClick: () => dispatch({
2294
+ floatId: node.id,
2295
+ minimized: true,
2296
+ type: "setFloatMinimized"
2297
+ }),
2298
+ onPointerDown: (event) => event.stopPropagation(),
2299
+ style: dockButtonStyle,
2300
+ title: "Minimize panel",
2301
+ type: "button",
2302
+ children: /* @__PURE__ */ jsx(MinimizeIcon, {})
2303
+ }), /* @__PURE__ */ jsx("button", {
2304
+ "aria-label": "Dock panel back into the main layout",
2305
+ "data-dashfoo": "float-dock",
2306
+ onClick: () => dispatch({
2307
+ floatId: node.id,
2308
+ type: "dockFloat"
2309
+ }),
2310
+ onPointerDown: (event) => event.stopPropagation(),
2311
+ style: dockButtonStyle,
2312
+ title: "Dock panel back",
2313
+ type: "button",
2314
+ children: /* @__PURE__ */ jsx(DockIcon, {})
2315
+ })] })
2316
+ ]
2317
+ }),
2318
+ /* @__PURE__ */ jsx("div", {
2319
+ "data-dashfoo": "float-body",
2320
+ style: bodyStyle,
2321
+ children: /* @__PURE__ */ jsx(LayoutRoot, {
2322
+ closableTabs,
2323
+ dispatch,
2324
+ draggableTabs,
2325
+ draggableTabsets: false,
2326
+ editable,
2327
+ floatable: false,
2328
+ keepMounted,
2329
+ maximizable: false,
2330
+ model: floatModel,
2331
+ renamableTabs,
2332
+ renderTab,
2333
+ renderTabLabel,
2334
+ renderTabsetToolbar,
2335
+ resizableSplits,
2336
+ snap: snap ?? void 0,
2337
+ children: /* @__PURE__ */ jsx(DragProvider, { children: /* @__PURE__ */ jsx(RowView, { node: node.layout }) })
2338
+ })
2339
+ }),
2340
+ editable && RESIZE_HANDLES.map((handle) => /* @__PURE__ */ jsx("div", {
2341
+ "data-dashfoo": "float-resize",
2342
+ "data-edge": handle.key,
2343
+ onPointerDown: handlePointerDown,
2344
+ style: {
2345
+ position: "absolute",
2346
+ touchAction: "none",
2347
+ zIndex: 1,
2348
+ ...handle.style
2349
+ }
2350
+ }, handle.key))
2351
+ ]
2352
+ });
2353
+ };
2354
+
2355
+ //#endregion
2356
+ //#region src/components/float-layer.tsx
2357
+ const overlayStyle = {
2358
+ inset: 0,
2359
+ pointerEvents: "none",
2360
+ position: "fixed",
2361
+ zIndex: 1
2362
+ };
2363
+ const FloatOverlay = ({ floats, global }) => {
2364
+ const [topId, setTopId] = useState(null);
2365
+ if (floats.length === 0) return null;
2366
+ return /* @__PURE__ */ jsx("div", {
2367
+ "data-dashfoo": "float-overlay",
2368
+ style: overlayStyle,
2369
+ children: floats.map((node) => /* @__PURE__ */ jsx(FloatPanel, {
2370
+ global,
2371
+ node,
2372
+ onFocus: () => setTopId(node.id),
2373
+ zIndex: node.id === topId ? 2 : 1
2374
+ }, node.id))
2375
+ });
2376
+ };
2377
+ const FloatLayer = ({ children, floats, global }) => /* @__PURE__ */ jsxs(FloatLayerContext.Provider, {
2378
+ value: true,
2379
+ children: [children, /* @__PURE__ */ jsx(FloatOverlay, {
2380
+ floats,
2381
+ global
2382
+ })]
2383
+ });
2384
+
2385
+ //#endregion
2386
+ //#region src/components/layout.tsx
2387
+ const Layout = {
2388
+ DragLayer: DragProvider,
2389
+ FloatLayer,
2390
+ Root: LayoutRoot,
2391
+ Rows: RowView,
2392
+ Tabset: TabsetView
2393
+ };
2394
+
2395
+ //#endregion
2396
+ //#region src/components/dashfoo-layout.tsx
2397
+ const DEFAULT_PERSIST_DEBOUNCE_MS = 300;
2398
+ const resolvePersist = (persist, hasDefaultModel) => {
2399
+ if (persist === void 0 || !hasDefaultModel) return null;
2400
+ if (typeof persist === "string") return {
2401
+ debounceMs: DEFAULT_PERSIST_DEBOUNCE_MS,
2402
+ key: persist,
2403
+ storage: localStorageAdapter
2404
+ };
2405
+ return {
2406
+ debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,
2407
+ key: persist.key,
2408
+ storage: persist.storage ?? localStorageAdapter
2409
+ };
2410
+ };
2411
+ const warnedComponents = /* @__PURE__ */ new Set();
2412
+ const DashfooLayout = forwardRef((props, ref) => {
2413
+ const { closableTabs = true, components, defaultModel, draggableTabs = true, draggableTabsets = true, editable = true, factory, floatable = false, keepMounted = false, maximizable = true, model, onAction, onActiveTabsetChange, onMaximizedTabsetChange, onModelChange, persist, renamableTabs = true, renderTabLabel, renderTabsetToolbar, resizableSplits = true, responsive, snap } = props;
2414
+ const persistence = usePersistence(useMemo(() => resolvePersist(persist, defaultModel !== void 0), [persist, defaultModel]), defaultModel);
2415
+ const handleModelChange = useCallback((next, action) => {
2416
+ persistence.save(next);
2417
+ onModelChange?.(next, action);
2418
+ }, [onModelChange, persistence]);
2419
+ const store = useDashfooStore({
2420
+ defaultModel: persistence.initialModel ?? defaultModel,
2421
+ model,
2422
+ onAction,
2423
+ onActiveTabsetChange,
2424
+ onMaximizedTabsetChange,
2425
+ onModelChange: handleModelChange
2426
+ });
2427
+ const hasSharedManager = useContext(SharedDragManagerContext) !== null;
2428
+ useImperativeHandle(ref, () => ({
2429
+ addTab: (tab, target) => store.dispatch({
2430
+ index: target.index,
2431
+ location: target.location ?? "center",
2432
+ tab,
2433
+ targetId: target.targetId,
2434
+ type: "addNode"
2435
+ }),
2436
+ canRedo: store.canRedo,
2437
+ canUndo: store.canUndo,
2438
+ closeTab: (tabId) => store.dispatch({
2439
+ tabId,
2440
+ type: "deleteTab"
2441
+ }),
2442
+ dispatch: store.dispatch,
2443
+ dockFloat: (floatId) => store.dispatch({
2444
+ floatId,
2445
+ type: "dockFloat"
2446
+ }),
2447
+ floatTab: (tabId) => store.dispatch({
2448
+ tabId,
2449
+ type: "floatTab"
2450
+ }),
2451
+ getModel: () => store.model,
2452
+ maximizeTabset: (tabsetId) => store.dispatch({
2453
+ tabsetId,
2454
+ type: "setMaximizedTabset"
2455
+ }),
2456
+ redo: store.redo,
2457
+ renameTab: (tabId, name) => store.dispatch({
2458
+ name,
2459
+ tabId,
2460
+ type: "renameTab"
2461
+ }),
2462
+ resetLayout: () => {
2463
+ persistence.clear();
2464
+ if (defaultModel !== void 0) store.setModel(defaultModel);
2465
+ },
2466
+ selectTab: (tabsetId, index) => store.dispatch({
2467
+ index,
2468
+ tabsetId,
2469
+ type: "selectTab"
2470
+ }),
2471
+ undo: store.undo
2472
+ }), [
2473
+ defaultModel,
2474
+ persistence,
2475
+ store
2476
+ ]);
2477
+ const renderTab = useCallback((tab) => {
2478
+ if (factory) return factory(tab);
2479
+ const Component = components?.[tab.component];
2480
+ if (!Component) {
2481
+ if (components && !warnedComponents.has(tab.component)) {
2482
+ warnedComponents.add(tab.component);
2483
+ console.warn(`[dashfoo] no component registered for "${tab.component}"`);
2484
+ }
2485
+ return null;
2486
+ }
2487
+ return /* @__PURE__ */ jsx(Component, { node: tab });
2488
+ }, [components, factory]);
2489
+ const [containerRef, width] = useContainerWidth();
2490
+ const isCompact = responsive !== void 0 && width <= responsive.maxWidth;
2491
+ const view = useMemo(() => isCompact ? stackModel(store.model, responsive?.orientation) : store.model, [
2492
+ isCompact,
2493
+ responsive?.orientation,
2494
+ store.model
2495
+ ]);
2496
+ const isCompactRef = useRef(isCompact);
2497
+ isCompactRef.current = isCompact;
2498
+ const dispatch = useCallback((action) => {
2499
+ if (isCompactRef.current && action.type === "adjustSplit") return;
2500
+ store.dispatch(action);
2501
+ }, [store]);
2502
+ const maximized = view.maximizedTabsetId ? findTabset(view, view.maximizedTabsetId) : void 0;
2503
+ const tree = /* @__PURE__ */ jsx(Layout.FloatLayer, {
2504
+ floats: store.model.floats ?? [],
2505
+ global: store.model.global,
2506
+ children: /* @__PURE__ */ jsx(Layout.DragLayer, { children: maximized ? /* @__PURE__ */ jsx(Layout.Tabset, { node: maximized }) : /* @__PURE__ */ jsx(Layout.Rows, { node: view.layout }) })
2507
+ });
2508
+ return /* @__PURE__ */ jsx(Layout.Root, {
2509
+ closableTabs,
2510
+ dispatch,
2511
+ draggableTabs: isCompact ? false : draggableTabs,
2512
+ draggableTabsets: isCompact ? false : draggableTabsets,
2513
+ editable,
2514
+ floatable: isCompact ? false : floatable,
2515
+ keepMounted,
2516
+ maximizable,
2517
+ model: view,
2518
+ renamableTabs,
2519
+ renderTab,
2520
+ renderTabLabel,
2521
+ renderTabsetToolbar,
2522
+ resizableSplits: isCompact ? false : resizableSplits,
2523
+ rootRef: containerRef,
2524
+ snap,
2525
+ children: hasSharedManager ? tree : /* @__PURE__ */ jsx(DashfooDragProvider, { children: tree })
2526
+ });
2527
+ });
2528
+ DashfooLayout.displayName = "DashfooLayout";
2529
+
2530
+ //#endregion
2531
+ //#region src/components/panel.tsx
2532
+ const PanelRoot = (props) => /* @__PURE__ */ jsx("div", {
2533
+ ...props,
2534
+ "data-dashfoo": "panel"
2535
+ });
2536
+ const PanelHeader = (props) => /* @__PURE__ */ jsx("div", {
2537
+ ...props,
2538
+ "data-dashfoo": "panel-header"
2539
+ });
2540
+ const PanelBody = (props) => /* @__PURE__ */ jsx("div", {
2541
+ ...props,
2542
+ "data-dashfoo": "panel-body"
2543
+ });
2544
+ const PanelTitle = (props) => /* @__PURE__ */ jsx("span", {
2545
+ ...props,
2546
+ "data-dashfoo": "panel-title"
2547
+ });
2548
+ const PanelIcon = (props) => /* @__PURE__ */ jsx("span", {
2549
+ ...props,
2550
+ "data-dashfoo": "panel-icon"
2551
+ });
2552
+ const PanelBadge = (props) => /* @__PURE__ */ jsx("span", {
2553
+ ...props,
2554
+ "data-dashfoo": "panel-badge"
2555
+ });
2556
+ const Panel = {
2557
+ Badge: PanelBadge,
2558
+ Body: PanelBody,
2559
+ Header: PanelHeader,
2560
+ Icon: PanelIcon,
2561
+ Root: PanelRoot,
2562
+ Title: PanelTitle
2563
+ };
2564
+
2565
+ //#endregion
2566
+ //#region src/components/tabset/tabset.ts
2567
+ const Tabset = {
2568
+ CloseButton: TabsetCloseButton,
2569
+ Content: TabsetContent,
2570
+ FloatButton: TabsetFloatButton,
2571
+ Grip: TabsetGrip,
2572
+ MaximizeButton: TabsetMaximizeButton,
2573
+ OverflowMenu: TabsetOverflowMenu,
2574
+ RenameInput: TabsetRenameInput,
2575
+ Root: TabsetRoot,
2576
+ Tab: TabsetTab,
2577
+ Tablist: TabsetTablist,
2578
+ TabStrip: TabsetTabStrip,
2579
+ Toolbar: TabsetToolbar,
2580
+ Trigger: TabsetTrigger
2581
+ };
2582
+
2583
+ //#endregion
2584
+ export { DashfooDragProvider, DashfooLayout, Layout, Panel, Tabset, activeBreakpoint, localStorageAdapter, matchBreakpoint, memoryStorageAdapter, useContainerWidth, useDashfooStore, useDragSubject, useDropIntent, useExternalTabSource, useLayout, usePersistence, useResponsiveModel, useTab, useTabset };
9
2585
  //# sourceMappingURL=index.js.map