# Node Canvas

A whiteboard-style canvas for data flows. Nodes sit on a grid and you place any content in them through slots. Drag a node and its edges re-route as it moves. Click a node to select it: a blue outline with corner handles appears, and the `#selection-toolbar` slot floats above it. Drag a corner handle to resize the node. Press Delete or Backspace to delete the selected node. Drag the empty canvas to pan. Hold Ctrl or ⌘ and scroll (or pinch) to zoom around the pointer, or use the zoom control. The minimap shows every node and the visible area, and a click on it moves the view there.

The package also ships small node bodies that match the design: a glowing source pill, a sticky note, a titled card, a stacked bar chart and a checklist with Filter and Sort.

In the demo, pick the source, note or frame tool and click the canvas to place a node. The selection toolbar switches a card between Chart, Table and Number, opens it in a larger view, and duplicates, locks or deletes it.

<demo-node-canvas>



</demo-node-canvas>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/node-canvas.json"
```

## Usage

```vue
<script setup lang="ts">
  import { ref } from 'vue';
  import NodeCanvas from '~/components/ui/node-canvas/NodeCanvas.vue';
  import NodeCanvasCard from '~/components/ui/node-canvas/NodeCanvasCard.vue';
  import NodeCanvasChecklist from '~/components/ui/node-canvas/NodeCanvasChecklist.vue';
  import NodeCanvasSource from '~/components/ui/node-canvas/NodeCanvasSource.vue';
  import type {
    NodeCanvasEdge,
    NodeCanvasNode,
  } from '~/components/ui/node-canvas/types';

  const nodes: NodeCanvasNode[] = [
    {
      id: 'mes',
      label: 'MES (Postgres) source',
      x: 38,
      y: 60,
      width: 220,
      height: 50,
      radius: 16,
      minimapColor: '#3b82f6',
    },
    {
      id: 'downtime',
      label: 'Downtime by cell',
      x: 431,
      y: 137,
      width: 277,
      height: 287,
    },
  ];

  const edges: NodeCanvasEdge[] = [
    {
      id: 'e1',
      from: 'mes',
      to: 'downtime',
      toAnchor: { side: 'left', offset: 0.19 },
    },
  ];

  const zoom = ref(0.84);
  const selected = ref<string | null>('downtime');
</script>

<template>
  <NodeCanvas
    v-model:zoom="zoom"
    v-model:selected="selected"
    :nodes="nodes"
    :edges="edges"
    :breadcrumb="['Plant North', 'Line 3 Health']"
  >
    <template #actions>
      <button class="rounded-2xl bg-foreground px-4 py-2 text-background">
        Share
      </button>
    </template>
    <template #node-mes>
      <NodeCanvasSource label="MES (Postgres)" />
    </template>
    <template #node-downtime>
      <NodeCanvasCard title="Downtime by cell">
        <NodeCanvasChecklist
          chip="Shift"
          :rows="[{ id: 'cnc-04', label: 'CNC-04', value: 312, checked: true }]"
        />
      </NodeCanvasCard>
    </template>
  </NodeCanvas>
