Skip to content

UI layout conventions

The map workspace (components/map-workspace/, hosting MapEditor, MapVisualiser, and MapTable) follows one layout contract for where a piece of UI lives. This exists so future additions have an obvious home instead of each screen inventing its own placement — see COLLAB-5610, which established it while fixing Wolfgang’s feedback that the node info panel’s position, and how to open/close it, were unpredictable.

MapTable (COLLAB-5641) is the third of three modes switched via ModeSwitcher, alongside Edit and Visualise — not a new kind of surface in the contract below. In V1 it renders full-width with no left workspace panel and no entity dock: it is the collection (a grid of nodes), so there’s no list for a workspace panel to hold and no separate detail view for a dock to open. A future connections table belongs as a second surface within Table mode (e.g. its own tab), not a fourth mode.

Left = map-scoped. Right = selection-scoped. Floating = viewport-scoped. Modal = interrupting.

Surface Position Scope Example content Driven by
Workspace panel Left, in-flow: w-12 rail + w-80 panel, collapses to w-0 The whole map Collections and their field catalogues (Nodes, Connections), map-wide config (Visualisation, Filter, Weighting, Appearance, Settings), map-wide ops (Import/Export) The user, via tabs — sticky across selections
Entity dock Right, in-flow: w-80, collapses to w-10 The current selection (one entity, or several) Node info, Connection info, a multi-select summary, any future single-entity surface The selection — never tabbed
Canvas overlay absolute over the canvas: bottom-left / bottom-right cards The current viewport Legends Canvas state
Dialog fixed inset-0 + scrim The whole app; blocks Destructive confirmations, the import wizard An explicit user action
Toast fixed, non-blocking The whole app Async mutation errors Side effects

Toolbars (EditorToolbar/VizToolbar) are a fifth case, but they don’t live in this workspace layout at all — COLLAB-5622 portals them into MapWorkspace’s header instead of floating over the canvas, via ToolbarSlotContext (see components/map-workspace/toolbar-slot.tsx).

  1. A collection is never replaced by a detail view. The Nodes/Connections lists in the left workspace panel always render; a selected node or connection’s own detail view opens in the right-hand dock instead of swapping the list out. (Before COLLAB-5610, the editor’s sidebar did exactly that swap — the opposite of this rule.)
  2. Selecting something never moves the left panel. selectNode/ selectEdge in map-editor.tsx don’t touch inspectorTab; only the entity dock reacts to selection.
  3. A canvas overlay never holds entity data — only viewport-scoped chrome (currently just the two visualiser legends).
  4. The entity dock is the same shell in both canvas modes. Visualise mode fills it with a read-only SelectionDetail; Edit mode fills it with NodeInspector/EdgeInspector. Shared shell, mode-specific body — this is how “consistent with wherever the inspector sits in the edit view” (the original ask) is satisfied. Table mode has no selection concept and no dock — a row’s cells edit inline instead of opening a detail view.
  5. “Inspector” means the right-hand dock. The editor’s left panel (InspectorSidebar) is a workspace panel in this vocabulary, not an inspector — it’s kept a distinct name to avoid exactly the confusion that prompted this doc.
  6. Multi-selection is a dock state, not a second selection model (COLLAB-5611). The editor reads React Flow’s own node.selected/ edge.selected flags as the single source of truth — a click always replaces the selection, a Shift-drag box-select can grow it to several nodes/connections. Exactly one selected entity still opens NodeInspector/EdgeInspector; more than one opens SelectionSummary (a count plus bulk delete) instead. There’s no separate “multi-select mode” to leave — the dock just reacts to however many things are selected.
  7. A collection and its field definitions share one tab (COLLAB-5639). There is no separate Fields tab: node fields are a section pinned to the bottom of the Nodes tab, and connection fields are the Connections tab’s list — each row carrying both the definition and how many connections use it. The rule generalises: a catalogue belongs in the tab whose collection it describes, not a tab of its own, so a count and the thing it counts are never a tab apart. Two corollaries the implementation leans on:
    • One row, one actions menu. A connection field’s row menu spans both scopes — map-scoped (hide/remove its connections) and org-scoped (edit/delete the field) — and names the scope in each item, rather than splitting them across two surfaces.
    • A pinned section outranks a long list. The Nodes tab’s field section is a shrink-0 sibling of the lists’ scroll region, capped at 40% with its own inner scroll, so neither side can squeeze the other out of view.

