dashfoo

API reference

Every public export of @dashfoo/react, @dashfoo/core, and @dashfoo/theme, with types and defaults.

This page lists the full public surface of the three packages. The guides cover the concepts; this is the lookup table. All three packages are ESM-only ("type": "module" with an exports map), so your tsconfig needs moduleResolution: "bundler", "node16", or "nodenext". Classic "node" resolution cannot find them. @dashfoo/react ships a "use client" banner, so it works as a client component in the Next.js App Router without a wrapper.

@dashfoo/react

The React binding: DashfooLayout, the Panel compound parts, the hooks, and the storage adapters. Source: packages/react/src.

DashfooLayout props

The component props. The props are a union: pass either model (controlled) or defaultModel (uncontrolled), never both and never neither. persist only exists on the defaultModel side. All three rules are enforced by the type, so the combinations that used to type-check and silently do nothing no longer compile. DashfooLayoutCommonProps is exported for helpers that spread overrides.

PropTypeDefaultDescription
modelDashfoononeControlled document, and the only source of truth: every dispatch is applied to this prop, so a change you do not apply is simply not applied. Normalized before render.
defaultModelDashfoononeUncontrolled initial document. The component owns it from there.
onModelChange(model: Dashfoo, action?: Action) => voidnoneFires after every change with the next model and the action that caused it (action is omitted for undo/redo). Not required for persistence: persist saves on its own.
componentsRecord<string, TabComponent>noneRegistry mapping tab.component keys to components. Each receives { node: TabNode }. An unregistered key renders nothing and logs one dev warning.
factory(tab: TabNode) => ReactNodenoneRender override. When set it resolves every tab and components is ignored.
persiststring | { key: string; storage?: StorageAdapter; debounceMs?: number }noneAuto-save and restore the model. Only accepted alongside defaultModel. A bare string is a localStorage key; the debounce defaults to 300 ms. See persistence.
onAction(action: Action) => Action | nullnoneIntercept each action before it commits: return it, a replacement, or null to veto.
onActiveTabsetChange(tabsetId: string | undefined) => voidnoneFires when the active tabset changes.
onMaximizedTabsetChange(tabsetId: string | undefined) => voidnoneFires when a tabset is maximized or restored.
renderTabLabel(tab: TabNode) => ReactNodenoneOverride the tab label. The accessible name stays tab.name.
renderTabsetToolbar(tabset: TabsetNode) => ReactNodenoneInject custom controls into a tabset's toolbar.
editablebooleantrueThe umbrella switch. false renders a static layout: no tab/tabset drag, close, rename, splitter resize, or external drops.
closableTabsbooleantrueShow the per-tab close control.
renamableTabsbooleantrueAllow double-click inline rename.
maximizablebooleantrueShow the tabset maximize/restore control.
floatablebooleanfalseShow a per-tabset control that floats the panel into a movable, resizable overlay, and render any floating panels (Dashfoo.floats). Opt-in. See floating panels.
draggableTabsbooleantrueAllow individual tabs to be dragged between tabsets.
draggableTabsetsbooleantrueShow the grip that drags a whole tabset.
resizableSplitsbooleantrueAllow dragging the splitters between panels. false disables them but keeps them mounted, so the gutters hold their size.
keepMountedbooleanfalseKeep inactive tab panels mounted (hidden) so their state survives a tab switch. By default inactive panels unmount.
responsive{ maxWidth: number; orientation?: Orientation }noneAt or below maxWidth (the layout's own container width, not the viewport) render a stacked column and lock drag + resize. A view-only projection: the model is untouched, so widening up is lossless and the layout never remounts. See responsive.
snap{ step?: number; divisions?: number | "panels"; threshold?: number }noneMagnetic snapping for splitter drags. The dragged boundary pulls onto a grid: the union of step (multiples of a fixed percent) and divisions (even splits: multiples of 100/d, where d is the number, or "panels" to divide by the row's panel count so a 3-panel row snaps to thirds). Within threshold percent (default 4) it snaps and commits on release as one undo step. Omitted โ‡’ off; replaces (not merges with) global.snap; a row can override via its own snap. Locked off in compact mode.

The model's global block layers under the boolean props: a feature is on only if neither the prop nor the global (tabEnableClose, tabEnableRename, tabSetEnableMaximize, tabEnableDrag, enableSplitResize) turns it off, and (for everything except maximize) only while editable is on. The root element is <div data-dashfoo="layout"> at 100% of its parent in both axes. A parent without a real height paints nothing (see getting started).

Static layouts

editable={false} freezes the arrangement in one move: tabs and tabsets cannot be dragged, tabs cannot be closed or renamed, splitters stop resizing (they stay mounted, so nothing reflows), and the layout rejects drops from useExternalTabSource, even under a shared DashfooDragProvider while a sibling layout stays editable. The dashboard remains interactive as a viewer: tab selection, keyboard tab navigation, the overflow menu, and maximize/restore remain available, and the imperative DashfooHandle / dispatch API still mutates the model programmatically. Flip the prop at runtime to build an "edit mode" toggle without a remount.

DashfooHandle

A ref on DashfooLayout exposes 14 imperative methods.

MethodSignatureEffect
addTab(tab: TabNode, target: { targetId: string; location?: DockLocation; index?: number }) => voidDispatches addNode. location defaults to "center"; index picks the slot when stacking into the target.
closeTab(tabId: string) => voidDispatches deleteTab.
selectTab(tabsetId: string, index: number) => voidDispatches selectTab.
renameTab(tabId: string, name: string) => voidDispatches renameTab.
maximizeTabset(tabsetId: string | null) => voidDispatches setMaximizedTabset; null restores.
floatTab(tabId: string) => voidFloats a tab into a movable, resizable panel.
dockFloat(floatId: string) => voidDocks a floating panel back into the main layout.
dispatch(action: Action) => voidRaw escape hatch for any of the 16 actions.
getModel() => DashfooThe current model.
undo() => voidStep back one action. No-op in controlled mode.
redo() => voidStep forward. No-op in controlled mode.
canUndo() => booleanReads the live history snapshot.
canRedo() => booleanReads the live history snapshot.
resetLayout() => voidRestore the original defaultModel, clearing undo history and any persisted copy. Unavailable in controlled mode (warns once).
import type { TabNode } from "@dashfoo/core";
import { model, row, tab, tabset } from "@dashfoo/core";
import type { DashfooHandle } from "@dashfoo/react";
import { DashfooLayout } from "@dashfoo/react";
import type { ReactNode } from "react";
import { useRef } from "react";

const Notes = ({ node }: { node: TabNode }): ReactNode => <div>{node.name}</div>;

const initial = model(row([tabset([tab("notes", "Notes")], { id: "main" })]));

const App = (): ReactNode => {
  const ref = useRef<DashfooHandle>(null);
  const handleAdd = (): void => {
    // ids default to the component name; pass an explicit id for a duplicate.
    ref.current?.addTab(tab("notes", "Scratch", { id: "notes-2" }), {
      location: "split-right",
      targetId: "main",
    });
  };
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <button onClick={handleAdd} type="button">
        Add tab
      </button>
      <div style={{ flex: 1, minHeight: 0 }}>
        <DashfooLayout components={{ notes: Notes }} defaultModel={initial} ref={ref} />
      </div>
    </div>
  );
};

export { App };

See controlled & history for the controlled-mode pairing and undo/redo wiring.

Panel compound parts

Unstyled building blocks for panel content. Each forwards its native props and adds a data-dashfoo attribute for styling.

PartElementAttribute
Panel.Root<div>data-dashfoo="panel"
Panel.Header<div>data-dashfoo="panel-header"
Panel.Body<div>data-dashfoo="panel-body"
Panel.Title<span>data-dashfoo="panel-title"
Panel.Icon<span>data-dashfoo="panel-icon"
Panel.Badge<span>data-dashfoo="panel-badge"

Prop types are exported as PanelRootProps, PanelHeaderProps, PanelBodyProps, PanelTitleProps, PanelIconProps, and PanelBadgeProps.

Layout and Tabset compound parts

The primitives DashfooLayout itself is built from, exported for hand-built layouts. Layout.Root (the frame + layout store), Layout.DragLayer (drag-dock, optional), Layout.Rows (the split tree, with a renderTabset render prop), Layout.Tabset (the stock tabset composition), and, for floating panels, Layout.FloatLayer (wrap the tree's content, passing floats + global, to render each floated panel as a draggable overlay). Plus the full tabset decomposition under Tabset.*: Root, TabStrip, Tablist, Tab, Trigger, RenameInput, CloseButton, Content, Toolbar, OverflowMenu, Grip, MaximizeButton, and FloatButton. Each part spreads native props and is unstyled beyond minimal structural flex. See Build your own layout for the part table and a complete example.

useDashfooStore

The store DashfooLayout uses internally, exposed for hosts that render their own chrome. Throws when given neither model nor defaultModel; every entry point is normalized.

UseDashfooStoreOptions:

OptionTypeDescription
defaultModelDashfooUncontrolled seed; the actor owns it with full undo/redo.
modelDashfooControlled document; every change routes through onModelChange.
onAction(action: Action) => Action | nullVeto or replace an action before it commits.
onActiveTabsetChange(tabsetId: string | undefined) => voidActive-tabset notifications.
onMaximizedTabsetChange(tabsetId: string | undefined) => voidMaximize/restore notifications.
onModelChange(model: Dashfoo, action?: Action) => voidFires once per real change (no-op transitions emit nothing).

Returns a DashfooStore:

FieldTypeDescription
modelDashfooThe current document.
dispatch(action: Action) => voidCommit one action.
undo() => voidUncontrolled only.
redo() => voidUncontrolled only.
canUndo() => booleanReads the live actor snapshot, not a stale render value.
canRedo() => booleanReads the live actor snapshot.
setModel(model: Dashfoo) => voidReplace the whole document and reset undo history.

useLayout

Selector hook over the layout store: the layout's resolved configuration and dispatch, for custom chrome (a renderTabsetToolbar button, a custom tab label). useLayout((state) => state.dispatch) re-renders only when the selected slice changes. Throws outside <DashfooLayout> / <Layout.Root>. The LayoutState fields:

FieldTypeDescription
dispatch(action: Action) => voidThe store's dispatch.
closableTabs, renamableTabs, maximizable, draggableTabs, draggableTabsets, resizableSplitsbooleanEffective values after editable and the model globals are layered in (maximizable ignores editable).
editablebooleanThe umbrella prop as passed; gates every structural capability above except maximizable.
keepMountedbooleanThe resolved prop.
maximizedTabsetIdstring | undefinedThe currently maximized tabset, if any. Must live in the main layout: normalize clears it once that tabset moves into a float.
renderTab(tab: TabNode) => ReactNodeThe resolved content renderer (factory or registry).
renderTabLabel, renderTabsetToolbaroptional render propsAs passed to DashfooLayout / Layout.Root.
splitDockbooleanFrom global.enableSplitDock.
tabLocation"top" | "bottom"From global.tabLocation, default "top".
tabsetMinSizenumberFrom global.tabSetMinSize, default 320 here.
tabStripEnabledbooleanFrom global.tabSetEnableTabStrip.

useTabset / useTab / useDragSubject / useDropIntent

useTabset((state) => โ€ฆ) selects from the per-tabset store inside <Tabset.Root>: node, visualSelected, activeTab, isMaximized, editingTabId, overflowItems, the resolved flags (tabsClosable, tabsRenamable, showMaximize), and the actions (selectTab, closeTab, renameTab, startEditing, toggleMaximize, selectOverflowTab).

useTab() returns { tab, index } (which tab the part belongs to) inside <Tabset.Tab>.

useDragSubject() returns the live drag subject ({ kind: "tab" | "tabset" | "external", id }) or null, including outside a drag layer. See Build your own layout.

useDropIntent() returns the live drop intent ({ targetId, location, index? }, where the drag would land if dropped right now) or null when nothing is dragging or the pointer is over no valid target (empty space, a non-editable layout, or a suppressed no-op drop). Both hooks work anywhere under a layout; under a DashfooDragProvider they work anywhere under the provider. Pair with dockZonePolygons from @dashfoo/core for custom drop indicators and drop-zone visualizations.

usePersistence

The load/save half behind the persist prop, usable standalone. usePersistence(config: PersistConfig | null, defaultModel: Dashfoo | undefined)

It loads the saved model once (validated; falls back to defaultModel on miss or corruption, pruning corrupt values with a warning), debounce-saves every change, and flushes a pending save on page hide (pagehide / visibilitychange โ†’ hidden) and on unmount. A null config makes it a no-op. initialModel starts as the seed, then updates after mount when storage is restored. Hosts that create a store once must apply that update with setModel; persist on DashfooLayout already does this without a history entry or a save.

TypeShape
PersistConfig{ key: string; storage: StorageAdapter; debounceMs: number }
Persistence{ initialModel: Dashfoo | undefined; save: (model: Dashfoo) => void; clear: () => void }

Storage adapters

ExportTypeDescription
StorageAdapter{ getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; removeItem: (key: string) => void }The minimal localStorage-shaped contract.
localStorageAdapterStorageAdapterSSR-safe browser adapter: reads return null and writes warn instead of throwing when storage is unavailable.
memoryStorageAdapter()() => StorageAdapterFactory returning a fresh Map-backed adapter, for tests, SSR, and ephemeral layouts.

External drag sources

Drag new tabs in from outside the layout. See drag & dock for the full flow.

ExportSignatureDescription
DashfooDragProvider({ children }) => ReactNodeShares one drag manager and one drag lifecycle between a layout, its floats and outside sources. A standalone layout needs no provider: it mounts one internally.
useExternalTabSource(options: ExternalTabSourceOptions) => { ref }Registers an element as a drag source; put ref on it.
ExternalTabSourceOptions{ createTab: () => TabNode; label?: string; disabled?: boolean }createTab runs at drag start and must mint a fresh tab id per call.

useContainerWidth

() => [ref: (el: HTMLElement | null) => void, width: number] tracks one element's content-box width via ResizeObserver. SSR-safe: seeds POSITIVE_INFINITY so the first render assumes the widest layout. The building block for hand-built responsive layouts (pair with stackModel).

useResponsiveModel

Picks a model per breakpoint, keyed off the container's own width (ResizeObserver) and/or media queries, and derives the structural-interaction flags from the active breakpoint's compact. Feed the result onto DashfooLayout/Layout.Root as reactive props; no key or remount is needed. SSR-safe. See responsive.

ExportSignatureDescription
useResponsiveModel(options: { breakpoints: Array<Breakpoint> }) => ResponsiveModelPut containerRef on the wrapper; pass model + the lock flags onto DashfooLayout (use model, not defaultModel).
matchBreakpoint(breakpoint: Breakpoint, width: number) => booleanNo query matches always; maxWidth matches at or below; media consults matchMedia.
activeBreakpoint(breakpoints: Array<Breakpoint>, width: number) => BreakpointFirst match wins; the last entry is the fallback. Throws on an empty array.
TypeShape
Breakpoint{ id: string; model: Dashfoo; query?: { maxWidth: number } | { media: string }; compact?: boolean }
ResponsiveModel{ breakpoint: string; containerRef: (el: HTMLElement | null) => void; model: Dashfoo; isCompact: boolean; draggableTabs: boolean; draggableTabsets: boolean; resizableSplits: boolean }

@dashfoo/core

The framework-free engine: schema, builders, actions, reducer, tree queries, geometry, history, serialization, and the XState machines. Source: packages/core/src.

Actions

Every document mutation is one of 18 immutable, discriminated actions. The reducer is exhaustive over the union; validate untrusted payloads with actionSchema before dispatch.

action.typePayloadEffect
addNode{ tab, targetId, location, index? }Insert a new tab at a dock location.
moveNode{ sourceId, targetId, location, index? }Move an existing tab to a dock target.
moveTabset{ sourceId, targetId, location }Move a whole tabset to a dock target (the grip drag). No index: a tabset merges or docks beside, so there is no strip slot to land at.
selectTab{ tabsetId, index }Set a tabset's selected index.
setActiveTabset{ tabsetId }Mark the focused tabset.
setMaximizedTabset{ tabsetId: string | null }Maximize one tabset, or restore with null.
renameTab{ tabId, name }Change a tab's name.
deleteTab{ tabId }Remove a tab.
deleteTabset{ tabsetId }Remove a whole tabset.
adjustSplit{ rowId, weights }Set the weights of a row's children (the splitter drag).
floatTab{ tabId, geometry?, floatId? }Float a tab out into a new floating panel.
floatTabset{ tabsetId, geometry?, floatId? }Float a whole tabset (with its tabs) into a panel.
dockFloat{ floatId, targetId?, location? }Dock a float back as its own panel (or center-merge).
moveFloat{ floatId, geometry }Update a floating panel's stored rect (drag/resize).
setFloatMinimized{ floatId, minimized }Collapse a float to a chip, or restore it.
renameFloat{ floatId, name }Rename a float's title (deduped to stay unique).
updateNodeAttributes{ nodeId, attrs }Patch mutable attributes on a tab, tabset, or row.
updateGlobalAttributes{ attrs }Patch the model's global block.

Related exports: actionSchema, dockLocationSchema, mutableNodeAttrsSchema, and the types Action, MutableNodeAttrs, DockLocation ("center" | "split-top" | "split-bottom" | "split-left" | "split-right"), and DropIntent ({ targetId: string; location: DockLocation; index?: number }). See drag & dock for how drops become actions.

Builders

Constructors that fill the mechanical fields (type, version, selected, weight, floats) so seed models stay short.

BuilderSignatureNotes
tab(component: string, name: string, options?: TabOptions) => TabNodeid defaults to component. Pass an explicit id when reusing a component.
tabset(children: Array<TabNode>, options?: TabsetOptions) => TabsetNodeselected defaults to 0 and is clamped into the strip; weight defaults to 1; id is auto-generated when omitted.
row(children: RowNode["children"], options?: RowOptions) => RowNodeorientation defaults to "row"; weight defaults to 1; id is auto-generated when omitted.
model(layout: RowNode, options?: ModelOptions) => DashfooFills version: 1, global: {} and floats: []; warns once on duplicate node ids.
floatNode(layout: RowNode, geometry: Geometry, options?: FloatOptions) => FloatNodeA floating panel wrapping a layout subtree at an in-app rect.

Option types: TabOptions (config, enableClose, enableDrag, enableRename, id), TabsetOptions (enableClose, enableMaximize, id, max, min, name, selected, weight), RowOptions (id, max, min, orientation, weight), ModelOptions (activeTabsetId, floats, global, maximizedTabsetId), FloatOptions (id, name). min/max accept a bare number (treated as px) or a Dimension.

Schema

Zod schemas and their inferred types. Field-by-field tables live in the model guide.

SchemaInferred type
dashfooSchemaDashfoo
rowNodeSchemaRowNode
tabsetNodeSchemaTabsetNode
tabNodeSchemaTabNode
floatNodeSchemaFloatNode
geometrySchemaGeometry
globalAttributesSchemaGlobalAttributes
dimensionSchemaDimension
edgeSchemaEdge
orientationSchemaOrientation
unitSchemaUnit
jsonValueSchemaJson

DashfooInput (z.input<typeof dashfooSchema>) is the loose counterpart of Dashfoo: the shape parseModel accepts, with weight and floats optional. Hold host-supplied or stored data as DashfooInput and parse it into a Dashfoo. tabsetNodeObjectSchema is tabsetNodeSchema before its clamping transform, for .pick()-ing mutable keys off.

Node (RowNode | TabsetNode | TabNode) is exported as a type only. FloatNode ({ type: "float"; id; layout: RowNode; geometry: Geometry; name?; minimized? }) holds a floating panel; see floating panels. The model's version is the pinned literal 1; payloads with any other version fail validation.

Reducer and normalize

ExportSignatureDescription
reducer(model: Dashfoo, action: Action) => DashfooThe pure engine: deep-copies via structuredClone (input never mutated), applies one action, runs normalize. Returns the input by reference when the action changed nothing, so identity equality means "no edit"; a rejected action skips normalize.
normalize(model: Dashfoo) => DashfooThe structural canonical pass: removes empty tabsets, collapses single-child rows, heals each float's layout and drops an emptied float, keeps activeTabsetId on a live tabset anywhere, and maximizedTabsetId on a live tabset in the main layout (floats opt out of maximize, so floating the maximized tabset clears it). selected and weight are settled at the parse boundary instead.

Tree helpers

Read-only queries over a model. All of them take the whole model and span every root, so a floated node is as reachable as a docked one. Matching is predicate-first: an id that also names a node of another kind cannot make two of these disagree about which node wins.

FunctionSignatureDescription
collectRoots(model: Dashfoo) => Array<RowNode>The main layout plus each floating panel's layout.
collectTabsets(model: Dashfoo) => Array<TabsetNode>Every tabset across all roots, depth-first.
getFirstTabset(model: Dashfoo) => TabsetNode | undefinedThe first tabset in document order.
findTabset(model: Dashfoo, tabsetId: string) => TabsetNode | undefinedA tabset by id.
findTab(model: Dashfoo, tabId: string) => TabLocation | undefinedA tab anywhere in the model: { tab, container, index }.
findFloat(model: Dashfoo, floatId: string) => FloatNode | undefinedA floating panel by id.
findRow(model: Dashfoo, id: string) => RowNode | undefinedA row by id.
findRootContaining(model: Dashfoo, nodeId: string) => RowNode | undefinedWhich root (main layout or a float) holds a node.
findAttributedNode(model: Dashfoo, id: string) => AttributedNode | undefinedAny row, tabset, or tab by id. Backs updateNodeAttributes.
findTabsetParent(model: Dashfoo, tabsetId: string) => { index: number; parent: RowNode } | undefinedThe row holding a tabset and its index within it.
findDuplicateIds(model: Dashfoo) => Array<string>Ids appearing more than once across the whole model.

Types: AttributedNode, TabContainer, TabLocation.

Ids

FunctionSignatureDescription
createNodeId(prefix?: string) => string`${prefix}-${uuid}`, prefix defaults to "node".
createTabId() => stringcreateNodeId("tab").

stackModel

stackModel(model: Dashfoo, orientation: Orientation = "column"): Dashfoo flattens any layout into a single row of all its tabsets in document order, each with equal weight, and clears maximize. It is the building block for a mobile breakpoint. See responsive.

Geometry

ExportSignatureDescription
resolveDockTarget(pointer: Point, rect: Rect, opts?: BandOptions) => DockLocationCenter of a tabset resolves to "center" (stack); within an outer edge band (default 22%) to "split-left" and friends.
dockZonePolygons(rect: Rect, opts?: BandOptions) => Array<DockZone>The full hit-region partition behind resolveDockTarget: the inner center rect plus four edge trapezoids, as polygons. Shares the band default, so a painted map always agrees with the live hit-test.
zoneRect(rect: Rect, location: DockLocation) => RectThe region the dock indicator highlights: the whole rect for center, the matching half for a split-*.

Types: Point ({ x: number; y: number }), Rect ({ x: number; y: number; width: number; height: number }), BandOptions ({ bandFraction?: number }).

History

Past/present/future snapshots over the reducer. Every dispatched action is one undo step; there is no coalescing. A splitter drag is still a single step because react-resizable-panels v4 commits one adjustSplit per release.

ExportSignatureDescription
createHistory(present: Dashfoo) => HistoryA fresh history with empty past and future.
dispatch(history: History, action: Action) => HistoryReduce present, push the old one to past, clear future.
undo(history: History) => HistoryStep back; returns the same history at the bottom.
redo(history: History) => HistoryStep forward; returns the same history at the top.
canUndo(history: History) => booleanpast is non-empty.
canRedo(history: History) => booleanfuture is non-empty.

History is { past: Array<Dashfoo>; present: Dashfoo; future: Array<Dashfoo> }.

Serialization

ExportSignatureDescription
toJSON(model: Dashfoo) => stringJSON.stringify, since the model is plain data.
fromJSON(json: string) => DashfooParse + parseModel. Throws on invalid input.
parseModel(value: unknown) => DashfooValidate against dashfooSchema (including the pinned version: 1), normalize, and warn on duplicate node ids. Takes DashfooInput-shaped data: weight and floats may be omitted and come back filled in.

Machines

Two XState machines, for hosts that bypass the React binding.

  • dashfooMachine: the document actor. Input { model: Dashfoo }, context { history: History }. Events: DISPATCH ({ action }), UNDO, REDO, and SET_MODEL ({ model }, which resets history). Its context and event unions are internal and not exported.
  • dragDockMachine: the drag/dock interaction lifecycle. Events: START ({ subject: DragSubject }), OVER ({ drop: DropResolution | null }), DROP, CANCEL. Its context is one DragState ({ kind: "idle" } | { kind: "dragging"; subject; drop }), so a drop target without a subject is unrepresentable. A valid drop emits { type: "COMMIT", action, scope }, where scope echoes back the DropResolution.scope that resolved the intent so the host can route the action: moveNode for a tab drag, moveTabset for a tabset-grip drag; addNode only for an external source. dropAction(drag) is the same decision as a plain function. Types: DragState, DragSubject, DropResolution.

@dashfoo/theme

The opt-in default skin over --dashfoo-* tokens, light by default, dark via data-dashfoo-theme="dark" on any ancestor. The full token table is in theming.

Entry pointContents
@dashfoo/theme/tailwind.cssTailwind v4 source entry: tokens + skin in cascade layers, plus an @theme bridge that generates dashfoo-* utilities.
@dashfoo/theme/dashfoo.cssThe prebuilt skin, compiled from the same source (tokens included, flattened). One import styles everything; no Tailwind needed.
@dashfoo/theme/tokens.cssThe tokens alone, for building your own skin on the same override surface.

The only JS export is DASHFOO_THEME_VERSION, a version constant for runtime checks.