</template>
```

Node positions are in canvas units at 100% zoom. The canvas keeps dragged positions itself and emits `node-move`; store them if you want them to persist. Passing a new `nodes` array resets the positions.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        nodes
      </code>
    </td>
    
    <td>
      <code>
        NodeCanvasNode[]
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Nodes to place. See the node options below.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        edges
      </code>
    </td>
    
    <td>
      <code>
        NodeCanvasEdge[]
      </code>
    </td>
    
    <td>
      <code>
        []
      </code>
    </td>
    
    <td>
      Connections between nodes. See the edge options below.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        zoom
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1
      </code>
    </td>
    
    <td>
      <code>
        v-model:zoom
      </code>
      
      . Current zoom, where <code>
        1
      </code>
      
       is 100%.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        selected
      </code>
    </td>
    
    <td>
      <code>
        string | null
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
    </td>
    
    <td>
      <code>
        v-model:selected
      </code>
      
      . Id of the selected node.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        breadcrumb
      </code>
    </td>
    
    <td>
      <code>
        string[]
      </code>
    </td>
    
    <td>
      <code>
        []
      </code>
    </td>
    
    <td>
      Header breadcrumb. The last item is the current page.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        grid-size
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        24
      </code>
    </td>
    
    <td>
      Grid cell size in canvas units.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        min-zoom
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.25
      </code>
    </td>
    
    <td>
      Lowest zoom.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        max-zoom
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      Highest zoom.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        zoom-step
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.1
      </code>
    </td>
    
    <td>
      Amount each zoom button press adds or removes.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        minimap
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Shows the minimap in the top-right corner.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        aria-label
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'Canvas'
      </code>
    </td>
    
    <td>
      Accessible name of the canvas region.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes for the root element.
    </td>
  </tr>
</tbody>
</table>

## Node options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Unique id. Also names the <code>
        #node-<id>
      </code>
      
       slot.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        label
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Accessible name of the node.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        x
      </code>
      
      , <code>
        y
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Top-left corner in canvas units.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Width in canvas units.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        height
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Height in canvas units. Used by edges, handles and the minimap.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rotate
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Tilt in degrees.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Corner radius of the selection outline. Defaults to <code>
        24
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        minimapColor
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Fill of the node in the minimap. Defaults to grey.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        interactive
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Set <code>
        false
      </code>
      
       to stop the node from being dragged or selected.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        locked
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Keeps the node selectable, but stops dragging, resizing and deleting.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        resizable
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Set <code>
        false
      </code>
      
       to hide the resize handles. Defaults to <code>
        true
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        minWidth
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Smallest width a resize can reach. Defaults to <code>
        80
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        minHeight
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Smallest height a resize can reach. Defaults to <code>
        48
      </code>
      
      .
    </td>
  </tr>
</tbody>
</table>

## Edge options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Unique id.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        from
      </code>
      
      , <code>
        to
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Source and target node ids.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fromAnchor
      </code>
    </td>
    
    <td>
      <code>
        NodeCanvasAnchor
      </code>
    </td>
    
    <td>
      <code>
        { side, offset }
      </code>
      
      . Side is <code>
        left
      </code>
      
      , <code>
        right
      </code>
      
      , <code>
        top
      </code>
      
       or <code>
        bottom
      </code>
      
      . Defaults to <code>
        right
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        toAnchor
      </code>
    </td>
    
    <td>
      <code>
        NodeCanvasAnchor
      </code>
    </td>
    
    <td>
      Same shape. Defaults to <code>
        left
      </code>
      
      . <code>
        offset
      </code>
      
       runs from <code>
        0
      </code>
      
       to <code>
        1
      </code>
      
       along the side.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        variant
      </code>
    </td>
    
    <td>
      <code>
        'solid' | 'dashed'
      </code>
    </td>
    
    <td>
      Solid draws a blue line, dashed a muted dashed line. Defaults to <code>
        solid
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        arrow
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Draws an arrowhead at the target.
    </td>
  </tr>
</tbody>
</table>

## Slots and events

<table>
<thead>
  <tr>
    <th>
      Name
    </th>
    
    <th>
      Props
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        #node-<id>
      </code>
      
       slot
    </td>
    
    <td>
      <code>
        { node, selected }
      </code>
    </td>
    
    <td>
      Content of one node. It fills the node box.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        #node
      </code>
      
       slot
    </td>
    
    <td>
      <code>
        { node, selected }
      </code>
    </td>
    
    <td>
      Fallback content for nodes without their own slot, e.g. nodes added at runtime.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        #selection-toolbar
      </code>
      
       slot
    </td>
    
    <td>
      <code>
        { node, selected }
      </code>
    </td>
    
    <td>
      Floats above the top-right corner of the selected node.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        #toolbar
      </code>
      
       slot
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Header area on the right, before the actions.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        #actions
      </code>
      
       slot
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Header actions, such as a Share button.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        node-move
      </code>
      
       event
    </td>
    
    <td>
      <code>
        (id, { x, y })
      </code>
    </td>
    
    <td>
      Fires while a node is dragged or moved with the arrow keys.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        node-resize
      </code>
      
       event
    </td>
    
    <td>
      <code>
        (id, { x, y, width, height })
      </code>
    </td>
    
    <td>
      Fires while a corner handle is dragged.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        node-delete
      </code>
      
       event
    </td>
    
    <td>
      <code>
        (id)
      </code>
    </td>
    
    <td>
      Delete or Backspace on the selected, unlocked node. Remove it from <code>
        nodes
      </code>
      
       to delete it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        canvas-click
      </code>
      
       event
    </td>
    
    <td>
      <code>
        ({ x, y })
      </code>
    </td>
    
    <td>
      A click on empty canvas, in canvas units. Use it to place new nodes.
    </td>
  </tr>
</tbody>
</table>

## Node bodies

<table>
<thead>
  <tr>
    <th>
      Component
    </th>
    
    <th>
      Props
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        NodeCanvasSource
      </code>
    </td>
    
    <td>
      <code>
        label
      </code>
      
      , <code>
        icon
      </code>
    </td>
    
    <td>
      Glowing blue source pill with a switch button. Emits <code>
        switch
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        NodeCanvasNote
      </code>
    </td>
    
    <td>
      <code>
        v-model:text
      </code>
      
      , <code>
        editable
      </code>
      
      , <code>
        autofocus
      </code>
      
      , <code>
        placeholder
      </code>
    </td>
    
    <td>
      Yellow sticky note. With <code>
        editable
      </code>
      
       you can type into it. Tilt it with the node's <code>
        rotate
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        NodeCanvasCard
      </code>
    </td>
    
    <td>
      <code>
        title
      </code>
      
      , <code>
        icon
      </code>
      
      , <code>
        icon-class
      </code>
    </td>
    
    <td>
      Titled card. Put any content in the default slot.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        NodeCanvasBars
      </code>
    </td>
    
    <td>
      <code>
        bars
      </code>
      
      , <code>
        label
      </code>
    </td>
    
    <td>
      Stacked bar chart. Each bar is a list of <code>
        { value, color }
      </code>
      
       from the bottom.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        NodeCanvasChecklist
      </code>
    </td>
    
    <td>
      <code>
        rows
      </code>
      
      , <code>
        chip
      </code>
      
      , <code>
        label-header
      </code>
      
      , <code>
        value-header
      </code>
    </td>
    
    <td>
      Checkbox rows with Filter (checked only) and Sort. Emits <code>
        change
      </code>
      
      .
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The canvas is a focusable region. Arrow keys pan, `+` and `-` zoom, and Escape clears the selection.
- Each node is focusable. Enter or Space selects it, and arrow keys move it (Shift moves further). Delete or Backspace fires `node-delete`.
- The corner handles of the selected node are buttons. Focus one and use the arrow keys to resize from that corner.
- The zoom percentage is announced through `aria-live`.
- The minimap is a button. Pressing it with the keyboard centers the view on the nodes.
- Checklist rows are real checkboxes with labels, and Filter and Sort set `aria-pressed`.
