Skip to main content
xriptxr

Dock Zone

Info:Shell
31

A drag-and-drop dockable-panel workspace: tabbed zones that rearrange by dragging.

docking panels workspace ide layout draggable panels tear-off float

Live demo

live · @xtyle/astro

DockZone

A movable-panel workspace

The workspace opens already split into zones, with the Terminal panel torn off into a floating window. Drag a tab onto another zone to move it: drop over the center to join as a tab, or against an edge to split. Clicking a tab just selects it. Float the Preview panel out with its button, or drag any tab out past the zones. Drag the floating window by its titlebar and it just moves — the workspace is open floor. Push it up against a zone's boundary and the drop films light up: release there and it re-docks as a split. Its button docks it back as a tab, and its corner resizes it. The kebab on a panel opens its overflow menu. Every one of those surfaces — tab strip, section header, header buttons, kebab, float window, drag film — is drawn by the component's xript fragment, so an app reskins or restructures them the same way a mod would. The layout is the drag physics only; the panels are your own content.

  • src
    • components
      • Button.astro
      • DockZone.astro
      • Table.astro
    • derive.ts
    • register.ts
  • package.json
  • README.md
  • Overview
  • Anchors
  • Knobs
  • Derivation
  • Color ramp
  • Coverage
Themed output midnight-ocean

A full token set derived from three anchors, ready to paint the whole library.

  • Contrast 4.2:1 below AA on --warn-bg
  • Token --border consumed, not produced
  • 3 components consume --accent-3
$ xtyle derive --bg #101418 --accent #4fd6be
✓ 214 tokens · AA on every text pair
$ _

A stacked tool rail

The same element in mode="stack": every panel is open at once as a collapsible section, the shape a tool or inspector rail wants. Click a section header to collapse it; drag a header to re-dock it, exactly like a tab.

  • Background
  • Gradient
  • Midground
  • Foreground
  • Highlights
  • Opacity 100%
  • Blend normal
  • X 128 · Y 96
  • Move layer
  • Recolor accent
  • Add gradient

Dock Zone is a movable-panel workspace, the editor-style chrome an app builds its layout from. Its direct children are the panels: any element with a data-panel-id and a data-title (or title) for its tab.

The zone reads them, arranges them into a layout of tabbed zones, and renders the tab strips and splits around them. Dragging a tab re-docks its panel onto another zone, joining it as a tab when dropped over the center or splitting the zone when dropped against an edge. Dragging a tab out past every zone tears it into a floating window that moves, resizes, and docks back on the same layout. A float's titlebar moves it: dragging anywhere across the open workspace just repositions the window, and a re-dock is only offered — films and all — once the pointer comes within the --dock-band of a zone's boundary, the seam a split would land against. Every rearrangement dispatches a layout-change event carrying the serializable layout, and setting the layout property restores a saved one, so a workspace persists across reloads. A panel carries its own header chrome, declared on the panel child: data-closable for a built-in close (a cancelable panel-close, then removal and a fresh layout-change), data-actions for direct header buttons, and data-menu for a kebab overflow <xtyle-menu>. A header button or a menu row both fire panel-action, and a data-badge puts trailing status text (a count) on the panel's own tab. A leaf renders in one of two modes: tabs (the default, one active panel behind a tab strip) or stack (every panel a collapsible section, the tool-rail shape); set mode for the whole workspace or per leaf in the tree. The layout physics are xtyle's own headless engine (resolveDrop for the drop geometry, dockPanel for the tree), the same primitives a consumer can drive directly from @xtyle/core/elements. The Svelte binding surfaces layout as a prop and reports rearrangement through onLayoutChange, close through onPanelClose, and header controls through onPanelAction; the Astro binding renders the panels and upgrades the workspace on the client. All of the chrome the workspace invents — the tab strips, the section headers and their chevrons, the header buttons and the kebab, the float windows and their resize grips, the drag films — is drawn by the component.dock-zone fragment, so an app reskins or restructures the panel chrome the same way a third-party mod would, while the element keeps the dock math, the pointer gestures, panel custody, and the persisted layout.

When to use

How this component composes with the rest of the set.

