@dashfoo/react 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,2537 @@
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
+ if (intent.location === "center") {
515
+ const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
516
+ if (strip) return /* @__PURE__ */ jsx("div", {
517
+ "data-dashfoo": "dock-indicator",
518
+ style: lineStyle(insertionLineRect(strip.getBoundingClientRect(), tabItemRects(strip, draggedId), intent.index ?? 0))
519
+ });
520
+ }
521
+ return /* @__PURE__ */ jsx("div", {
522
+ "data-dashfoo": "dock-indicator",
523
+ style: paneStyle(zoneRect(element.getBoundingClientRect(), intent.location))
524
+ });
525
+ };
526
+ const previewStyle = {
527
+ left: 0,
528
+ position: "fixed",
529
+ top: 0,
530
+ zIndex: 9999
531
+ };
532
+ const DragPreview = ({ overlayRef, preview }) => {
533
+ if (!preview) return null;
534
+ return /* @__PURE__ */ jsx("div", {
535
+ "data-dashfoo": "drag-preview",
536
+ ref: overlayRef,
537
+ style: {
538
+ ...previewStyle,
539
+ transform: `translate(${preview.x}px, ${preview.y}px)`
540
+ },
541
+ children: preview.label
542
+ });
543
+ };
544
+
545
+ //#endregion
546
+ //#region src/components/drag-adapter.tsx
547
+ const tabRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()]);
548
+ const intentForTabset = (id, element, point, draggedId) => {
549
+ const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
550
+ if (strip && pointInRect(point, strip.getBoundingClientRect())) return {
551
+ index: insertionIndex(tabRects(strip, draggedId), point.x),
552
+ location: "center",
553
+ targetId: id
554
+ };
555
+ const target = resolveDockTarget(point, element.getBoundingClientRect());
556
+ const location = target.kind === "tab" ? "center" : `split-${target.edge}`;
557
+ if (location === "center" && strip) return {
558
+ index: tabRects(strip, draggedId).length,
559
+ location,
560
+ targetId: id
561
+ };
562
+ return {
563
+ location,
564
+ targetId: id
565
+ };
566
+ };
567
+ const PREVIEW_OFFSET = {
568
+ x: 12,
569
+ y: 8
570
+ };
571
+ const labelOf = (source) => {
572
+ const raw = source?.data?.label;
573
+ return typeof raw === "string" ? raw : "";
574
+ };
575
+ const isTabFactory = (value) => typeof value === "function";
576
+ const subjectFor = (source) => {
577
+ const data = source.data;
578
+ if (data?.type === "tabset") return {
579
+ id: String(data.tabsetId),
580
+ kind: "tabset"
581
+ };
582
+ if (data?.type === "external") {
583
+ if (!isTabFactory(data.createTab)) {
584
+ console.warn("[dashfoo] external drag source is missing its createTab function");
585
+ return null;
586
+ }
587
+ let candidate;
588
+ try {
589
+ candidate = data.createTab();
590
+ } catch (error) {
591
+ console.warn("[dashfoo] external drag source createTab threw", error);
592
+ return null;
593
+ }
594
+ const parsed = tabNodeSchema.safeParse(candidate);
595
+ if (!parsed.success) {
596
+ console.warn("[dashfoo] external drag source returned an invalid tab", parsed.error);
597
+ return null;
598
+ }
599
+ return {
600
+ id: String(source.id),
601
+ kind: "external",
602
+ tab: parsed.data
603
+ };
604
+ }
605
+ return {
606
+ id: String(source.id),
607
+ kind: "tab"
608
+ };
609
+ };
610
+ const DragProvider = ({ children, onCommit, splitDock }) => {
611
+ const actorRef = useActorRef(dragDockMachine);
612
+ const layoutStore = useContext(LayoutStoreContext);
613
+ const [tabsets] = useState(() => /* @__PURE__ */ new Map());
614
+ const overlayRef = useRef(null);
615
+ const [preview, setPreview] = useState(null);
616
+ const sharedManager = useContext(SharedDragManagerContext);
617
+ const [ownManager] = useState(() => sharedManager ? null : createDragManager());
618
+ useInsertionEffect(() => () => ownManager?.destroy(), [ownManager]);
619
+ const manager = sharedManager ?? ownManager;
620
+ 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");
621
+ const inheritedStore = useContext(DragSubjectStoreContext);
622
+ const [subjectStore] = useState(() => inheritedStore ?? createDragSubjectStore());
623
+ const ownerId = useId();
624
+ useEffect(() => {
625
+ const subscription = actorRef.subscribe((snapshot) => {
626
+ const { intent, subject } = snapshot.context;
627
+ const state = subjectStore.getState();
628
+ if (intent !== null) {
629
+ if (!sameDropIntent(state.intent, intent) || state.intentOwner !== ownerId) subjectStore.setState({
630
+ intent,
631
+ intentOwner: ownerId
632
+ });
633
+ } else if (state.intent !== null && state.intentOwner === ownerId) subjectStore.setState({
634
+ intent: null,
635
+ intentOwner: null
636
+ });
637
+ if (!sameDragSubject(subjectStore.getState().subject, subject)) subjectStore.setState({ subject });
638
+ });
639
+ return () => {
640
+ subscription.unsubscribe();
641
+ };
642
+ }, [
643
+ actorRef,
644
+ ownerId,
645
+ subjectStore
646
+ ]);
647
+ const onCommitRef = useRef(onCommit);
648
+ const splitDockRef = useRef(splitDock);
649
+ useEffect(() => {
650
+ onCommitRef.current = onCommit;
651
+ splitDockRef.current = splitDock;
652
+ });
653
+ useEffect(() => {
654
+ const subscription = actorRef.on("COMMIT", (emitted) => {
655
+ if (layoutStore?.getState().editable === false) return;
656
+ const commit = onCommitRef.current ?? layoutStore?.getState().dispatch;
657
+ if (!commit) {
658
+ warnOnce("drag-layer-no-commit", "Layout.DragLayer has neither an onCommit prop nor a <Layout.Root> above it; drops are ignored");
659
+ return;
660
+ }
661
+ commit(emitted.action);
662
+ });
663
+ return () => {
664
+ subscription.unsubscribe();
665
+ };
666
+ }, [actorRef, layoutStore]);
667
+ const registerTabset = useCallback((id, element) => {
668
+ if (element) tabsets.set(id, element);
669
+ else tabsets.delete(id);
670
+ }, [tabsets]);
671
+ const getTabsetElement = useCallback((id) => tabsets.get(id), [tabsets]);
672
+ const attachOverlay = useCallback((element) => {
673
+ overlayRef.current = element;
674
+ }, []);
675
+ useEffect(() => {
676
+ const tabsetAt = (point) => {
677
+ const top = document.elementFromPoint(point.x, point.y);
678
+ if (!top) return;
679
+ for (const [id, element] of tabsets) if (element.contains(top)) return {
680
+ element,
681
+ id
682
+ };
683
+ };
684
+ const resolveIntent = (targetId, element, point, draggedId) => {
685
+ if (layoutStore?.getState().editable === false) return null;
686
+ if (!shouldAllowDrop(draggedId, targetId, [...element.querySelectorAll("[data-dashfoo=\"tab\"]")].map((tab) => tab.dataset.tabId ?? ""))) return null;
687
+ const intent = intentForTabset(targetId, element, point, draggedId);
688
+ if (!(splitDockRef.current ?? layoutStore?.getState().splitDock ?? true) && intent.location.startsWith("split-")) return {
689
+ location: "center",
690
+ targetId
691
+ };
692
+ return intent;
693
+ };
694
+ const positionOverlay = (point) => {
695
+ const element = overlayRef.current;
696
+ if (element) element.style.transform = `translate(${point.x + PREVIEW_OFFSET.x}px, ${point.y + PREVIEW_OFFSET.y}px)`;
697
+ };
698
+ const handleStart = (event) => {
699
+ const source = event.operation.source;
700
+ if (!source) return;
701
+ const subject = subjectFor(source);
702
+ if (!subject) return;
703
+ const point = event.operation.position.current;
704
+ setPreview({
705
+ label: labelOf(source),
706
+ x: point.x + PREVIEW_OFFSET.x,
707
+ y: point.y + PREVIEW_OFFSET.y
708
+ });
709
+ actorRef.send({
710
+ subject,
711
+ type: "START"
712
+ });
713
+ };
714
+ const handleMove = (event) => {
715
+ const op = event.operation;
716
+ const point = op.position.current;
717
+ positionOverlay(point);
718
+ const draggedId = op.source ? String(op.source.id) : void 0;
719
+ const hit = tabsetAt(point);
720
+ actorRef.send({
721
+ intent: hit ? resolveIntent(hit.id, hit.element, point, draggedId) : null,
722
+ type: "OVER"
723
+ });
724
+ };
725
+ const handleEnd = (event) => {
726
+ setPreview(null);
727
+ if (event.canceled) {
728
+ actorRef.send({ type: "CANCEL" });
729
+ return;
730
+ }
731
+ const op = event.operation;
732
+ const point = op.position.current;
733
+ const draggedId = op.source ? String(op.source.id) : void 0;
734
+ const hit = tabsetAt(point);
735
+ if (hit) actorRef.send({
736
+ intent: resolveIntent(hit.id, hit.element, point, draggedId),
737
+ type: "OVER"
738
+ });
739
+ actorRef.send({ type: "DROP" });
740
+ };
741
+ const offStart = manager.monitor.addEventListener("dragstart", handleStart);
742
+ const offMove = manager.monitor.addEventListener("dragmove", handleMove);
743
+ const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
744
+ return () => {
745
+ offStart();
746
+ offMove();
747
+ offEnd();
748
+ };
749
+ }, [
750
+ actorRef,
751
+ layoutStore,
752
+ manager,
753
+ tabsets
754
+ ]);
755
+ const contextValue = useMemo(() => ({
756
+ manager,
757
+ registerTabset
758
+ }), [manager, registerTabset]);
759
+ return /* @__PURE__ */ jsx(DragContext.Provider, {
760
+ value: contextValue,
761
+ children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
762
+ value: subjectStore,
763
+ children: [
764
+ children,
765
+ /* @__PURE__ */ jsx(DockIndicator, {
766
+ actorRef,
767
+ getTabsetElement
768
+ }),
769
+ /* @__PURE__ */ jsx(DragPreview, {
770
+ overlayRef: attachOverlay,
771
+ preview
772
+ })
773
+ ]
774
+ })
775
+ });
776
+ };
777
+
778
+ //#endregion
779
+ //#region src/components/float-context.ts
780
+ const FloatLayerContext = createContext(false);
781
+ const useHasFloatLayer = () => useContext(FloatLayerContext);
782
+
783
+ //#endregion
784
+ //#region src/lib/float-geometry.ts
785
+ const MIN_WIDTH = 200;
786
+ const MIN_HEIGHT = 120;
787
+ const DEFAULT_WIDTH = 480;
788
+ const DEFAULT_HEIGHT = 340;
789
+ const LIFT_OFFSET = 16;
790
+ const clampToBounds = (rect, bounds, size = rect) => ({
791
+ height: rect.height,
792
+ left: Math.max(0, Math.min(rect.left, Math.max(0, bounds.width - size.width))),
793
+ top: Math.max(0, Math.min(rect.top, Math.max(0, bounds.height - size.height))),
794
+ width: rect.width
795
+ });
796
+ const measureFloatRect = (element) => {
797
+ const rect = (element.closest("[data-dashfoo=\"tabset\"]") ?? element).getBoundingClientRect();
798
+ return clampToBounds({
799
+ height: Math.max(MIN_HEIGHT, Math.min(Math.round(rect.height), DEFAULT_HEIGHT)),
800
+ left: Math.round(rect.left) + LIFT_OFFSET,
801
+ top: Math.round(rect.top) + LIFT_OFFSET,
802
+ width: Math.max(MIN_WIDTH, Math.min(Math.round(rect.width), DEFAULT_WIDTH))
803
+ }, {
804
+ height: window.innerHeight,
805
+ width: window.innerWidth
806
+ });
807
+ };
808
+ const resizeRect = (start, edges, dx, dy) => {
809
+ let { height, left, top, width } = start;
810
+ if (edges.x === 1) width = Math.max(MIN_WIDTH, start.width + dx);
811
+ else if (edges.x === -1) {
812
+ width = Math.max(MIN_WIDTH, start.width - dx);
813
+ left = Math.max(0, start.left + (start.width - width));
814
+ }
815
+ if (edges.y === 1) height = Math.max(MIN_HEIGHT, start.height + dy);
816
+ else if (edges.y === -1) {
817
+ height = Math.max(MIN_HEIGHT, start.height - dy);
818
+ top = Math.max(0, start.top + (start.height - height));
819
+ }
820
+ return {
821
+ height,
822
+ left,
823
+ top,
824
+ width
825
+ };
826
+ };
827
+
828
+ //#endregion
829
+ //#region src/components/layout-root.tsx
830
+ const DEFAULT_TABSET_MIN_SIZE = 320;
831
+ const rootStyle = {
832
+ display: "flex",
833
+ height: "100%",
834
+ position: "relative",
835
+ width: "100%"
836
+ };
837
+ 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 }) => {
838
+ const global = model.global;
839
+ const snapshot = {
840
+ closableTabs: editable && closableTabs && global.tabEnableClose !== false,
841
+ dispatch,
842
+ draggableTabs: editable && draggableTabs && global.tabEnableDrag !== false,
843
+ draggableTabsets: editable && draggableTabsets,
844
+ editable,
845
+ floatable: editable && floatable,
846
+ keepMounted,
847
+ maximizable: maximizable && global.tabSetEnableMaximize !== false,
848
+ maximizedTabsetId: model.maximizedTabsetId,
849
+ renamableTabs: editable && renamableTabs && global.tabEnableRename !== false,
850
+ renderTab,
851
+ renderTabLabel,
852
+ renderTabsetToolbar,
853
+ resizableSplits: editable && resizableSplits && global.enableSplitResize !== false,
854
+ snap: snap ?? global.snap ?? null,
855
+ splitDock: global.enableSplitDock !== false,
856
+ tabLocation: global.tabLocation ?? "top",
857
+ tabsetMinSize: global.tabSetMinSize ?? DEFAULT_TABSET_MIN_SIZE,
858
+ tabStripEnabled: global.tabSetEnableTabStrip !== false
859
+ };
860
+ const [store] = useState(() => createLayoutStore(snapshot));
861
+ useLayoutEffect(() => {
862
+ store.setState(snapshot);
863
+ });
864
+ const layoutStyle = global.splitterSize === void 0 ? {
865
+ ...rootStyle,
866
+ ...style
867
+ } : {
868
+ ...rootStyle,
869
+ "--dashfoo-splitter-size": `${global.splitterSize}px`,
870
+ ...style
871
+ };
872
+ return /* @__PURE__ */ jsx(LayoutStoreContext.Provider, {
873
+ value: store,
874
+ children: /* @__PURE__ */ jsx("div", {
875
+ ...props,
876
+ "data-dashfoo": "layout",
877
+ ref: rootRef,
878
+ style: layoutStyle,
879
+ children
880
+ })
881
+ });
882
+ };
883
+
884
+ //#endregion
885
+ //#region src/lib/tab-ids.ts
886
+ const tabDomId = (tabsetId, tabId) => `dashfoo-tab-${tabsetId}-${tabId}`;
887
+ const panelDomId = (tabsetId) => `dashfoo-panel-${tabsetId}`;
888
+
889
+ //#endregion
890
+ //#region src/components/tabset/tabset-store.ts
891
+ const tabButtonIn = (scope, tabsetId, tabId) => scope?.querySelector(`#${CSS.escape(tabDomId(tabsetId, tabId))}`) ?? null;
892
+ const createTabsetStore = (initial) => createStore((set, get) => ({
893
+ ...initial,
894
+ cancelRename: () => set({ editingTabId: null }),
895
+ closeTab: (tabId) => {
896
+ set({ restoreFocus: true });
897
+ get().dispatch({
898
+ tabId,
899
+ type: "deleteTab"
900
+ });
901
+ },
902
+ commitRename: (tabId, value) => {
903
+ set({ editingTabId: null });
904
+ const { node, renameTab } = get();
905
+ const trimmed = value.trim();
906
+ const current = node.children.find((tab) => tab.id === tabId)?.name;
907
+ if (trimmed && trimmed !== current) renameTab(tabId, trimmed);
908
+ },
909
+ editingTabId: null,
910
+ overflowItems: [],
911
+ registerRenameInput: () => {
912
+ set((state) => ({ renameInputCount: state.renameInputCount + 1 }));
913
+ return () => set((state) => ({ renameInputCount: state.renameInputCount - 1 }));
914
+ },
915
+ registerTablist: (element) => {
916
+ if (element) {
917
+ const current = get().tablistElement;
918
+ 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");
919
+ set({ tablistElement: element });
920
+ return;
921
+ }
922
+ set({ tablistElement: null });
923
+ },
924
+ renameInputCount: 0,
925
+ renameTab: (tabId, name) => get().dispatch({
926
+ name,
927
+ tabId,
928
+ type: "renameTab"
929
+ }),
930
+ restoreFocus: false,
931
+ selectOverflowTab: (tabId) => {
932
+ const { dispatch, node, tablistElement } = get();
933
+ const index = node.children.findIndex((tab) => tab.id === tabId);
934
+ if (index === -1) return;
935
+ dispatch({
936
+ index,
937
+ tabsetId: node.id,
938
+ type: "selectTab"
939
+ });
940
+ tabButtonIn(tablistElement, node.id, tabId)?.scrollIntoView({
941
+ block: "nearest",
942
+ inline: "nearest"
943
+ });
944
+ },
945
+ selectTab: (index, options) => {
946
+ const { dispatch, node, tablistElement } = get();
947
+ dispatch({
948
+ index,
949
+ tabsetId: node.id,
950
+ type: "selectTab"
951
+ });
952
+ if (options?.focus) {
953
+ const tabId = node.children[index]?.id;
954
+ if (tabId) tabButtonIn(tablistElement, node.id, tabId)?.focus();
955
+ }
956
+ },
957
+ startEditing: (tabId) => {
958
+ const { node, renameInputCount, tabsRenamable } = get();
959
+ const tab = node.children.find((child) => child.id === tabId);
960
+ if (!tabsRenamable || tab?.enableRename === false) return;
961
+ if (renameInputCount === 0) {
962
+ warnOnce(`missing-rename-input-${node.id}`, "startEditing needs a <Tabset.RenameInput> in the composition; ignoring");
963
+ return;
964
+ }
965
+ set({ editingTabId: tabId });
966
+ },
967
+ tablistElement: null,
968
+ toggleMaximize: () => {
969
+ const { dispatch, isMaximized, node } = get();
970
+ dispatch({
971
+ tabsetId: isMaximized ? null : node.id,
972
+ type: "setMaximizedTabset"
973
+ });
974
+ }
975
+ }));
976
+ const TabsetStoreContext = createContext(null);
977
+ const useTabset = (selector) => {
978
+ const store = useContext(TabsetStoreContext);
979
+ if (store === null) throw new Error("dashfoo Tabset parts must be rendered inside <Tabset.Root>.");
980
+ return useStore(store, selector);
981
+ };
982
+ const TabContext = createContext(null);
983
+ const useTab = () => {
984
+ const value = useContext(TabContext);
985
+ if (value === null) throw new Error("dashfoo tab parts must be rendered inside <Tabset.Tab>.");
986
+ return value;
987
+ };
988
+
989
+ //#endregion
990
+ //#region src/components/tabset/tabset-content.tsx
991
+ const contentStyle = {
992
+ flex: 1,
993
+ minHeight: 0,
994
+ overflow: "auto"
995
+ };
996
+ const TabPanel = ({ render, tab }) => {
997
+ const renderTab = useLayout((state) => state.renderTab);
998
+ return render ? render(tab) : renderTab(tab);
999
+ };
1000
+ const TabsetContent = ({ children, style, ...props }) => {
1001
+ const node = useTabset((state) => state.node);
1002
+ const visualSelected = useTabset((state) => state.visualSelected);
1003
+ const keepMounted = useLayout((state) => state.keepMounted);
1004
+ const mergedStyle = {
1005
+ ...contentStyle,
1006
+ ...style
1007
+ };
1008
+ if (keepMounted && node.children.length > 0) return node.children.map((tab, index) => /* @__PURE__ */ jsx("div", {
1009
+ ...props,
1010
+ "aria-labelledby": tabDomId(node.id, tab.id),
1011
+ "data-dashfoo": "tabcontent",
1012
+ hidden: index !== visualSelected || void 0,
1013
+ id: index === visualSelected ? panelDomId(node.id) : void 0,
1014
+ role: index === visualSelected ? "tabpanel" : void 0,
1015
+ style: mergedStyle,
1016
+ tabIndex: index === visualSelected ? 0 : void 0,
1017
+ children: /* @__PURE__ */ jsx(TabPanel, {
1018
+ render: children,
1019
+ tab
1020
+ })
1021
+ }, tab.id));
1022
+ const active = node.children[visualSelected];
1023
+ if (active) return /* @__PURE__ */ jsx("div", {
1024
+ ...props,
1025
+ "aria-labelledby": tabDomId(node.id, active.id),
1026
+ "data-dashfoo": "tabcontent",
1027
+ id: panelDomId(node.id),
1028
+ role: "tabpanel",
1029
+ style: mergedStyle,
1030
+ tabIndex: 0,
1031
+ children: /* @__PURE__ */ jsx(TabPanel, {
1032
+ render: children,
1033
+ tab: active
1034
+ })
1035
+ });
1036
+ return /* @__PURE__ */ jsx("div", {
1037
+ ...props,
1038
+ "data-dashfoo": "tabcontent",
1039
+ style: mergedStyle
1040
+ });
1041
+ };
1042
+
1043
+ //#endregion
1044
+ //#region src/components/tab-overflow.tsx
1045
+ const OverflowIcon = () => /* @__PURE__ */ jsxs("svg", {
1046
+ "aria-hidden": "true",
1047
+ fill: "currentColor",
1048
+ height: "10",
1049
+ viewBox: "0 0 10 10",
1050
+ width: "10",
1051
+ children: [
1052
+ /* @__PURE__ */ jsx("circle", {
1053
+ cx: "2",
1054
+ cy: "5",
1055
+ r: "1"
1056
+ }),
1057
+ /* @__PURE__ */ jsx("circle", {
1058
+ cx: "5",
1059
+ cy: "5",
1060
+ r: "1"
1061
+ }),
1062
+ /* @__PURE__ */ jsx("circle", {
1063
+ cx: "8",
1064
+ cy: "5",
1065
+ r: "1"
1066
+ })
1067
+ ]
1068
+ });
1069
+ const menuStyle = {
1070
+ position: "absolute",
1071
+ right: 0,
1072
+ top: "100%",
1073
+ zIndex: 10
1074
+ };
1075
+ const TabOverflowMenu = ({ items, onSelect }) => {
1076
+ const [open, setOpen] = useState(false);
1077
+ const [activeIndex, setActiveIndex] = useState(0);
1078
+ const rootRef = useRef(null);
1079
+ const triggerRef = useRef(null);
1080
+ const itemRefs = useRef([]);
1081
+ useEffect(() => {
1082
+ if (!open) return;
1083
+ const handlePointerDown = (event) => {
1084
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false);
1085
+ };
1086
+ document.addEventListener("pointerdown", handlePointerDown);
1087
+ return () => {
1088
+ document.removeEventListener("pointerdown", handlePointerDown);
1089
+ };
1090
+ }, [open]);
1091
+ useEffect(() => {
1092
+ if (open) itemRefs.current[0]?.focus();
1093
+ }, [open]);
1094
+ const closeAndReturnFocus = () => {
1095
+ setOpen(false);
1096
+ triggerRef.current?.focus();
1097
+ };
1098
+ const focusItem = (index) => {
1099
+ setActiveIndex(index);
1100
+ itemRefs.current[index]?.focus();
1101
+ };
1102
+ const handleToggle = () => {
1103
+ setActiveIndex(0);
1104
+ setOpen((value) => !value);
1105
+ };
1106
+ const handleSelect = (id) => {
1107
+ onSelect(id);
1108
+ closeAndReturnFocus();
1109
+ };
1110
+ const handleMenuKeyDown = (event) => {
1111
+ const last = items.length - 1;
1112
+ switch (event.key) {
1113
+ case "ArrowDown":
1114
+ event.preventDefault();
1115
+ focusItem(activeIndex >= last ? 0 : activeIndex + 1);
1116
+ break;
1117
+ case "ArrowUp":
1118
+ event.preventDefault();
1119
+ focusItem(activeIndex <= 0 ? last : activeIndex - 1);
1120
+ break;
1121
+ case "Home":
1122
+ event.preventDefault();
1123
+ focusItem(0);
1124
+ break;
1125
+ case "End":
1126
+ event.preventDefault();
1127
+ focusItem(last);
1128
+ break;
1129
+ case "Escape":
1130
+ event.preventDefault();
1131
+ closeAndReturnFocus();
1132
+ break;
1133
+ default: break;
1134
+ }
1135
+ };
1136
+ const handleFocusOut = (event) => {
1137
+ if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
1138
+ setOpen(false);
1139
+ };
1140
+ return /* @__PURE__ */ jsxs("div", {
1141
+ "data-dashfoo": "tab-overflow-root",
1142
+ ref: rootRef,
1143
+ style: { position: "relative" },
1144
+ children: [/* @__PURE__ */ jsx("button", {
1145
+ "aria-expanded": open,
1146
+ "aria-haspopup": "menu",
1147
+ "aria-label": "More tabs",
1148
+ "data-dashfoo": "tab-overflow",
1149
+ onClick: handleToggle,
1150
+ ref: triggerRef,
1151
+ type: "button",
1152
+ children: /* @__PURE__ */ jsx(OverflowIcon, {})
1153
+ }), open ? /* @__PURE__ */ jsx("div", {
1154
+ "data-dashfoo": "tab-overflow-menu",
1155
+ onBlur: handleFocusOut,
1156
+ onKeyDown: handleMenuKeyDown,
1157
+ role: "menu",
1158
+ style: menuStyle,
1159
+ tabIndex: -1,
1160
+ children: items.map((item, index) => /* @__PURE__ */ jsx("button", {
1161
+ "data-dashfoo": "tab-overflow-item",
1162
+ onClick: () => handleSelect(item.id),
1163
+ ref: (node) => {
1164
+ itemRefs.current[index] = node;
1165
+ },
1166
+ role: "menuitem",
1167
+ tabIndex: index === activeIndex ? 0 : -1,
1168
+ type: "button",
1169
+ children: item.name
1170
+ }, item.id))
1171
+ }) : null]
1172
+ });
1173
+ };
1174
+
1175
+ //#endregion
1176
+ //#region src/components/tabset/tabset-overflow.tsx
1177
+ const TabsetOverflowMenu = () => {
1178
+ const overflowItems = useTabset((state) => state.overflowItems);
1179
+ const selectOverflowTab = useTabset((state) => state.selectOverflowTab);
1180
+ if (overflowItems.length === 0) return null;
1181
+ return /* @__PURE__ */ jsx(TabOverflowMenu, {
1182
+ items: overflowItems,
1183
+ onSelect: selectOverflowTab
1184
+ });
1185
+ };
1186
+
1187
+ //#endregion
1188
+ //#region src/hooks/use-tab-overflow.ts
1189
+ const overflowingIds = (tablist) => {
1190
+ const box = tablist.getBoundingClientRect();
1191
+ return [...tablist.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => {
1192
+ const rect = tab.getBoundingClientRect();
1193
+ return (rect.right > box.right + 1 || rect.left < box.left - 1) && tab.dataset.tabId ? [tab.dataset.tabId] : [];
1194
+ });
1195
+ };
1196
+ const useTabOverflow = (tablist, signature) => {
1197
+ const [overflow, setOverflow] = useState([]);
1198
+ useEffect(() => {
1199
+ const recompute = () => {
1200
+ setOverflow((previous) => {
1201
+ const next = tablist ? overflowingIds(tablist) : [];
1202
+ return next.length === 0 && previous.length === 0 ? previous : next;
1203
+ });
1204
+ };
1205
+ recompute();
1206
+ if (!tablist) return;
1207
+ const observer = new ResizeObserver(recompute);
1208
+ observer.observe(tablist);
1209
+ tablist.addEventListener("scroll", recompute, { passive: true });
1210
+ return () => {
1211
+ observer.disconnect();
1212
+ tablist.removeEventListener("scroll", recompute);
1213
+ };
1214
+ }, [tablist, signature]);
1215
+ return overflow;
1216
+ };
1217
+
1218
+ //#endregion
1219
+ //#region src/lib/tab-selection.ts
1220
+ const fallbackSelectedIndex = (count, removedIndex) => removedIndex + 1 < count ? removedIndex + 1 : removedIndex - 1;
1221
+
1222
+ //#endregion
1223
+ //#region src/components/tabset/tabset-root.tsx
1224
+ const tabsetStyle = {
1225
+ display: "flex",
1226
+ flexDirection: "column",
1227
+ height: "100%",
1228
+ minHeight: 0,
1229
+ minWidth: 0,
1230
+ width: "100%"
1231
+ };
1232
+ const TabsetRoot = ({ children, node, ref: userRef, style, ...props }) => {
1233
+ const closableTabs = useLayout((state) => state.closableTabs);
1234
+ const dispatch = useLayout((state) => state.dispatch);
1235
+ const maximizable = useLayout((state) => state.maximizable);
1236
+ const maximizedTabsetId = useLayout((state) => state.maximizedTabsetId);
1237
+ const renamableTabs = useLayout((state) => state.renamableTabs);
1238
+ const tabLocation = useLayout((state) => state.tabLocation);
1239
+ const { ref } = useTabsetDroppable(node.id);
1240
+ const dragSubject = useDragSubject();
1241
+ const tabsetRef = useRef(null);
1242
+ const isMaximized = maximizedTabsetId === node.id;
1243
+ const showMaximize = maximizable && node.enableMaximize !== false;
1244
+ const tabsClosable = closableTabs && node.enableClose !== false;
1245
+ const tabsRenamable = renamableTabs;
1246
+ const draggingTabIndex = dragSubject?.kind === "tab" ? node.children.findIndex((tab) => tab.id === dragSubject.id) : -1;
1247
+ const visualSelected = draggingTabIndex !== -1 && draggingTabIndex === node.selected ? fallbackSelectedIndex(node.children.length, draggingTabIndex) : node.selected;
1248
+ const activeTab = node.children[visualSelected];
1249
+ const isDragSource = dragSubject?.kind === "tabset" && dragSubject.id === node.id;
1250
+ const snapshot = {
1251
+ activeTab,
1252
+ dispatch,
1253
+ isMaximized,
1254
+ node,
1255
+ showMaximize,
1256
+ tabsClosable,
1257
+ tabsRenamable,
1258
+ visualSelected
1259
+ };
1260
+ const [store] = useState(() => createTabsetStore(snapshot));
1261
+ useLayoutEffect(() => {
1262
+ store.setState(snapshot);
1263
+ });
1264
+ const overflow = useTabOverflow(useStore(store, (state) => state.tablistElement), node.children.map((tab) => `${tab.id}:${tab.name}`).join("|"));
1265
+ const overflowItems = useMemo(() => {
1266
+ const names = new Map(node.children.map((tab) => [tab.id, tab.name]));
1267
+ return overflow.map((id) => ({
1268
+ id,
1269
+ name: names.get(id) ?? id
1270
+ }));
1271
+ }, [overflow, node.children]);
1272
+ useLayoutEffect(() => {
1273
+ store.setState({ overflowItems });
1274
+ }, [overflowItems, store]);
1275
+ useEffect(() => {
1276
+ if (!store.getState().restoreFocus) return;
1277
+ store.setState({ restoreFocus: false });
1278
+ const activeTabId = node.children[node.selected]?.id;
1279
+ if (activeTabId) tabsetRef.current?.querySelector(`#${CSS.escape(tabDomId(node.id, activeTabId))}`)?.focus();
1280
+ else tabsetRef.current?.focus();
1281
+ }, [
1282
+ node.children,
1283
+ node.selected,
1284
+ node.id,
1285
+ store
1286
+ ]);
1287
+ return /* @__PURE__ */ jsx(TabsetStoreContext.Provider, {
1288
+ value: store,
1289
+ children: /* @__PURE__ */ jsx("div", {
1290
+ ...props,
1291
+ "data-dashfoo": "tabset",
1292
+ "data-dragging-source": isDragSource || void 0,
1293
+ "data-tab-location": tabLocation,
1294
+ ref: (element) => {
1295
+ ref(element);
1296
+ tabsetRef.current = element;
1297
+ if (typeof userRef === "function") userRef(element);
1298
+ else if (userRef) userRef.current = element;
1299
+ },
1300
+ style: {
1301
+ ...tabsetStyle,
1302
+ ...style
1303
+ },
1304
+ tabIndex: -1,
1305
+ children
1306
+ })
1307
+ });
1308
+ };
1309
+
1310
+ //#endregion
1311
+ //#region src/lib/merge-refs.ts
1312
+ const applyRef = (ref, value) => {
1313
+ if (typeof ref === "function") ref(value);
1314
+ else if (ref) ref.current = value;
1315
+ };
1316
+ const mergeRefs = (...refs) => (value) => {
1317
+ for (const ref of refs) applyRef(ref, value);
1318
+ };
1319
+
1320
+ //#endregion
1321
+ //#region src/components/tabset/tabset-tab.tsx
1322
+ const TabsetTab = ({ children, ref: userRef, tab, ...props }) => {
1323
+ const store = useContext(TabsetStoreContext);
1324
+ const node = useTabset((state) => state.node);
1325
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1326
+ const dragSubject = useDragSubject();
1327
+ const isDragging = dragSubject?.kind === "tab" && dragSubject.id === tab.id;
1328
+ const itemRef = useRef(null);
1329
+ const wasEditing = useRef(false);
1330
+ const index = node.children.findIndex((child) => child.id === tab.id);
1331
+ useEffect(() => {
1332
+ 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`);
1333
+ }, [
1334
+ index === -1,
1335
+ store,
1336
+ tab.id
1337
+ ]);
1338
+ useEffect(() => {
1339
+ if (wasEditing.current && !editing) itemRef.current?.querySelector("[data-dashfoo=\"tab\"]")?.focus();
1340
+ wasEditing.current = editing;
1341
+ }, [editing]);
1342
+ const contextValue = useMemo(() => ({
1343
+ index,
1344
+ tab
1345
+ }), [index, tab]);
1346
+ const refCallback = useMemo(() => mergeRefs(itemRef, userRef), [userRef]);
1347
+ return /* @__PURE__ */ jsx(TabContext.Provider, {
1348
+ value: contextValue,
1349
+ children: /* @__PURE__ */ jsx("span", {
1350
+ ...props,
1351
+ "data-dashfoo": "tab-item",
1352
+ "data-dragging": isDragging || void 0,
1353
+ ref: refCallback,
1354
+ children
1355
+ })
1356
+ });
1357
+ };
1358
+ const TabsetTrigger = ({ children, onClick, onDoubleClick, ref: userRef, ...props }) => {
1359
+ const { index, tab } = useTab();
1360
+ const node = useTabset((state) => state.node);
1361
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1362
+ const selectTab = useTabset((state) => state.selectTab);
1363
+ const startEditing = useTabset((state) => state.startEditing);
1364
+ const tabsRenamable = useTabset((state) => state.tabsRenamable);
1365
+ const visualSelected = useTabset((state) => state.visualSelected);
1366
+ const draggableTabs = useLayout((state) => state.draggableTabs);
1367
+ const renderTabLabel = useLayout((state) => state.renderTabLabel);
1368
+ const { ref } = useTabDraggable(tab.id, !draggableTabs || tab.enableDrag === false, tab.name);
1369
+ const refCallback = useMemo(() => mergeRefs(ref, userRef), [ref, userRef]);
1370
+ if (editing) return null;
1371
+ const selected = index === visualSelected;
1372
+ const renamable = tabsRenamable && tab.enableRename !== false;
1373
+ const handleClick = (event) => {
1374
+ onClick?.(event);
1375
+ selectTab(index);
1376
+ };
1377
+ const handleDoubleClick = (event) => {
1378
+ onDoubleClick?.(event);
1379
+ if (renamable) startEditing(tab.id);
1380
+ };
1381
+ return /* @__PURE__ */ jsx("button", {
1382
+ "aria-label": renderTabLabel ? tab.name : void 0,
1383
+ ...props,
1384
+ "aria-controls": panelDomId(node.id),
1385
+ "aria-selected": selected,
1386
+ "data-dashfoo": "tab",
1387
+ "data-tab-id": tab.id,
1388
+ id: tabDomId(node.id, tab.id),
1389
+ onClick: handleClick,
1390
+ onDoubleClick: handleDoubleClick,
1391
+ ref: refCallback,
1392
+ role: "tab",
1393
+ tabIndex: selected ? 0 : -1,
1394
+ type: "button",
1395
+ children: children ?? (renderTabLabel ? renderTabLabel(tab) : tab.name)
1396
+ });
1397
+ };
1398
+
1399
+ //#endregion
1400
+ //#region src/components/tabset-icons.tsx
1401
+ const iconStyle = { pointerEvents: "none" };
1402
+ const Icon = ({ children }) => /* @__PURE__ */ jsx("svg", {
1403
+ "aria-hidden": "true",
1404
+ fill: "none",
1405
+ height: "12",
1406
+ stroke: "currentColor",
1407
+ strokeLinecap: "round",
1408
+ strokeLinejoin: "round",
1409
+ strokeWidth: "2",
1410
+ style: iconStyle,
1411
+ viewBox: "0 0 24 24",
1412
+ width: "12",
1413
+ children
1414
+ });
1415
+ const CloseIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] });
1416
+ const GripIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
1417
+ /* @__PURE__ */ jsx("circle", {
1418
+ cx: "12",
1419
+ cy: "5",
1420
+ r: "1"
1421
+ }),
1422
+ /* @__PURE__ */ jsx("circle", {
1423
+ cx: "19",
1424
+ cy: "5",
1425
+ r: "1"
1426
+ }),
1427
+ /* @__PURE__ */ jsx("circle", {
1428
+ cx: "5",
1429
+ cy: "5",
1430
+ r: "1"
1431
+ }),
1432
+ /* @__PURE__ */ jsx("circle", {
1433
+ cx: "12",
1434
+ cy: "12",
1435
+ r: "1"
1436
+ }),
1437
+ /* @__PURE__ */ jsx("circle", {
1438
+ cx: "19",
1439
+ cy: "12",
1440
+ r: "1"
1441
+ }),
1442
+ /* @__PURE__ */ jsx("circle", {
1443
+ cx: "5",
1444
+ cy: "12",
1445
+ r: "1"
1446
+ }),
1447
+ /* @__PURE__ */ jsx("circle", {
1448
+ cx: "12",
1449
+ cy: "19",
1450
+ r: "1"
1451
+ }),
1452
+ /* @__PURE__ */ jsx("circle", {
1453
+ cx: "19",
1454
+ cy: "19",
1455
+ r: "1"
1456
+ }),
1457
+ /* @__PURE__ */ jsx("circle", {
1458
+ cx: "5",
1459
+ cy: "19",
1460
+ r: "1"
1461
+ })
1462
+ ] });
1463
+ const MaximizeIcon = ({ maximized }) => maximized ? /* @__PURE__ */ jsxs(Icon, { children: [
1464
+ /* @__PURE__ */ jsx("path", { d: "m14 10 7-7" }),
1465
+ /* @__PURE__ */ jsx("path", { d: "M20 10h-6V4" }),
1466
+ /* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
1467
+ /* @__PURE__ */ jsx("path", { d: "M4 14h6v6" })
1468
+ ] }) : /* @__PURE__ */ jsxs(Icon, { children: [
1469
+ /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
1470
+ /* @__PURE__ */ jsx("path", { d: "m21 3-7 7" }),
1471
+ /* @__PURE__ */ jsx("path", { d: "m3 21 7-7" }),
1472
+ /* @__PURE__ */ jsx("path", { d: "M9 21H3v-6" })
1473
+ ] });
1474
+ const FloatIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [/* @__PURE__ */ jsx("rect", {
1475
+ height: "14",
1476
+ rx: "2",
1477
+ width: "18",
1478
+ x: "3",
1479
+ y: "6"
1480
+ }), /* @__PURE__ */ jsx("path", { d: "M3 10h18" })] });
1481
+ const MinimizeIcon = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
1482
+ const DockIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
1483
+ /* @__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" }),
1484
+ /* @__PURE__ */ jsx("path", { d: "M11 8 7 12l4 4" }),
1485
+ /* @__PURE__ */ jsx("path", { d: "M7 12h10" })
1486
+ ] });
1487
+
1488
+ //#endregion
1489
+ //#region src/components/tabset/tabset-tab-controls.tsx
1490
+ const RenameEditor = ({ defaultValue, onBlur, onKeyDown, ref: userRef, ...props }) => {
1491
+ const { tab } = useTab();
1492
+ const cancelRename = useTabset((state) => state.cancelRename);
1493
+ const commitRename = useTabset((state) => state.commitRename);
1494
+ const ref = useRef(null);
1495
+ const done = useRef(false);
1496
+ const refCallback = useMemo(() => mergeRefs(ref, userRef), [userRef]);
1497
+ useEffect(() => {
1498
+ ref.current?.focus();
1499
+ ref.current?.select();
1500
+ }, []);
1501
+ const handleKeyDown = (event) => {
1502
+ onKeyDown?.(event);
1503
+ if (event.key === "Enter") {
1504
+ done.current = true;
1505
+ commitRename(tab.id, event.currentTarget.value);
1506
+ } else if (event.key === "Escape") {
1507
+ done.current = true;
1508
+ cancelRename();
1509
+ }
1510
+ };
1511
+ const handleCommitOnBlur = (event) => {
1512
+ onBlur?.(event);
1513
+ if (done.current) return;
1514
+ done.current = true;
1515
+ commitRename(tab.id, event.currentTarget.value);
1516
+ };
1517
+ return /* @__PURE__ */ jsx("input", {
1518
+ "aria-label": `Rename ${tab.name}`,
1519
+ ...props,
1520
+ "data-dashfoo": "tab-rename",
1521
+ defaultValue: defaultValue ?? tab.name,
1522
+ onBlur: handleCommitOnBlur,
1523
+ onKeyDown: handleKeyDown,
1524
+ ref: refCallback,
1525
+ type: "text"
1526
+ });
1527
+ };
1528
+ const TabsetRenameInput = (props) => {
1529
+ const { tab } = useTab();
1530
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1531
+ const registerRenameInput = useTabset((state) => state.registerRenameInput);
1532
+ useEffect(() => registerRenameInput(), [registerRenameInput]);
1533
+ if (!editing) return null;
1534
+ return /* @__PURE__ */ jsx(RenameEditor, { ...props });
1535
+ };
1536
+ const TabsetCloseButton = ({ children, onClick, ...props }) => {
1537
+ const { index, tab } = useTab();
1538
+ const closeTab = useTabset((state) => state.closeTab);
1539
+ const editing = useTabset((state) => state.editingTabId === tab.id);
1540
+ const tabsClosable = useTabset((state) => state.tabsClosable);
1541
+ const visualSelected = useTabset((state) => state.visualSelected);
1542
+ if (!(tabsClosable && tab.enableClose !== false) || editing) return null;
1543
+ const handleClick = (event) => {
1544
+ onClick?.(event);
1545
+ event.stopPropagation();
1546
+ closeTab(tab.id);
1547
+ };
1548
+ return /* @__PURE__ */ jsx("button", {
1549
+ "aria-label": `Close ${tab.name}`,
1550
+ ...props,
1551
+ "data-dashfoo": "tab-close",
1552
+ onClick: handleClick,
1553
+ tabIndex: index === visualSelected ? 0 : -1,
1554
+ type: "button",
1555
+ children: children ?? /* @__PURE__ */ jsx(CloseIcon, {})
1556
+ });
1557
+ };
1558
+
1559
+ //#endregion
1560
+ //#region src/components/tabset/tabset-tablist.tsx
1561
+ const stripStyle = {
1562
+ display: "flex",
1563
+ flexShrink: 0
1564
+ };
1565
+ const tablistStyle = {
1566
+ display: "flex",
1567
+ minWidth: 0,
1568
+ overflowX: "auto"
1569
+ };
1570
+ const TabsetTabStrip = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
1571
+ ...props,
1572
+ "data-dashfoo": "tabstrip",
1573
+ style: {
1574
+ ...stripStyle,
1575
+ ...style
1576
+ }
1577
+ });
1578
+ const TabsetTablist = ({ onKeyDown, ref: userRef, style, ...props }) => {
1579
+ const node = useTabset((state) => state.node);
1580
+ const registerTablist = useTabset((state) => state.registerTablist);
1581
+ const selectTab = useTabset((state) => state.selectTab);
1582
+ const handleKeyDown = (event) => {
1583
+ onKeyDown?.(event);
1584
+ if (event.defaultPrevented) return;
1585
+ if (!(event.target instanceof HTMLElement) || !event.target.closest("[role=\"tab\"]")) return;
1586
+ const count = node.children.length;
1587
+ if (count === 0) return;
1588
+ const next = {
1589
+ ArrowLeft: (node.selected - 1 + count) % count,
1590
+ ArrowRight: (node.selected + 1) % count,
1591
+ End: count - 1,
1592
+ Home: 0
1593
+ }[event.key];
1594
+ if (next === void 0) return;
1595
+ event.preventDefault();
1596
+ selectTab(next, { focus: true });
1597
+ };
1598
+ const refCallback = useMemo(() => mergeRefs(registerTablist, userRef), [registerTablist, userRef]);
1599
+ return /* @__PURE__ */ jsx("div", {
1600
+ "aria-label": node.name ?? "Tabs",
1601
+ ...props,
1602
+ "aria-orientation": "horizontal",
1603
+ "data-dashfoo": "tablist",
1604
+ onKeyDown: handleKeyDown,
1605
+ ref: refCallback,
1606
+ role: "tablist",
1607
+ style: {
1608
+ ...tablistStyle,
1609
+ ...style
1610
+ },
1611
+ tabIndex: -1
1612
+ });
1613
+ };
1614
+
1615
+ //#endregion
1616
+ //#region src/components/tabset/tabset-toolbar.tsx
1617
+ const toolbarStyle = {
1618
+ alignItems: "center",
1619
+ display: "flex",
1620
+ flexShrink: 0,
1621
+ marginInlineStart: "auto"
1622
+ };
1623
+ const TabsetToolbar = ({ style, ...props }) => /* @__PURE__ */ jsx("div", {
1624
+ ...props,
1625
+ "data-dashfoo": "tabset-toolbar",
1626
+ style: {
1627
+ ...toolbarStyle,
1628
+ ...style
1629
+ }
1630
+ });
1631
+ const TabsetGrip = ({ children, ref: userRef, ...props }) => {
1632
+ const activeTabName = useTabset((state) => state.activeTab?.name ?? "");
1633
+ const isMaximized = useTabset((state) => state.isMaximized);
1634
+ const node = useTabset((state) => state.node);
1635
+ const hidden = !useLayout((state) => state.draggableTabsets) || isMaximized;
1636
+ const { ref } = useTabsetDraggable(node.id, hidden, activeTabName);
1637
+ const refCallback = useMemo(() => mergeRefs(ref, userRef), [ref, userRef]);
1638
+ if (hidden) return null;
1639
+ return /* @__PURE__ */ jsx("button", {
1640
+ "aria-label": "Move tabset",
1641
+ ...props,
1642
+ "data-dashfoo": "tabset-grip",
1643
+ ref: refCallback,
1644
+ type: "button",
1645
+ children: children ?? /* @__PURE__ */ jsx(GripIcon, {})
1646
+ });
1647
+ };
1648
+ const TabsetMaximizeButton = ({ children, onClick, ...props }) => {
1649
+ const isMaximized = useTabset((state) => state.isMaximized);
1650
+ const showMaximize = useTabset((state) => state.showMaximize);
1651
+ const toggleMaximize = useTabset((state) => state.toggleMaximize);
1652
+ if (!showMaximize) return null;
1653
+ const handleClick = (event) => {
1654
+ onClick?.(event);
1655
+ toggleMaximize();
1656
+ };
1657
+ return /* @__PURE__ */ jsx("button", {
1658
+ "aria-label": isMaximized ? "Restore" : "Maximize",
1659
+ ...props,
1660
+ "aria-pressed": isMaximized,
1661
+ "data-dashfoo": "tabset-maximize",
1662
+ onClick: handleClick,
1663
+ type: "button",
1664
+ children: children ?? /* @__PURE__ */ jsx(MaximizeIcon, { maximized: isMaximized })
1665
+ });
1666
+ };
1667
+ const TabsetFloatButton = ({ children, onClick, ...props }) => {
1668
+ const floatable = useLayout((state) => state.floatable);
1669
+ const dispatch = useLayout((state) => state.dispatch);
1670
+ const isMaximized = useTabset((state) => state.isMaximized);
1671
+ const node = useTabset((state) => state.node);
1672
+ const hasFloatLayer = useHasFloatLayer();
1673
+ if (!floatable || isMaximized) return null;
1674
+ if (!hasFloatLayer) {
1675
+ warnOnce("float-no-layer", "Tabset.FloatButton needs a <Layout.FloatLayer> around the layout (DashfooLayout adds one); the float control is hidden");
1676
+ return null;
1677
+ }
1678
+ const handleClick = (event) => {
1679
+ onClick?.(event);
1680
+ dispatch({
1681
+ geometry: measureFloatRect(event.currentTarget),
1682
+ tabsetId: node.id,
1683
+ type: "floatTabset"
1684
+ });
1685
+ };
1686
+ return /* @__PURE__ */ jsx("button", {
1687
+ "aria-label": "Float panel",
1688
+ ...props,
1689
+ "data-dashfoo": "tabset-float",
1690
+ onClick: handleClick,
1691
+ type: "button",
1692
+ children: children ?? /* @__PURE__ */ jsx(FloatIcon, {})
1693
+ });
1694
+ };
1695
+
1696
+ //#endregion
1697
+ //#region src/components/tabset/tabset-view.tsx
1698
+ const tabsetContent = /* @__PURE__ */ jsx(TabsetContent, {});
1699
+ const DefaultTabsetLayout = () => {
1700
+ const draggableTabsets = useLayout((state) => state.draggableTabsets);
1701
+ const floatable = useLayout((state) => state.floatable);
1702
+ const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
1703
+ const tabLocation = useLayout((state) => state.tabLocation);
1704
+ const tabStripEnabled = useLayout((state) => state.tabStripEnabled);
1705
+ const isMaximized = useTabset((state) => state.isMaximized);
1706
+ const node = useTabset((state) => state.node);
1707
+ const overflowCount = useTabset((state) => state.overflowItems.length);
1708
+ const showToolbar = useTabset((state) => state.showMaximize) || renderTabsetToolbar !== void 0 || overflowCount > 0 || floatable && !isMaximized || draggableTabsets && !isMaximized;
1709
+ const strip = tabStripEnabled ? /* @__PURE__ */ jsxs(TabsetTabStrip, { children: [/* @__PURE__ */ jsx(TabsetTablist, { children: node.children.map((tab) => /* @__PURE__ */ jsxs(TabsetTab, {
1710
+ tab,
1711
+ children: [
1712
+ /* @__PURE__ */ jsx(TabsetTrigger, {}),
1713
+ /* @__PURE__ */ jsx(TabsetRenameInput, {}),
1714
+ /* @__PURE__ */ jsx(TabsetCloseButton, {})
1715
+ ]
1716
+ }, tab.id)) }), showToolbar ? /* @__PURE__ */ jsxs(TabsetToolbar, { children: [
1717
+ /* @__PURE__ */ jsx(TabsetOverflowMenu, {}),
1718
+ /* @__PURE__ */ jsx(TabsetGrip, {}),
1719
+ renderTabsetToolbar?.(node),
1720
+ /* @__PURE__ */ jsx(TabsetFloatButton, {}),
1721
+ /* @__PURE__ */ jsx(TabsetMaximizeButton, {})
1722
+ ] }) : null] }) : null;
1723
+ return tabLocation === "bottom" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [tabsetContent, strip] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [strip, tabsetContent] });
1724
+ };
1725
+ const TabsetView = ({ node }) => /* @__PURE__ */ jsx(TabsetRoot, {
1726
+ node,
1727
+ children: /* @__PURE__ */ jsx(DefaultTabsetLayout, {})
1728
+ });
1729
+
1730
+ //#endregion
1731
+ //#region src/components/row-view.tsx
1732
+ const dimensionToSize = (dimension) => `${dimension.value}${dimension.unit}`;
1733
+ const dimensionToPixels = (dimension) => dimension?.unit === "px" ? dimension.value : void 0;
1734
+ const groupStyle = {
1735
+ display: "flex",
1736
+ flex: 1,
1737
+ minHeight: 0,
1738
+ minWidth: 0
1739
+ };
1740
+ const WEIGHT_EPSILON = .01;
1741
+ const layoutFromWeights = (children, total) => children.reduce((layout, child) => {
1742
+ layout[child.id] = (child.weight ?? 1) / total * 100;
1743
+ return layout;
1744
+ }, {});
1745
+ const orderedFromLayout = (layout, children) => children.map((child) => layout[child.id] ?? 0);
1746
+ const layoutFromOrdered = (sizes, children) => children.reduce((layout, child, index) => {
1747
+ layout[child.id] = sizes[index] ?? 0;
1748
+ return layout;
1749
+ }, {});
1750
+ const layoutsMatch = (current, expected) => {
1751
+ const currentIds = Object.keys(current);
1752
+ const expectedIds = Object.keys(expected);
1753
+ if (currentIds.length !== expectedIds.length) return false;
1754
+ return expectedIds.every((id) => Math.abs((current[id] ?? NaN) - expected[id]) <= WEIGHT_EPSILON);
1755
+ };
1756
+ const descendantMinSize = (child, axis, tabsetMinSize) => {
1757
+ const explicit = dimensionToPixels(child.min);
1758
+ if (explicit !== void 0) return explicit;
1759
+ if (child.min !== void 0) return;
1760
+ if (child.type === "tabset") return tabsetMinSize;
1761
+ const childMinimums = [];
1762
+ for (const grandchild of child.children) {
1763
+ const minimum = descendantMinSize(grandchild, axis, tabsetMinSize);
1764
+ if (minimum === void 0) return;
1765
+ childMinimums.push(minimum);
1766
+ }
1767
+ if (childMinimums.length === 0) return;
1768
+ return child.orientation === axis ? childMinimums.reduce((sum, value) => sum + value, 0) : Math.max(...childMinimums);
1769
+ };
1770
+ const RowView = ({ node, renderTabset }) => {
1771
+ const dispatch = useLayout((state) => state.dispatch);
1772
+ const resizableSplits = useLayout((state) => state.resizableSplits);
1773
+ const tabsetMinSize = useLayout((state) => state.tabsetMinSize);
1774
+ const globalSnap = useLayout((state) => state.snap);
1775
+ const effectiveSnap = node.snap ?? globalSnap;
1776
+ const snapActive = snapEnabled(effectiveSnap);
1777
+ const orientation = node.orientation === "row" ? "horizontal" : "vertical";
1778
+ const total = node.children.reduce((sum, child) => sum + (child.weight ?? 1), 0);
1779
+ const desiredLayout = useMemo(() => layoutFromWeights(node.children, total), [node.children, total]);
1780
+ const measured = useRef(false);
1781
+ const groupRef = useRef(null);
1782
+ const groupElement = useRef(null);
1783
+ const syncing = useRef(false);
1784
+ const activeBoundary = useRef(null);
1785
+ const snapping = useRef(false);
1786
+ const snappedBoundaryRef = useRef(null);
1787
+ const [snappedBoundary, setSnappedBoundary] = useState(null);
1788
+ const setHighlight = (boundary) => {
1789
+ if (boundary !== snappedBoundaryRef.current) {
1790
+ snappedBoundaryRef.current = boundary;
1791
+ setSnappedBoundary(boundary);
1792
+ }
1793
+ };
1794
+ const setSnapTransition = (active) => {
1795
+ const element = groupElement.current;
1796
+ if (!element) return;
1797
+ if (active) element.dataset.dashfooSnapping = "true";
1798
+ else delete element.dataset.dashfooSnapping;
1799
+ };
1800
+ useLayoutEffect(() => {
1801
+ const group = groupRef.current;
1802
+ if (!group) return;
1803
+ const current = group.getLayout();
1804
+ if (!(Object.keys(current).length === Object.keys(desiredLayout).length && Object.keys(desiredLayout).every((id) => id in current)) || layoutsMatch(current, desiredLayout)) return;
1805
+ syncing.current = true;
1806
+ group.setLayout(desiredLayout);
1807
+ const frame = requestAnimationFrame(() => {
1808
+ syncing.current = false;
1809
+ });
1810
+ return () => {
1811
+ cancelAnimationFrame(frame);
1812
+ syncing.current = false;
1813
+ };
1814
+ }, [desiredLayout]);
1815
+ const handleLayoutChange = (layout) => {
1816
+ if (snapping.current || syncing.current || !resizableSplits) return;
1817
+ if (effectiveSnap === null || !snapActive) return;
1818
+ const boundary = activeBoundary.current;
1819
+ if (boundary === null) return;
1820
+ const { sizes, snapped } = snapSizes(orderedFromLayout(layout, node.children), boundary, effectiveSnap);
1821
+ setHighlight(snapped ? boundary : null);
1822
+ if (!snapped) {
1823
+ setSnapTransition(false);
1824
+ return;
1825
+ }
1826
+ setSnapTransition(true);
1827
+ const group = groupRef.current;
1828
+ const target = layoutFromOrdered(sizes, node.children);
1829
+ if (!group || layoutsMatch(layout, target)) return;
1830
+ snapping.current = true;
1831
+ group.setLayout(target);
1832
+ snapping.current = false;
1833
+ };
1834
+ const handleLayoutChanged = (layout) => {
1835
+ if (!measured.current) {
1836
+ measured.current = true;
1837
+ return;
1838
+ }
1839
+ if (syncing.current) return;
1840
+ const boundary = activeBoundary.current;
1841
+ activeBoundary.current = null;
1842
+ setHighlight(null);
1843
+ setSnapTransition(false);
1844
+ if (!resizableSplits) return;
1845
+ let weights = node.children.map((child) => layout[child.id] ?? child.weight ?? 1);
1846
+ if (boundary !== null && effectiveSnap !== null && snapActive) weights = snapSizes(weights, boundary, effectiveSnap).sizes;
1847
+ if (!weights.some((weight, index) => Math.abs(weight - (node.children[index]?.weight ?? 1)) > WEIGHT_EPSILON)) return;
1848
+ dispatch({
1849
+ rowId: node.id,
1850
+ type: "adjustSplit",
1851
+ weights
1852
+ });
1853
+ };
1854
+ return /* @__PURE__ */ jsx(Group, {
1855
+ "data-dashfoo": "row",
1856
+ disabled: !resizableSplits,
1857
+ elementRef: groupElement,
1858
+ groupRef,
1859
+ onLayoutChange: snapActive ? handleLayoutChange : void 0,
1860
+ onLayoutChanged: handleLayoutChanged,
1861
+ orientation,
1862
+ style: groupStyle,
1863
+ children: node.children.map((child, index) => {
1864
+ const percent = (child.weight ?? 1) / total * 100;
1865
+ let min = child.min ? dimensionToSize(child.min) : void 0;
1866
+ if (min === void 0) {
1867
+ const minimum = descendantMinSize(child, node.orientation, tabsetMinSize);
1868
+ min = minimum === void 0 ? void 0 : `${minimum}px`;
1869
+ }
1870
+ const max = child.max ? dimensionToSize(child.max) : void 0;
1871
+ return /* @__PURE__ */ jsxs(Fragment, { children: [index > 0 ? /* @__PURE__ */ jsx(Separator, {
1872
+ "data-dashfoo": "splitter",
1873
+ "data-dashfoo-snapped": snappedBoundary === index - 1 ? "true" : void 0,
1874
+ disabled: !resizableSplits,
1875
+ onPointerDown: snapActive && resizableSplits ? () => {
1876
+ activeBoundary.current = index - 1;
1877
+ } : void 0
1878
+ }) : null, /* @__PURE__ */ jsx(Panel$1, {
1879
+ defaultSize: `${percent}%`,
1880
+ id: child.id,
1881
+ maxSize: max,
1882
+ minSize: min,
1883
+ children: child.type === "row" ? /* @__PURE__ */ jsx(RowView, {
1884
+ node: child,
1885
+ renderTabset
1886
+ }) : renderTabset?.(child) ?? /* @__PURE__ */ jsx(TabsetView, { node: child })
1887
+ })] }, child.id);
1888
+ })
1889
+ }, node.id);
1890
+ };
1891
+
1892
+ //#endregion
1893
+ //#region src/components/float-panel.tsx
1894
+ const floatTitle = (node) => node.name ?? "Panel";
1895
+ const titleBarStyle = {
1896
+ alignItems: "center",
1897
+ display: "flex",
1898
+ flexShrink: 0,
1899
+ gap: "0.375rem",
1900
+ touchAction: "none"
1901
+ };
1902
+ const titleStyle = {
1903
+ flex: 1,
1904
+ minWidth: 0,
1905
+ overflow: "hidden",
1906
+ textOverflow: "ellipsis",
1907
+ whiteSpace: "nowrap"
1908
+ };
1909
+ const dockButtonStyle = {
1910
+ alignItems: "center",
1911
+ cursor: "pointer",
1912
+ display: "inline-flex",
1913
+ flexShrink: 0
1914
+ };
1915
+ const bodyStyle = {
1916
+ flex: 1,
1917
+ minHeight: 0,
1918
+ position: "relative"
1919
+ };
1920
+ const RESIZE_HANDLES = [
1921
+ {
1922
+ edges: {
1923
+ x: 0,
1924
+ y: -1
1925
+ },
1926
+ key: "n",
1927
+ style: {
1928
+ cursor: "ns-resize",
1929
+ height: 6,
1930
+ insetInline: 10,
1931
+ top: -3
1932
+ }
1933
+ },
1934
+ {
1935
+ edges: {
1936
+ x: 0,
1937
+ y: 1
1938
+ },
1939
+ key: "s",
1940
+ style: {
1941
+ bottom: -3,
1942
+ cursor: "ns-resize",
1943
+ height: 6,
1944
+ insetInline: 10
1945
+ }
1946
+ },
1947
+ {
1948
+ edges: {
1949
+ x: 1,
1950
+ y: 0
1951
+ },
1952
+ key: "e",
1953
+ style: {
1954
+ cursor: "ew-resize",
1955
+ insetBlock: 10,
1956
+ right: -3,
1957
+ width: 6
1958
+ }
1959
+ },
1960
+ {
1961
+ edges: {
1962
+ x: -1,
1963
+ y: 0
1964
+ },
1965
+ key: "w",
1966
+ style: {
1967
+ cursor: "ew-resize",
1968
+ insetBlock: 10,
1969
+ left: -3,
1970
+ width: 6
1971
+ }
1972
+ },
1973
+ {
1974
+ edges: {
1975
+ x: 1,
1976
+ y: -1
1977
+ },
1978
+ key: "ne",
1979
+ style: {
1980
+ cursor: "nesw-resize",
1981
+ height: 14,
1982
+ right: -4,
1983
+ top: -4,
1984
+ width: 14
1985
+ }
1986
+ },
1987
+ {
1988
+ edges: {
1989
+ x: -1,
1990
+ y: -1
1991
+ },
1992
+ key: "nw",
1993
+ style: {
1994
+ cursor: "nwse-resize",
1995
+ height: 14,
1996
+ left: -4,
1997
+ top: -4,
1998
+ width: 14
1999
+ }
2000
+ },
2001
+ {
2002
+ edges: {
2003
+ x: 1,
2004
+ y: 1
2005
+ },
2006
+ key: "se",
2007
+ style: {
2008
+ bottom: -4,
2009
+ cursor: "nwse-resize",
2010
+ height: 14,
2011
+ right: -4,
2012
+ width: 14
2013
+ }
2014
+ },
2015
+ {
2016
+ edges: {
2017
+ x: -1,
2018
+ y: 1
2019
+ },
2020
+ key: "sw",
2021
+ style: {
2022
+ bottom: -4,
2023
+ cursor: "nesw-resize",
2024
+ height: 14,
2025
+ left: -4,
2026
+ width: 14
2027
+ }
2028
+ }
2029
+ ];
2030
+ const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
2031
+ const CHIP_SIZE = {
2032
+ height: 34,
2033
+ width: 168
2034
+ };
2035
+ const TAP_SLOP = 4;
2036
+ const FloatTitleEditor = ({ dispatch, node, onDone }) => {
2037
+ const ref = useRef(null);
2038
+ const done = useRef(false);
2039
+ useEffect(() => {
2040
+ ref.current?.focus();
2041
+ ref.current?.select();
2042
+ }, []);
2043
+ const commit = (value) => {
2044
+ const trimmed = value.trim();
2045
+ if (trimmed && trimmed !== floatTitle(node)) dispatch({
2046
+ floatId: node.id,
2047
+ name: trimmed,
2048
+ type: "renameFloat"
2049
+ });
2050
+ onDone();
2051
+ };
2052
+ const handleKeyDown = (event) => {
2053
+ if (event.key === "Enter") {
2054
+ done.current = true;
2055
+ commit(event.currentTarget.value);
2056
+ } else if (event.key === "Escape") {
2057
+ done.current = true;
2058
+ onDone();
2059
+ }
2060
+ };
2061
+ const handleBlur = (event) => {
2062
+ if (done.current) return;
2063
+ done.current = true;
2064
+ commit(event.currentTarget.value);
2065
+ };
2066
+ return /* @__PURE__ */ jsx("input", {
2067
+ "aria-label": `Rename ${floatTitle(node)}`,
2068
+ "data-dashfoo": "float-rename",
2069
+ defaultValue: floatTitle(node),
2070
+ onBlur: handleBlur,
2071
+ onKeyDown: handleKeyDown,
2072
+ onPointerDown: (event) => event.stopPropagation(),
2073
+ ref,
2074
+ style: titleStyle,
2075
+ type: "text"
2076
+ });
2077
+ };
2078
+ const FloatTitle = ({ dispatch, node, renamable }) => {
2079
+ const [editing, setEditing] = useState(false);
2080
+ if (renamable && editing) return /* @__PURE__ */ jsx(FloatTitleEditor, {
2081
+ dispatch,
2082
+ node,
2083
+ onDone: () => setEditing(false)
2084
+ });
2085
+ return /* @__PURE__ */ jsx("span", {
2086
+ "data-dashfoo": "float-title",
2087
+ onDoubleClick: renamable ? () => setEditing(true) : void 0,
2088
+ style: titleStyle,
2089
+ title: renamable ? "Double-click to rename" : void 0,
2090
+ children: floatTitle(node)
2091
+ });
2092
+ };
2093
+ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2094
+ const dispatch = useLayout((state) => state.dispatch);
2095
+ const closableTabs = useLayout((state) => state.closableTabs);
2096
+ const draggableTabs = useLayout((state) => state.draggableTabs);
2097
+ const editable = useLayout((state) => state.editable);
2098
+ const renamableTabs = useLayout((state) => state.renamableTabs);
2099
+ const resizableSplits = useLayout((state) => state.resizableSplits);
2100
+ const keepMounted = useLayout((state) => state.keepMounted);
2101
+ const renderTab = useLayout((state) => state.renderTab);
2102
+ const renderTabLabel = useLayout((state) => state.renderTabLabel);
2103
+ const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
2104
+ const snap = useLayout((state) => state.snap);
2105
+ const panelRef = useRef(null);
2106
+ const gestureRef = useRef(null);
2107
+ const latestRef = useRef(node.geometry);
2108
+ const movedRef = useRef(false);
2109
+ const setPanel = (element) => {
2110
+ panelRef.current = element;
2111
+ };
2112
+ const restore = () => {
2113
+ if (!editable) return;
2114
+ dispatch({
2115
+ floatId: node.id,
2116
+ minimized: false,
2117
+ type: "setFloatMinimized"
2118
+ });
2119
+ };
2120
+ const handlePointerDown = (event) => {
2121
+ const panel = panelRef.current;
2122
+ if (!panel || !editable) return;
2123
+ movedRef.current = false;
2124
+ const edgeKey = event.currentTarget.dataset.edge;
2125
+ const parent = panel.offsetParent;
2126
+ gestureRef.current = {
2127
+ bounds: {
2128
+ height: parent?.clientHeight ?? 0,
2129
+ width: parent?.clientWidth ?? 0
2130
+ },
2131
+ edges: edgeKey ? EDGE_BY_KEY.get(edgeKey) ?? null : null,
2132
+ pointerId: event.pointerId,
2133
+ start: node.geometry,
2134
+ startX: event.clientX,
2135
+ startY: event.clientY
2136
+ };
2137
+ latestRef.current = node.geometry;
2138
+ };
2139
+ const handlePointerMove = (event) => {
2140
+ const gesture = gestureRef.current;
2141
+ const panel = panelRef.current;
2142
+ if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2143
+ const dx = event.clientX - gesture.startX;
2144
+ const dy = event.clientY - gesture.startY;
2145
+ if (!movedRef.current) {
2146
+ if (Math.hypot(dx, dy) <= TAP_SLOP) return;
2147
+ movedRef.current = true;
2148
+ panel.setPointerCapture(gesture.pointerId);
2149
+ }
2150
+ const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
2151
+ ...gesture.start,
2152
+ left: gesture.start.left + dx,
2153
+ top: gesture.start.top + dy
2154
+ }, gesture.bounds, node.minimized ? CHIP_SIZE : void 0);
2155
+ latestRef.current = next;
2156
+ panel.style.left = `${next.left}px`;
2157
+ panel.style.top = `${next.top}px`;
2158
+ if (!node.minimized) {
2159
+ panel.style.width = `${next.width}px`;
2160
+ panel.style.height = `${next.height}px`;
2161
+ }
2162
+ };
2163
+ const handlePointerUp = (event) => {
2164
+ const gesture = gestureRef.current;
2165
+ if (!gesture || event.pointerId !== gesture.pointerId) return;
2166
+ gestureRef.current = null;
2167
+ if (!movedRef.current) {
2168
+ if (node.minimized) restore();
2169
+ return;
2170
+ }
2171
+ panelRef.current?.releasePointerCapture(event.pointerId);
2172
+ dispatch({
2173
+ floatId: node.id,
2174
+ geometry: latestRef.current,
2175
+ type: "moveFloat"
2176
+ });
2177
+ };
2178
+ if (node.minimized) return /* @__PURE__ */ jsxs("button", {
2179
+ "aria-label": `Restore ${floatTitle(node)} panel`,
2180
+ "data-dashfoo": "float-chip",
2181
+ onKeyDown: (event) => {
2182
+ if (event.key === "Enter" || event.key === " ") {
2183
+ event.preventDefault();
2184
+ restore();
2185
+ }
2186
+ },
2187
+ onPointerDown: handlePointerDown,
2188
+ onPointerDownCapture: onFocus,
2189
+ onPointerMove: handlePointerMove,
2190
+ onPointerUp: handlePointerUp,
2191
+ ref: setPanel,
2192
+ style: {
2193
+ left: node.geometry.left,
2194
+ pointerEvents: "auto",
2195
+ position: "absolute",
2196
+ top: node.geometry.top,
2197
+ zIndex
2198
+ },
2199
+ title: floatTitle(node),
2200
+ type: "button",
2201
+ children: [/* @__PURE__ */ jsx(FloatIcon, {}), /* @__PURE__ */ jsx("span", {
2202
+ "data-dashfoo": "float-chip-label",
2203
+ children: floatTitle(node)
2204
+ })]
2205
+ });
2206
+ const floatModel = {
2207
+ global,
2208
+ layout: node.layout,
2209
+ version: 1
2210
+ };
2211
+ return /* @__PURE__ */ jsxs("div", {
2212
+ "data-dashfoo": "float",
2213
+ onPointerDownCapture: onFocus,
2214
+ onPointerMove: handlePointerMove,
2215
+ onPointerUp: handlePointerUp,
2216
+ ref: setPanel,
2217
+ style: {
2218
+ height: node.geometry.height,
2219
+ left: node.geometry.left,
2220
+ pointerEvents: "auto",
2221
+ position: "absolute",
2222
+ top: node.geometry.top,
2223
+ width: node.geometry.width,
2224
+ zIndex
2225
+ },
2226
+ children: [
2227
+ /* @__PURE__ */ jsxs("div", {
2228
+ "data-dashfoo": "float-titlebar",
2229
+ onPointerDown: handlePointerDown,
2230
+ style: editable ? titleBarStyle : {
2231
+ ...titleBarStyle,
2232
+ cursor: "default"
2233
+ },
2234
+ children: [
2235
+ /* @__PURE__ */ jsx("span", {
2236
+ "aria-hidden": "true",
2237
+ "data-dashfoo": "float-grip",
2238
+ children: /* @__PURE__ */ jsx(GripIcon, {})
2239
+ }),
2240
+ /* @__PURE__ */ jsx(FloatTitle, {
2241
+ dispatch,
2242
+ node,
2243
+ renamable: editable
2244
+ }),
2245
+ editable && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
2246
+ "aria-label": "Minimize panel",
2247
+ "data-dashfoo": "float-minimize",
2248
+ onClick: () => dispatch({
2249
+ floatId: node.id,
2250
+ minimized: true,
2251
+ type: "setFloatMinimized"
2252
+ }),
2253
+ onPointerDown: (event) => event.stopPropagation(),
2254
+ style: dockButtonStyle,
2255
+ type: "button",
2256
+ children: /* @__PURE__ */ jsx(MinimizeIcon, {})
2257
+ }), /* @__PURE__ */ jsx("button", {
2258
+ "aria-label": "Dock panel back into the main layout",
2259
+ "data-dashfoo": "float-dock",
2260
+ onClick: () => dispatch({
2261
+ floatId: node.id,
2262
+ type: "dockFloat"
2263
+ }),
2264
+ onPointerDown: (event) => event.stopPropagation(),
2265
+ style: dockButtonStyle,
2266
+ type: "button",
2267
+ children: /* @__PURE__ */ jsx(DockIcon, {})
2268
+ })] })
2269
+ ]
2270
+ }),
2271
+ /* @__PURE__ */ jsx("div", {
2272
+ "data-dashfoo": "float-body",
2273
+ style: bodyStyle,
2274
+ children: /* @__PURE__ */ jsx(LayoutRoot, {
2275
+ closableTabs,
2276
+ dispatch,
2277
+ draggableTabs,
2278
+ draggableTabsets: false,
2279
+ editable,
2280
+ floatable: false,
2281
+ keepMounted,
2282
+ maximizable: false,
2283
+ model: floatModel,
2284
+ renamableTabs,
2285
+ renderTab,
2286
+ renderTabLabel,
2287
+ renderTabsetToolbar,
2288
+ resizableSplits,
2289
+ snap: snap ?? void 0,
2290
+ children: /* @__PURE__ */ jsx(DragProvider, { children: /* @__PURE__ */ jsx(RowView, { node: node.layout }) })
2291
+ })
2292
+ }),
2293
+ editable && RESIZE_HANDLES.map((handle) => /* @__PURE__ */ jsx("div", {
2294
+ "data-dashfoo": "float-resize",
2295
+ "data-edge": handle.key,
2296
+ onPointerDown: handlePointerDown,
2297
+ style: {
2298
+ position: "absolute",
2299
+ touchAction: "none",
2300
+ zIndex: 1,
2301
+ ...handle.style
2302
+ }
2303
+ }, handle.key))
2304
+ ]
2305
+ });
2306
+ };
2307
+
2308
+ //#endregion
2309
+ //#region src/components/float-layer.tsx
2310
+ const overlayStyle = {
2311
+ inset: 0,
2312
+ pointerEvents: "none",
2313
+ position: "fixed"
2314
+ };
2315
+ const FloatOverlay = ({ floats, global }) => {
2316
+ const [topId, setTopId] = useState(null);
2317
+ if (floats.length === 0) return null;
2318
+ return /* @__PURE__ */ jsx("div", {
2319
+ "data-dashfoo": "float-overlay",
2320
+ style: overlayStyle,
2321
+ children: floats.map((node) => /* @__PURE__ */ jsx(FloatPanel, {
2322
+ global,
2323
+ node,
2324
+ onFocus: () => setTopId(node.id),
2325
+ zIndex: node.id === topId ? 2 : 1
2326
+ }, node.id))
2327
+ });
2328
+ };
2329
+ const FloatLayer = ({ children, floats, global }) => /* @__PURE__ */ jsxs(FloatLayerContext.Provider, {
2330
+ value: true,
2331
+ children: [children, /* @__PURE__ */ jsx(FloatOverlay, {
2332
+ floats,
2333
+ global
2334
+ })]
2335
+ });
2336
+
2337
+ //#endregion
2338
+ //#region src/components/layout.tsx
2339
+ const Layout = {
2340
+ DragLayer: DragProvider,
2341
+ FloatLayer,
2342
+ Root: LayoutRoot,
2343
+ Rows: RowView,
2344
+ Tabset: TabsetView
2345
+ };
2346
+
2347
+ //#endregion
2348
+ //#region src/components/dashfoo-layout.tsx
2349
+ const DEFAULT_PERSIST_DEBOUNCE_MS = 300;
2350
+ const resolvePersist = (persist, hasDefaultModel) => {
2351
+ if (persist === void 0 || !hasDefaultModel) return null;
2352
+ if (typeof persist === "string") return {
2353
+ debounceMs: DEFAULT_PERSIST_DEBOUNCE_MS,
2354
+ key: persist,
2355
+ storage: localStorageAdapter
2356
+ };
2357
+ return {
2358
+ debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,
2359
+ key: persist.key,
2360
+ storage: persist.storage ?? localStorageAdapter
2361
+ };
2362
+ };
2363
+ const warnedComponents = /* @__PURE__ */ new Set();
2364
+ const DashfooLayout = forwardRef((props, ref) => {
2365
+ 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;
2366
+ const persistence = usePersistence(useMemo(() => resolvePersist(persist, defaultModel !== void 0), [persist, defaultModel]), defaultModel);
2367
+ const handleModelChange = useCallback((next, action) => {
2368
+ persistence.save(next);
2369
+ onModelChange?.(next, action);
2370
+ }, [onModelChange, persistence]);
2371
+ const store = useDashfooStore({
2372
+ defaultModel: persistence.initialModel ?? defaultModel,
2373
+ model,
2374
+ onAction,
2375
+ onActiveTabsetChange,
2376
+ onMaximizedTabsetChange,
2377
+ onModelChange: handleModelChange
2378
+ });
2379
+ const hasSharedManager = useContext(SharedDragManagerContext) !== null;
2380
+ useImperativeHandle(ref, () => ({
2381
+ addTab: (tab, target) => store.dispatch({
2382
+ index: target.index,
2383
+ location: target.location ?? "center",
2384
+ tab,
2385
+ targetId: target.targetId,
2386
+ type: "addNode"
2387
+ }),
2388
+ canRedo: store.canRedo,
2389
+ canUndo: store.canUndo,
2390
+ closeTab: (tabId) => store.dispatch({
2391
+ tabId,
2392
+ type: "deleteTab"
2393
+ }),
2394
+ dispatch: store.dispatch,
2395
+ dockFloat: (floatId) => store.dispatch({
2396
+ floatId,
2397
+ type: "dockFloat"
2398
+ }),
2399
+ floatTab: (tabId) => store.dispatch({
2400
+ tabId,
2401
+ type: "floatTab"
2402
+ }),
2403
+ getModel: () => store.model,
2404
+ maximizeTabset: (tabsetId) => store.dispatch({
2405
+ tabsetId,
2406
+ type: "setMaximizedTabset"
2407
+ }),
2408
+ redo: store.redo,
2409
+ renameTab: (tabId, name) => store.dispatch({
2410
+ name,
2411
+ tabId,
2412
+ type: "renameTab"
2413
+ }),
2414
+ resetLayout: () => {
2415
+ persistence.clear();
2416
+ if (defaultModel !== void 0) store.setModel(defaultModel);
2417
+ },
2418
+ selectTab: (tabsetId, index) => store.dispatch({
2419
+ index,
2420
+ tabsetId,
2421
+ type: "selectTab"
2422
+ }),
2423
+ undo: store.undo
2424
+ }), [
2425
+ defaultModel,
2426
+ persistence,
2427
+ store
2428
+ ]);
2429
+ const renderTab = useCallback((tab) => {
2430
+ if (factory) return factory(tab);
2431
+ const Component = components?.[tab.component];
2432
+ if (!Component) {
2433
+ if (components && !warnedComponents.has(tab.component)) {
2434
+ warnedComponents.add(tab.component);
2435
+ console.warn(`[dashfoo] no component registered for "${tab.component}"`);
2436
+ }
2437
+ return null;
2438
+ }
2439
+ return /* @__PURE__ */ jsx(Component, { node: tab });
2440
+ }, [components, factory]);
2441
+ const [containerRef, width] = useContainerWidth();
2442
+ const isCompact = responsive !== void 0 && width <= responsive.maxWidth;
2443
+ const view = useMemo(() => isCompact ? stackModel(store.model, responsive?.orientation) : store.model, [
2444
+ isCompact,
2445
+ responsive?.orientation,
2446
+ store.model
2447
+ ]);
2448
+ const isCompactRef = useRef(isCompact);
2449
+ isCompactRef.current = isCompact;
2450
+ const dispatch = useCallback((action) => {
2451
+ if (isCompactRef.current && action.type === "adjustSplit") return;
2452
+ store.dispatch(action);
2453
+ }, [store]);
2454
+ const maximized = view.maximizedTabsetId ? findTabset(view, view.maximizedTabsetId) : void 0;
2455
+ const tree = /* @__PURE__ */ jsx(Layout.FloatLayer, {
2456
+ floats: store.model.floats ?? [],
2457
+ global: store.model.global,
2458
+ children: /* @__PURE__ */ jsx(Layout.DragLayer, { children: maximized ? /* @__PURE__ */ jsx(Layout.Tabset, { node: maximized }) : /* @__PURE__ */ jsx(Layout.Rows, { node: view.layout }) })
2459
+ });
2460
+ return /* @__PURE__ */ jsx(Layout.Root, {
2461
+ closableTabs,
2462
+ dispatch,
2463
+ draggableTabs: isCompact ? false : draggableTabs,
2464
+ draggableTabsets: isCompact ? false : draggableTabsets,
2465
+ editable,
2466
+ floatable: isCompact ? false : floatable,
2467
+ keepMounted,
2468
+ maximizable,
2469
+ model: view,
2470
+ renamableTabs,
2471
+ renderTab,
2472
+ renderTabLabel,
2473
+ renderTabsetToolbar,
2474
+ resizableSplits: isCompact ? false : resizableSplits,
2475
+ rootRef: containerRef,
2476
+ snap,
2477
+ children: hasSharedManager ? tree : /* @__PURE__ */ jsx(DashfooDragProvider, { children: tree })
2478
+ });
2479
+ });
2480
+ DashfooLayout.displayName = "DashfooLayout";
2481
+
2482
+ //#endregion
2483
+ //#region src/components/panel.tsx
2484
+ const PanelRoot = (props) => /* @__PURE__ */ jsx("div", {
2485
+ ...props,
2486
+ "data-dashfoo": "panel"
2487
+ });
2488
+ const PanelHeader = (props) => /* @__PURE__ */ jsx("div", {
2489
+ ...props,
2490
+ "data-dashfoo": "panel-header"
2491
+ });
2492
+ const PanelBody = (props) => /* @__PURE__ */ jsx("div", {
2493
+ ...props,
2494
+ "data-dashfoo": "panel-body"
2495
+ });
2496
+ const PanelTitle = (props) => /* @__PURE__ */ jsx("span", {
2497
+ ...props,
2498
+ "data-dashfoo": "panel-title"
2499
+ });
2500
+ const PanelIcon = (props) => /* @__PURE__ */ jsx("span", {
2501
+ ...props,
2502
+ "data-dashfoo": "panel-icon"
2503
+ });
2504
+ const PanelBadge = (props) => /* @__PURE__ */ jsx("span", {
2505
+ ...props,
2506
+ "data-dashfoo": "panel-badge"
2507
+ });
2508
+ const Panel = {
2509
+ Badge: PanelBadge,
2510
+ Body: PanelBody,
2511
+ Header: PanelHeader,
2512
+ Icon: PanelIcon,
2513
+ Root: PanelRoot,
2514
+ Title: PanelTitle
2515
+ };
2516
+
2517
+ //#endregion
2518
+ //#region src/components/tabset/tabset.ts
2519
+ const Tabset = {
2520
+ CloseButton: TabsetCloseButton,
2521
+ Content: TabsetContent,
2522
+ FloatButton: TabsetFloatButton,
2523
+ Grip: TabsetGrip,
2524
+ MaximizeButton: TabsetMaximizeButton,
2525
+ OverflowMenu: TabsetOverflowMenu,
2526
+ RenameInput: TabsetRenameInput,
2527
+ Root: TabsetRoot,
2528
+ Tab: TabsetTab,
2529
+ Tablist: TabsetTablist,
2530
+ TabStrip: TabsetTabStrip,
2531
+ Toolbar: TabsetToolbar,
2532
+ Trigger: TabsetTrigger
2533
+ };
2534
+
2535
+ //#endregion
2536
+ export { DashfooDragProvider, DashfooLayout, Layout, Panel, Tabset, activeBreakpoint, localStorageAdapter, matchBreakpoint, memoryStorageAdapter, useContainerWidth, useDashfooStore, useDragSubject, useDropIntent, useExternalTabSource, useLayout, usePersistence, useResponsiveModel, useTab, useTabset };
9
2537
  //# sourceMappingURL=index.js.map