cnc-dock
An application-neutral dock: recursive row and column splits, draggable tab groups, resizable gutters, URL-loaded panels, companion-page navigation, responsive collapsing, and a public API for applications that need to control the workspace.
Quick start
Bind a serializable config. Split nodes contain other layout nodes; tab groups
contain panel descriptors. A block panel loads its url through
cnc-block. The dock owns live layout updates, so rendering does not depend on a
page-level save handler.
<cnc-dock
:config="dockConfig"
collapse-width="760"
min-column-width="400"
@layout-changed="persist($event.detail.layout)"
>
<div slot="empty-state">Choose a page.</div>
</cnc-dock>
Layout node keys
Every node needs a stable id. A split's children are layout nodes; a
tab group's children are panel descriptors.
| Key | Purpose |
|---|---|
| type |
cols
and rows create splits; tabs creates a tab group.
|
| id | Stable identity used by events, navigation, drag/drop, and the public API. |
| sizes | Percentages assigned to split children and rewritten by gutter resizing. |
| children | Nested nodes for a split, or panel descriptors for a tab group. |
| activeChildId | ID of the visible panel in a tab group. |
| tabPlacement |
Passed directly to cnc-tabs placement: three alignments on each of the
top, right, bottom, and left edges.
Use an explicit -center suffix for centered tabs; a bare edge name selects
its leading alignment (for example, top means top-left).
|
| collapseOnEmpty | Remove the group and simplify its parent split when its final tab closes or moves. |
Panel descriptors
{
id: 'users',
type: 'block',
url: '/users',
canonicalUrl: '/users',
label: 'Users',
icon: 'lucide/users',
closable: true,
pinned: false
}
id
is the stable tab identity. canonicalUrl prevents duplicate tabs. Pinned tabs are
preserved when peer navigation would otherwise replace the current tab. Companion tabs may also
carry openedFromTabId, openedFromUrl, and
companionGroupId; the dock maintains those fields automatically.
Navigation
A block loaded into the dock should navigate with cnc-link. The link describes the
destination and its relationship to the current panel; the dock decides whether to replace a
tab, activate an existing URL, or open a companion group.
<cnc-link
href="/dock-samples/dock-users"
data-navigation="collection"
data-view-type="block"
data-label="Users"
>Open people directory</cnc-link>
-
hrefis the block URL and becomes the destination's canonical URL unless one is supplied explicitly. -
data-navigation="collection"orpeerreplaces the current unpinned tab. Usecollectionwhen returning to a resource list. -
data-navigation="detail"orrelatedopens or reuses a companion group to the right. If there is not enough room formin-column-width, it opens as another tab in the source group. data-navigation-scope="local"leaves navigation inside the loaded block.-
data-view-type="block"tells the dock to load the URL withcnc-block. data-labelsupplies the tab caption; otherwise it is derived from the URL.data-iconsupplies thecnc-iconname shown on the tab.-
data-contextis JSON application metadata copied into the new tab descriptor.
Applications may dispatch navigation-request directly when navigation does not
originate from a link. Prefer cnc-link inside ordinary loaded content because it
preserves normal link semantics and keeps the destination declaration beside the UI.
Messages between panels
Use dock._dock.msg(target, eventName, detail) for transient communication with an
already-mounted panel. The target can identify a tab by tabId, url,
path, or _key. The method returns true when it finds and
dispatches to a mounted destination.
// Sender: locate the containing dock and notify the Users panel.
const dock = this.$el.closest('cnc-dock')
dock?._dock.msg(
{ url: '/dock-samples/dock-users' },
'users-refresh',
{ reason: 'company-updated' }
)
// Receiver controller: browser event wiring belongs in mount().
mount() {
const receive = (event) => this.reloadUsers(event.detail)
this.$el.addEventListener('users-refresh', receive)
return () => this.$el.removeEventListener('users-refresh', receive)
}
The dock sends a non-bubbling CustomEvent to message targets inside the panel. A
loaded cnc-block receives it automatically. Custom panel markup can opt in with
data-dock-message-target; compatibility targets
data-workspace-message-target
and data-editor-id are also recognized. Messages are not queued for closed or
unmounted tabs and are not durable state.
Public API
The imperative facade is available at dock._dock. Prefer events and bound config
for ordinary authoring; use this facade when an application needs targeted workspace commands.
dock._dock.activateTabById(tabId, groupId)
dock._dock.closeTab(tabId, groupId)
dock._dock.togglePin(tabId, groupId)
dock._dock.addTabToGroup(tab, groupId, activate)
dock._dock.addTabInNewGroup(tab, groupId, direction, position)
dock._dock.moveTab(tabId, fromGroupId, toGroupId, activate)
dock._dock.navigate(request)
dock._dock.findTabByUrl(url)
dock._dock.getGroup(groupId)
dock._dock.getAllGroups()
dock._dock.getConfig()
dock._dock.setConfig(config)
dock._dock.msg(target, eventName, detail)