Give the zone a height (it fills its container); inside a Splitter or an AppShell main region it takes the available space.
Persist the workspace by saving layout-change.detail.layout and restoring it into the layout property on load.
For a headless workspace (your own panel chrome), drive resolveDrop and dockPanel from @xtyle/core/elements directly instead.
Give a panel header controls without leaving the layout: data-closable for a built-in close, data-actions for direct buttons, and data-menu for a kebab <xtyle-menu>. The chrome always reflects the active panel.
panel-close is cancelable, so a consumer can gate removal (unsaved changes) with preventDefault(); otherwise the zone removes the panel and reports the new tree through layout-change.
To reshape the chrome itself (a tab that carries an icon, a different float titlebar, a kebab that isn't a ), fill the component.dock-zone slot: the built-in panel chrome is a fragment, so an override goes through the same surface a third-party mod would use. The dock math, the gestures, and the layout stay with the element, so a reshaped workspace still drags, splits, floats, and persists.

Props

8 props, straight from the manifest.

PropTypeDefaultBindingsDescription
layout html: layout, read once at mount DockLayout all panels in one zone
html svelte
The whole workspace (a DockLayout from @xtyle/core/elements: { tree, floating }, the docked tree plus any floating windows). Set the JS property to restore a persisted layout and read it back from layout-change.detail; for a declarative start, pass it as a JSON layout attribute. A bare DockNode tree is still accepted and read as a layout with no floats. A leaf's mode / collapsed and each float's rect travel in the one layout, so the whole workspace persists together.
mode html: mode, read once at mount "tabs" | "stack" "tabs"
html svelte astro
The starting render mode when no layout is authored: "tabs" shows one active panel behind a tab strip; "stack" shows every panel as a collapsible section stacked top-to-bottom (a tool/inspector rail). Per-leaf mode lives in the layout tree; this attribute only seeds the auto single zone.
data-badge html: data-badge on panel string (on a panel child)
html svelte astro
Trailing status text on the panel's own tab (and its stacked-section header), like an unread or problem count. Rides on every panel's tab, not just the active one; decorative (aria-hidden), so the tab's accessible name stays its title.
data-closable html: data-closable on panel boolean attribute (on a panel child)
html svelte astro
Renders a close button in the panel's header. Clicking it fires a cancelable panel-close (detail: { panelId }); unless a listener calls preventDefault(), the zone removes the panel and fires layout-change with the new tree.
data-actions html: data-actions on panel JSON { id, label, icon? }[] (on a panel child)
html svelte astro
Direct header buttons for a panel. icon is a short glyph shown on the button, label its accessible name. Clicking one fires panel-action (detail: { panelId, actionId }).
data-menu html: data-menu on panel JSON MenuItem[] (on a panel child)
html svelte astro
A kebab (⋮) overflow menu for the panel. The kebab is a real button in the panel's header; pressing it opens the workspace's shared cursor-anchored <xtyle-menu> loaded with this panel's rows (so it carries headings, hints, keyboard navigation, and intent: "danger"). Selecting a row fires panel-action with the row's value as the actionId.
onPanelClose (event: CustomEvent<{ panelId }>) => void
svelte
Svelte callback for the panel-close event. Call event.preventDefault() to veto the removal (e.g. an unsaved-changes guard).
onPanelAction (event: CustomEvent<{ panelId, actionId }>) => void
svelte
Svelte callback for the panel-action event fired by a header button or a menu row.

Events

What the component emits, and what rides along on event.detail. The name in the first column is the one addEventListener takes.

EventDetailBindingsDescription
layout-change svelte: onLayoutChange { layout }
html svelte astro
The dock's arrangement changed — a drag, a tear-off, a close.
panel-action svelte: onPanelAction { panel, action }
html svelte astro
A panel's own action button was activated.
panel-close svelte: onPanelClose { panel }
html svelte astro
A panel was dismissed.

Methods

What you can call on the element itself — the half of the surface a prop or an event cannot express. A control driven by a gesture, or one whose state the platform owns, opens here. Astro renders on the server and hands back no instance, so no method is reachable from it.

MethodReturnsBindingsDescription
closePanel(panelId: string) void
html svelte
Close a panel by id, whether it is docked or floating, through the same path as the built-in close button: a cancelable panel-close, then removal and layout-change. Call it from a data-menu close row or a shortcut; pulling a panel out of the tree by hand and re-setting layout will not stick, since the panel is still a DOM child the zone recovers.
floatPanel(panelId: string, rect?: { x: number; y: number; width: number; height: number }) void
html svelte
Tear a docked panel out into a floating window over the workspace, opening at rect or a cascaded default, and report the new arrangement through layout-change. The imperative half of the drag that does the same thing. The window drags by its titlebar (clamped to the workspace), resizes from a bottom-right grip, and carries dock and close buttons; its rect rides on the same layout as the docks, so the whole workspace persists together.
dockFloating(panelId: string, target?: string, region?: "center" | "start" | "end" | "top" | "bottom") void
html svelte
Dock a floating panel back into the tree, routing through the same drop path a tab move uses and defaulting to the centre region of the zone it floated out of. Pass a target zone id and region to land it elsewhere. The float window's dock button calls this, and it is the only way back to a tab, since a titlebar drag only ever offers a split.

Anatomy

The named parts that make up the component. A ::part() handle is reachable from your own stylesheet; the class beside it is the component's internal selector, which a shadow boundary keeps to itself.

root

Success:::part(root) .xtyle-dock-zone__root

The fragment's workspace root: the flex container the zones, the drag films, and the float layer all render inside. Everything below is drawn by the component.dock-zone fill, so a mod reshapes any of it.

--space-1

zone

Success:::part(zone) .xtyle-dock-zone__leaf

A leaf of the layout: a tab strip over the active panel's body.

--bg-1 --line --radius-md

tab

Success:::part(tab) .xtyle-dock-zone__tab

A draggable tab; the active one carries an accent underline. A data-badge renders trailing status text (.xtyle-dock-zone__badge, muted) after the title.

--fg-2 --text-sm --accent --space-1

actions

Success:::part(actions) .xtyle-dock-zone__actions

The active panel's header controls, pinned to the end of the tab strip: direct action buttons, the kebab, and the close button (its hover tints --danger).

--fg-0 --fg-2 --danger --state-hover

kebab

Success:::part(kebab) .xtyle-dock-zone__kebab

The overflow button a data-menu panel gets. A real button around a real glyph (.xtyle-dock-zone__kebab-glyph), so a mod can swap the glyph for an icon or restructure the control; it opens the one cursor-anchored <xtyle-menu> the workspace shares.

--fg-2 --state-hover --text-lg

section

Success:::part(section) .xtyle-dock-zone__section

A stack-mode panel: a disclosure header (a rotating chevron, .xtyle-dock-zone__chevron, and the title, the panel's controls beside it) over a body shown when expanded.

--bg-2 --fg-0 --fg-2 --line --state-hover

float

Success:::part(float) .xtyle-dock-zone__float

A torn-off panel's window: a titlebar (.xtyle-dock-zone__float-head) that drags the window around the workspace, with dock and close buttons, the panel's body, and a bottom-right resize grip (.xtyle-dock-zone__float-resize).

--bg-1 --bg-2 --elevation-4 --line --radius-md --weight-medium

highlight

internal .xtyle-dock-zone__film

The drag-preview films, shown only while a drop is actually on offer: the drop target (--accent), the remnant a split would leave (--accent-2), and every other zone (--accent-3).

--accent --accent-2 --accent-3 --border-normal

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 31/31 consumed tokens produced default register: 310 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --accent-2 --accent-3 --bg-1 --bg-2 --border-normal --border-thick --border-thin --danger --duration-fast --ease-standard --elevation-4 --fg-0 --fg-1 --fg-2 --font-sans --line --radius-md --radius-sm --ring --space-0 --space-1 --space-2 --space-3 --space-4 --space-6 --space-8 --state-hover --text-lg --text-sm --weight-medium

Slots

default
html

The panels: elements carrying data-panel-id and data-title. Their content shows in the active zone body. A panel can also carry its own header chrome: data-closable, data-actions, data-menu, and data-badge.

Accessibility

A zone's tab strip is a real role="tablist" of role="tab" buttons, each aria-controls-linked to the zone's panel body, which is a role="tabpanel" named by the active tab. A tab outside a tablist is not a tab to assistive tech, so the relationship is declared rather than implied.
The strip is one Tab stop: the active tab carries tabindex="0" and the rest -1, so Tab reaches the strip and moves past it rather than through every panel name.
Ids are scoped to the element instance, so several docks on one page never collide.
Dragging is pointer-driven; clicking a tab activates its panel without a drag, so rearrangement is not the only way to switch panels.
Header controls are real <button>s with an accessible name (data-actions uses its label, close reads Close <title>, the kebab reads <title> options); the kebab opens a full <xtyle-menu> with keyboard navigation, and its glyph is aria-hidden.
A floating window's titlebar controls are named buttons (Dock <title>, Close <title>), so a torn-off panel can be docked back or closed without a pointer drag.
A data-badge is decorative (aria-hidden), so a tab's accessible name stays its title; put anything a screen reader must announce in data-title.
In stack mode each section header is a real <button> with aria-expanded, so its collapsed state is announced and it toggles by keyboard; the chevron is decorative (aria-hidden).

Code

Panels and persistence

Three panels in one zone; drag a tab to split or re-tab, and the layout persists to localStorage.

<xtyle-dock-zone style="height: 320px;">
	<section data-panel-id="files" data-title="Files">The file tree.</section>
	<section data-panel-id="outline" data-title="Outline">The document outline.</section>
	<section data-panel-id="preview" data-title="Preview">The rendered preview.</section>
</xtyle-dock-zone>

<script type="module">
	import "@xtyle/core/elements";
	const zone = document.querySelector("xtyle-dock-zone");
	zone.addEventListener("layout-change", (e) => {
		localStorage.setItem("layout", JSON.stringify(e.detail.layout));
	});
	const saved = localStorage.getItem("layout");
	if (saved) zone.layout = JSON.parse(saved);
</script>

Panel controls

A panel with a close button, a direct action button, and a kebab overflow menu, all declared on the panel child, all reporting through panel-action / panel-close.

<xtyle-dock-zone style="height: 320px;">
	<section
		data-panel-id="editor"
		data-title="Editor"
		data-badge="3"
		data-closable
		data-actions='[{ "id": "split", "label": "Split editor", "icon": "◫" }]'
		data-menu='[{ "heading": "Panel" }, { "label": "Reveal in tree", "value": "reveal" }, { "separator": true }, { "label": "Close", "value": "close", "intent": "danger" }]'
	>The document.</section>
	<section data-panel-id="preview" data-title="Preview" data-closable>The rendered preview.</section>
</xtyle-dock-zone>

<script type="module">
	import "@xtyle/core/elements";
	const zone = document.querySelector("xtyle-dock-zone");
	// A header button or a menu row: both arrive as panel-action.
	zone.addEventListener("panel-action", (e) => {
		if (e.detail.actionId === "close") zone.closePanel(e.detail.panelId); // same path as the ✕ button
		else runCommand(e.detail.panelId, e.detail.actionId);
	});
	// The built-in close is cancelable; the zone removes the panel and re-reports the layout unless you veto it.
	zone.addEventListener("panel-close", (e) => {
		if (e.detail.panelId === "editor" && hasUnsavedChanges()) e.preventDefault();
	});
</script>

Stacked rail

mode="stack" renders every panel as a collapsible section instead of a tab strip, the shape a tool or inspector rail wants. Per-leaf mode also lives in the layout tree, so a stacked rail and a tabbed editor coexist in one workspace.

<!-- A tool rail: every panel open at once, each a collapsible section. -->
<xtyle-dock-zone mode="stack" style="height: 320px; max-width: 280px;">
	<section data-panel-id="layers" data-title="Layers">The layer list.</section>
	<section data-panel-id="props" data-title="Properties">The property inspector.</section>
	<section data-panel-id="history" data-title="History">The undo stack.</section>
</xtyle-dock-zone>

<!-- For a mixed workspace (a stacked rail beside a tabbed editor), author the leaf's mode in the tree: -->
<script type="module">
	import "@xtyle/core/elements";
	document.querySelector("xtyle-dock-zone").layout = {
		kind: "split", direction: "row", sizes: [1, 3],
		children: [
			{ kind: "leaf", id: "rail", panels: ["layers", "props", "history"], active: 0, mode: "stack", collapsed: ["history"] },
			{ kind: "leaf", id: "main", panels: ["editor", "preview"], active: 0 },
		],
	};
</script>