C&C Docs
Log in

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>
  • href is the block URL and becomes the destination's canonical URL unless one is supplied explicitly.
  • data-navigation="collection" or peer replaces the current unpinned tab. Use collection when returning to a resource list.
  • data-navigation="detail" or related opens or reuses a companion group to the right. If there is not enough room for min-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 with cnc-block.
  • data-label supplies the tab caption; otherwise it is derived from the URL.
  • data-icon supplies the cnc-icon name shown on the tab.
  • data-context is 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)