components/map-workspace/layout/:

  • z-layers.ts — the named z-index scale (below). New/edited code should import LAYER rather than write a raw z-* class; existing correct call sites weren’t swept when this was introduced, so don’t assume every z-indexed element in the codebase already uses it.
  • side-panel.tsxSidePanel/PanelRail/PanelRailButton/ PanelRailToggle/PanelColumn/PanelHeader/PanelBody: the rail + content-panel shell. Used by VizSidebar and InspectorSidebar (both left workspace panels). Deliberately composable parts, not one component — the two consumers differ in real ways (rail width, whether the rail itself collapses, a badge) that don’t collapse into one prop bag cleanly.
  • canvas-overlay.tsxCanvasOverlay: the floating-card shell used by ColourLegend/DirectionalLegend.
  • entity-dock.tsx + use-entity-dock.ts + use-dismiss-on-escape.ts — the right-hand dock and its behaviour (below).
0 canvas (React Flow / force-graph)
10 canvas overlays (the two legends); the active rail tab's own stacking
40 editor add-node click-catcher
50 toasts, add-node drag ghost
90/91 context menu scrim / menu
100 dialogs
200 global loading indicator

The entity dock itself needs no z-index — see below.

EntityDock (entity-dock.tsx) takes title, collapsed, onExpand, onDismiss, emptyHint, railLabel, and children (the mode-specific body — SelectionDetail, or NodeInspector/EdgeInspector). It renders either:

  • Expanded (w-80): a header (title + close) over the children, or emptyHint when nothing is selected.
  • Collapsed (w-10): a full-height button showing railLabel as vertical text ([writing-mode:vertical-rl]) plus a chevron, so the rail names what it opens rather than being a bare icon.

Empty state. The dock opens expanded, showing emptyHint — the same role the visualiser’s old persistent bottom bar played (COLLAB-5605: “click a node” was signposted nowhere, so the panel’s own existence, visible before the first click, is the affordance). It only collapses to the rail after an explicit dismissal — the header’s X, Escape, or a canvas background click — never on its own. Selecting anything re-expands it. useEntityDock owns this as { collapsed, expand, dismiss }.

Push, not float. The dock is in-flow (SidePanel-style), so it narrows the canvas rather than overlaying it — it never covers the very node/ connection it’s describing. This is a deliberate simplicity-over-polish call: with both the left workspace panel and the right dock open, the chrome budget is 48 + 320 + 320 = 688px, which gets tight well before common laptop widths. Making the dock float below some breakpoint instead is a known, not-yet-built follow-up — see Constraints & edge cases.

Dismissal and Escape. useDismissOnEscape(paneRef, onDismiss) attaches a keydown listener with three guards, all necessary because MapEditor and MapVisualiser both stay permanently mounted (MapWorkspace only toggles hidden between them, per its own module comment) — so two of these listeners are live at any time:

  1. Skip if this pane is the hidden one (offsetParent === null).
  2. Skip if a dialog owns Escape ([aria-modal="true"] is present — ui/dialog.tsx/ui/confirm-dialog.tsx).
  3. Skip if focus is in a form control (input, textarea, select, [contenteditable="true"]) — protects the editor’s own inputs and VizToolbar’s Escape-to-cancel-rename from being hijacked.
  • A new single-entity surface (something that only makes sense for one selected node/connection/whatever) → the entity dock, not a new panel.
  • A new collection or map-wide config → a tab in the left workspace panel.
  • A new viewport-scoped hint or legend → CanvasOverlay.
  • Anything that interrupts the user or needs explicit confirmation → a Dialog/ConfirmDialog, not a dock or panel.