create-substrate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +27 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/prompts.d.ts +6 -0
  6. package/dist/prompts.d.ts.map +1 -0
  7. package/dist/prompts.js +127 -0
  8. package/dist/prompts.js.map +1 -0
  9. package/dist/scaffold.d.ts +10 -0
  10. package/dist/scaffold.d.ts.map +1 -0
  11. package/dist/scaffold.js +395 -0
  12. package/dist/scaffold.js.map +1 -0
  13. package/dist/surfaces/3d-scene.d.ts +3 -0
  14. package/dist/surfaces/3d-scene.d.ts.map +1 -0
  15. package/dist/surfaces/3d-scene.js +184 -0
  16. package/dist/surfaces/3d-scene.js.map +1 -0
  17. package/dist/surfaces/animation.d.ts +3 -0
  18. package/dist/surfaces/animation.d.ts.map +1 -0
  19. package/dist/surfaces/animation.js +211 -0
  20. package/dist/surfaces/animation.js.map +1 -0
  21. package/dist/surfaces/blank.d.ts +3 -0
  22. package/dist/surfaces/blank.d.ts.map +1 -0
  23. package/dist/surfaces/blank.js +72 -0
  24. package/dist/surfaces/blank.js.map +1 -0
  25. package/dist/surfaces/canvas-2d.d.ts +3 -0
  26. package/dist/surfaces/canvas-2d.d.ts.map +1 -0
  27. package/dist/surfaces/canvas-2d.js +139 -0
  28. package/dist/surfaces/canvas-2d.js.map +1 -0
  29. package/dist/surfaces/data-vis.d.ts +3 -0
  30. package/dist/surfaces/data-vis.d.ts.map +1 -0
  31. package/dist/surfaces/data-vis.js +175 -0
  32. package/dist/surfaces/data-vis.js.map +1 -0
  33. package/dist/surfaces/image-gen.d.ts +3 -0
  34. package/dist/surfaces/image-gen.d.ts.map +1 -0
  35. package/dist/surfaces/image-gen.js +193 -0
  36. package/dist/surfaces/image-gen.js.map +1 -0
  37. package/dist/surfaces/index.d.ts +4 -0
  38. package/dist/surfaces/index.d.ts.map +1 -0
  39. package/dist/surfaces/index.js +17 -0
  40. package/dist/surfaces/index.js.map +1 -0
  41. package/dist/surfaces/node-editor.d.ts +3 -0
  42. package/dist/surfaces/node-editor.d.ts.map +1 -0
  43. package/dist/surfaces/node-editor.js +211 -0
  44. package/dist/surfaces/node-editor.js.map +1 -0
  45. package/dist/surfaces/types.d.ts +22 -0
  46. package/dist/surfaces/types.d.ts.map +1 -0
  47. package/dist/surfaces/types.js +10 -0
  48. package/dist/surfaces/types.js.map +1 -0
  49. package/dist/utils/detect-pm.d.ts +5 -0
  50. package/dist/utils/detect-pm.d.ts.map +1 -0
  51. package/dist/utils/detect-pm.js +20 -0
  52. package/dist/utils/detect-pm.js.map +1 -0
  53. package/dist/utils/fs.d.ts +7 -0
  54. package/dist/utils/fs.d.ts.map +1 -0
  55. package/dist/utils/fs.js +52 -0
  56. package/dist/utils/fs.js.map +1 -0
  57. package/dist/utils/logger.d.ts +10 -0
  58. package/dist/utils/logger.d.ts.map +1 -0
  59. package/dist/utils/logger.js +15 -0
  60. package/dist/utils/logger.js.map +1 -0
  61. package/dist/utils/shell.d.ts +7 -0
  62. package/dist/utils/shell.d.ts.map +1 -0
  63. package/dist/utils/shell.js +28 -0
  64. package/dist/utils/shell.js.map +1 -0
  65. package/package.json +35 -0
  66. package/skills/3d-scene/SKILL.md +172 -0
  67. package/skills/animation/SKILL.md +194 -0
  68. package/skills/canvas-2d/SKILL.md +132 -0
  69. package/skills/composing-panels/SKILL.md +309 -0
  70. package/skills/create-custom-tool/SKILL.md +157 -0
  71. package/skills/data-visualisation/SKILL.md +228 -0
  72. package/skills/image-generation/SKILL.md +211 -0
  73. package/skills/scaffold-playground/SKILL.md +141 -0
  74. package/skills/substrate-canvas/SKILL.md +217 -0
  75. package/skills/substrate-controls/SKILL.md +242 -0
  76. package/skills/substrate-feedback/SKILL.md +219 -0
  77. package/skills/substrate-interaction/SKILL.md +286 -0
  78. package/skills/substrate-nodes/SKILL.md +208 -0
  79. package/skills/substrate-scaffold/SKILL.md +206 -0
  80. package/skills/theming/SKILL.md +117 -0
  81. package/skills/wire-interactions/SKILL.md +155 -0
@@ -0,0 +1,132 @@
1
+ # 2D canvas
2
+
3
+ Guide to building spatial 2D tools with Substrate — shape editors, diagram builders, whiteboard apps, and Figma-like design surfaces.
4
+
5
+ ## When to use this surface
6
+
7
+ Use a 2D canvas when the app involves objects on an infinite pannable/zoomable plane: design tools, diagram editors, whiteboard apps, map builders, node graphs, or any tool where users place, select, and manipulate flat shapes and elements.
8
+
9
+ ## Surface setup
10
+
11
+ This is Substrate's default surface. The `DesignCanvas` component provides a full 2D rendering pipeline out of the box.
12
+
13
+ ### Install
14
+
15
+ ```bash
16
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/design-canvas
17
+ ```
18
+
19
+ This pulls in the canvas renderer, hit-test system, interaction hooks, transform utilities, and all required stores.
20
+
21
+ ### Mount
22
+
23
+ ```tsx
24
+ import { DesignCanvas } from "@/registry/substrate/canvas/design-canvas"
25
+
26
+ export default function PlaygroundPage() {
27
+ return (
28
+ <div className="h-screen w-screen bg-canvas">
29
+ <DesignCanvas />
30
+ </div>
31
+ )
32
+ }
33
+ ```
34
+
35
+ ## Architecture
36
+
37
+ The 2D canvas uses a layered pipeline:
38
+
39
+ ```
40
+ Pure renderer functions (canvas/renderer.ts)
41
+ → useCanvasRenderer (rAF loop with dirty flag)
42
+ → useCanvasInteraction (state machine for pointer events)
43
+ → useCanvasDrag (ref-based drag tracking, no re-renders)
44
+ → useCanvasTransform (screen ↔ world coordinate conversion)
45
+ → useHitTest (spatial queries)
46
+ → useDocumentStore (element CRUD + undo/redo)
47
+ ```
48
+
49
+ ### Rendering
50
+
51
+ The renderer uses the Canvas 2D API directly — no React reconciliation in the render loop. Renderer functions are pure: they take a context, camera, and element data, and draw. The `useCanvasRenderer` hook runs a `requestAnimationFrame` loop that only repaints when `markDirty()` has been called.
52
+
53
+ ### Coordinate systems
54
+
55
+ - **Screen space** — pixel coordinates relative to the canvas element
56
+ - **World space** — infinite canvas coordinates, affected by camera pan/zoom
57
+
58
+ Always convert pointer events to world space before hit-testing or element manipulation. Use `screenToWorld()` and `worldToScreen()` from `useCanvasTransform`.
59
+
60
+ ### State
61
+
62
+ `useDocumentStore` holds the element map, element order, selection set, and history stack. All mutations go through store actions. Call `pushSnapshot()` before any mutation to enable undo/redo.
63
+
64
+ ## Wiring to Substrate chrome
65
+
66
+ ### Toolbar
67
+
68
+ The default tool set for a 2D canvas:
69
+
70
+ | Tool | Purpose | Shortcut |
71
+ | --------- | ----------------------------- | -------- |
72
+ | Select | Click/drag to select elements | V |
73
+ | Hand | Pan the canvas | H |
74
+ | Rectangle | Draw rectangles | R |
75
+ | Ellipse | Draw ellipses | O |
76
+ | Line | Draw lines | L |
77
+ | Text | Place text elements | T |
78
+ | Frame | Draw frame containers | F |
79
+
80
+ ### Panels
81
+
82
+ See the [composing panels](./composing-panels.md) skill for full details. In summary:
83
+
84
+ - **Left panel** — layers list (render order, visibility toggles, selection)
85
+ - **Right panel** — properties inspector (transform, fill, stroke, opacity, type-specific properties)
86
+
87
+ ### Interactions
88
+
89
+ See the [wire interactions](./wire-interactions.md) skill for the full state machine. Key patterns:
90
+
91
+ - Click to select, shift-click to multi-select
92
+ - Drag on empty space with select tool → marquee selection
93
+ - Drag on selected element → move
94
+ - Drag on resize handle → resize
95
+ - Drag with shape tool → create new element
96
+ - Middle-click or hand tool → pan
97
+
98
+ ## Extending the canvas
99
+
100
+ ### Custom elements
101
+
102
+ To add a new element type (e.g. star, arrow, image), follow the [create custom tool](./create-custom-tool.md) skill. The pipeline is: type definition → factory → renderer → hit-test → interaction → shortcut → cursor → toolbar.
103
+
104
+ ### Custom rendering
105
+
106
+ For specialised visuals (gradients, patterns, blend modes, filters), modify the renderer functions in `canvas/renderer.ts`. Each element type has its own render case — add drawing logic there.
107
+
108
+ ### Canvas overlays
109
+
110
+ For UI elements that sit on top of the canvas (selection indicators, context menus, floating labels), render them as absolutely positioned DOM elements rather than drawing them on the canvas. Convert world positions to screen positions for placement.
111
+
112
+ ## Theming
113
+
114
+ The canvas background is driven by the `--canvas` CSS variable, read at render time via `getComputedStyle`. Grid dots use `--canvas-foreground`. See the [theming](./theming.md) skill for the full token table.
115
+
116
+ ## What to build
117
+
118
+ Some examples of what this surface enables:
119
+
120
+ - **Design tool** — shape creation, styling, alignment, export (the default Substrate experience)
121
+ - **Diagram editor** — nodes and connectors with auto-routing, swimlanes, labels
122
+ - **Whiteboard** — freeform drawing, sticky notes, image placement, real-time collaboration
123
+ - **Map builder** — tile-based or freeform map editor for games or floor plans
124
+ - **Node graph** — visual programming interface with typed ports and connections
125
+ - **Wireframe tool** — UI component blocks with snap-to-grid and responsive breakpoints
126
+
127
+ ## Related reference skills
128
+
129
+ - **substrate-canvas** — full catalogue of spatial hooks (useViewport, useSelection, useDragReorder) and utilities (snapToGrid, boundingBox, math)
130
+ - **substrate-scaffold** — Toolbar, Panel, StatusBar, ZoomControls for the app shell
131
+ - **substrate-controls** — NumberInput, Slider, ColourPicker, ActionControls for property panes
132
+ - **substrate-interaction** — useUndoable, useClipboard, createKeyboardShortcuts, LayerList
@@ -0,0 +1,309 @@
1
+ # Composing panels
2
+
3
+ Guide to building property panels, layer lists, and inspector UIs using Substrate's panel component system.
4
+
5
+ ## Component hierarchy
6
+
7
+ Substrate's panel system is a composition of four components:
8
+
9
+ ```
10
+ Panel (side panel shell — left or right)
11
+ └── Pane (collapsible content section)
12
+ ├── PaneHeader
13
+ │ ├── PaneLabel (section title)
14
+ │ └── PaneAction (header button)
15
+ └── Action (form field row)
16
+ ├── ActionLabel
17
+ └── ActionControls
18
+ └── Slider, input, colour picker, etc.
19
+ ```
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/panel
25
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/pane
26
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/action
27
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/slider
28
+ ```
29
+
30
+ ## Panel positioning
31
+
32
+ ```tsx
33
+ import { Panel } from "@/registry/substrate/components/panel"
34
+
35
+ {
36
+ /* Left panel — layers, tool options */
37
+ }
38
+ ;<Panel position="left">{/* Panes go here */}</Panel>
39
+
40
+ {
41
+ /* Right panel — properties inspector */
42
+ }
43
+ ;<Panel position="right">{/* Panes go here */}</Panel>
44
+ ```
45
+
46
+ Panels are absolutely positioned and sized via CSS. They sit alongside the canvas in the playground layout.
47
+
48
+ ## Building a properties inspector
49
+
50
+ A typical properties panel reads from `useDocumentStore` and writes back via `updateElement`:
51
+
52
+ ```tsx
53
+ "use client"
54
+
55
+ import { useDocumentStore } from "@/registry/substrate/stores/document-store"
56
+ import {
57
+ Pane,
58
+ PaneHeader,
59
+ PaneLabel,
60
+ PaneAction,
61
+ } from "@/registry/substrate/components/pane"
62
+ import {
63
+ Action,
64
+ ActionLabel,
65
+ ActionControls,
66
+ } from "@/registry/substrate/components/action"
67
+ import { Slider } from "@/registry/substrate/components/slider"
68
+
69
+ export function PropertiesPanel() {
70
+ const selectedIds = useDocumentStore((s) => s.selectedIds)
71
+ const elements = useDocumentStore((s) => s.elements)
72
+ const updateElement = useDocumentStore((s) => s.updateElement)
73
+ const pushSnapshot = useDocumentStore((s) => s.pushSnapshot)
74
+
75
+ // Single selection only for properties
76
+ if (selectedIds.size !== 1) return null
77
+ const element = elements[[...selectedIds][0]]
78
+ if (!element) return null
79
+
80
+ const handleChange = (changes: Record<string, unknown>) => {
81
+ pushSnapshot()
82
+ updateElement(element.id, changes)
83
+ }
84
+
85
+ return (
86
+ <>
87
+ {/* Position and size */}
88
+ <Pane>
89
+ <PaneHeader>
90
+ <PaneLabel>Transform</PaneLabel>
91
+ </PaneHeader>
92
+ <Action>
93
+ <ActionLabel>X</ActionLabel>
94
+ <ActionControls>
95
+ <input
96
+ type="number"
97
+ value={Math.round(element.x)}
98
+ onChange={(e) => handleChange({ x: Number(e.target.value) })}
99
+ className="w-full bg-transparent text-xs tabular-nums"
100
+ />
101
+ </ActionControls>
102
+ </Action>
103
+ <Action>
104
+ <ActionLabel>Y</ActionLabel>
105
+ <ActionControls>
106
+ <input
107
+ type="number"
108
+ value={Math.round(element.y)}
109
+ onChange={(e) => handleChange({ y: Number(e.target.value) })}
110
+ className="w-full bg-transparent text-xs tabular-nums"
111
+ />
112
+ </ActionControls>
113
+ </Action>
114
+ <Action>
115
+ <ActionLabel>W</ActionLabel>
116
+ <ActionControls>
117
+ <input
118
+ type="number"
119
+ value={Math.round(element.width)}
120
+ onChange={(e) => handleChange({ width: Number(e.target.value) })}
121
+ className="w-full bg-transparent text-xs tabular-nums"
122
+ />
123
+ </ActionControls>
124
+ </Action>
125
+ <Action>
126
+ <ActionLabel>H</ActionLabel>
127
+ <ActionControls>
128
+ <input
129
+ type="number"
130
+ value={Math.round(element.height)}
131
+ onChange={(e) => handleChange({ height: Number(e.target.value) })}
132
+ className="w-full bg-transparent text-xs tabular-nums"
133
+ />
134
+ </ActionControls>
135
+ </Action>
136
+ </Pane>
137
+
138
+ {/* Opacity */}
139
+ <Pane>
140
+ <PaneHeader>
141
+ <PaneLabel>Opacity</PaneLabel>
142
+ </PaneHeader>
143
+ <Action>
144
+ <ActionLabel>{Math.round(element.opacity * 100)}%</ActionLabel>
145
+ <ActionControls>
146
+ <Slider
147
+ value={[element.opacity * 100]}
148
+ onValueChange={([v]) => handleChange({ opacity: v / 100 })}
149
+ max={100}
150
+ step={1}
151
+ />
152
+ </ActionControls>
153
+ </Action>
154
+ </Pane>
155
+ </>
156
+ )
157
+ }
158
+ ```
159
+
160
+ ## Building a layers panel
161
+
162
+ A layers panel lists elements in render order (bottom to top) and lets users select, reorder, and toggle visibility:
163
+
164
+ ```tsx
165
+ "use client"
166
+
167
+ import { useDocumentStore } from "@/registry/substrate/stores/document-store"
168
+ import {
169
+ Pane,
170
+ PaneHeader,
171
+ PaneLabel,
172
+ } from "@/registry/substrate/components/pane"
173
+
174
+ export function LayersPanel() {
175
+ const elements = useDocumentStore((s) => s.elements)
176
+ const elementOrder = useDocumentStore((s) => s.elementOrder)
177
+ const selectedIds = useDocumentStore((s) => s.selectedIds)
178
+ const select = useDocumentStore((s) => s.select)
179
+ const updateElement = useDocumentStore((s) => s.updateElement)
180
+
181
+ return (
182
+ <Pane>
183
+ <PaneHeader>
184
+ <PaneLabel>Layers</PaneLabel>
185
+ </PaneHeader>
186
+ <div className="flex flex-col-reverse">
187
+ {elementOrder.map((id) => {
188
+ const el = elements[id]
189
+ if (!el) return null
190
+ return (
191
+ <button
192
+ key={id}
193
+ onClick={(e) => select([id], e.shiftKey)}
194
+ data-selected={selectedIds.has(id) || undefined}
195
+ className="flex items-center gap-2 px-2 py-1 text-xs rounded hover:bg-accent data-[selected]:bg-accent"
196
+ >
197
+ <span className="truncate">{el.name}</span>
198
+ <button
199
+ onClick={(e) => {
200
+ e.stopPropagation()
201
+ updateElement(id, { visible: !el.visible })
202
+ }}
203
+ className="ml-auto opacity-50 hover:opacity-100"
204
+ >
205
+ {el.visible ? "👁" : "—"}
206
+ </button>
207
+ </button>
208
+ )
209
+ })}
210
+ </div>
211
+ </Pane>
212
+ )
213
+ }
214
+ ```
215
+
216
+ ## Wiring panels to the store
217
+
218
+ Use `usePanelStore` to control panel visibility:
219
+
220
+ ```tsx
221
+ import { usePanelStore } from "@/registry/substrate/stores/panel-store"
222
+
223
+ const leftPanel = usePanelStore((s) => s.leftPanel)
224
+ const rightPanel = usePanelStore((s) => s.rightPanel)
225
+ const toggleLeft = usePanelStore((s) => s.toggleLeft)
226
+ const toggleRight = usePanelStore((s) => s.toggleRight)
227
+ ```
228
+
229
+ Then conditionally render:
230
+
231
+ ```tsx
232
+ {
233
+ leftPanel && (
234
+ <Panel position="left">
235
+ <LayersPanel />
236
+ </Panel>
237
+ )
238
+ }
239
+
240
+ {
241
+ rightPanel && (
242
+ <Panel position="right">
243
+ <PropertiesPanel />
244
+ </Panel>
245
+ )
246
+ }
247
+ ```
248
+
249
+ ## Creating custom panes
250
+
251
+ Follow the compound component pattern:
252
+
253
+ 1. **Wrap in `<Pane>`** for consistent spacing
254
+ 2. **Use `<PaneHeader>` + `<PaneLabel>`** for the section title
255
+ 3. **Use `<PaneAction>`** for header buttons (add, remove, toggle)
256
+ 4. **Use `<Action>` + `<ActionLabel>` + `<ActionControls>`** for each property row
257
+ 5. **Read from `useDocumentStore`** with granular selectors
258
+ 6. **Call `pushSnapshot()` before mutations** to enable undo/redo
259
+ 7. **Use `updateElement(id, changes)`** to write back
260
+
261
+ ## Patterns
262
+
263
+ ### Conditional panes by element type
264
+
265
+ Show different properties based on the selected element's type:
266
+
267
+ ```tsx
268
+ {
269
+ element.type === "rectangle" && (
270
+ <Pane>
271
+ <PaneHeader>
272
+ <PaneLabel>Corner radius</PaneLabel>
273
+ </PaneHeader>
274
+ <Action>
275
+ <ActionControls>
276
+ <Slider
277
+ value={[element.cornerRadius]}
278
+ onValueChange={([v]) => handleChange({ cornerRadius: v })}
279
+ max={100}
280
+ />
281
+ </ActionControls>
282
+ </Action>
283
+ </Pane>
284
+ )
285
+ }
286
+ ```
287
+
288
+ ### Multi-selection
289
+
290
+ When multiple elements are selected, show shared properties or a summary:
291
+
292
+ ```tsx
293
+ if (selectedIds.size > 1) {
294
+ return (
295
+ <Pane>
296
+ <PaneHeader>
297
+ <PaneLabel>{selectedIds.size} elements selected</PaneLabel>
298
+ </PaneHeader>
299
+ {/* Show shared opacity, alignment buttons, etc. */}
300
+ </Pane>
301
+ )
302
+ }
303
+ ```
304
+
305
+ ## Related reference skills
306
+
307
+ - **substrate-scaffold** — full catalogue of Panel, Pane, CollapsiblePane, PaneHeader, and layout components
308
+ - **substrate-controls** — full catalogue of ActionControls, NumberInput, Slider, NumberSlider, Select, Toggle, ColourPicker, and the data-slot composition pattern
309
+ - **substrate-interaction** — HistoryControls (auto-positioned in PaneHeader), LayerList, TreeList
@@ -0,0 +1,157 @@
1
+ # Create a custom tool
2
+
3
+ End-to-end guide for adding a new canvas tool to Substrate — from type definition through to toolbar entry and keyboard shortcut.
4
+
5
+ ## Overview
6
+
7
+ A tool in Substrate touches these files:
8
+
9
+ 1. **`types.ts`** — add to the `ToolType` union
10
+ 2. **`lib/element-factory.ts`** — add a factory function (if the tool creates elements)
11
+ 3. **`canvas/renderer.ts`** — add rendering logic
12
+ 4. **`canvas/hit-test.ts`** — add hit-testing (if clickable)
13
+ 5. **`hooks/use-canvas-interaction.ts`** — handle pointer events for the tool
14
+ 6. **`hooks/use-keyboard-shortcuts.ts`** — bind a key
15
+ 7. **`canvas/design-canvas.tsx`** — add cursor mapping
16
+ 8. **Toolbar** — add a toggle in your page
17
+
18
+ ## Step 1: Define the type
19
+
20
+ In `types.ts`, extend the `ToolType` union:
21
+
22
+ ```ts
23
+ export type ToolType =
24
+ | "select"
25
+ | "hand"
26
+ | "rectangle"
27
+ | "ellipse"
28
+ | "line"
29
+ | "text"
30
+ | "frame"
31
+ | "your-tool" // ← add here
32
+ ```
33
+
34
+ If the tool creates a new element type, also extend `ElementType` and create a corresponding interface:
35
+
36
+ ```ts
37
+ export type ElementType = "rectangle" | "ellipse" | "text" | "line" | "frame" | "your-element"
38
+
39
+ export interface YourElement extends BaseElement {
40
+ type: "your-element"
41
+ // Custom properties
42
+ someProperty: number
43
+ }
44
+
45
+ export type DesignElement =
46
+ | RectangleElement
47
+ | EllipseElement
48
+ | TextElement
49
+ | LineElement
50
+ | FrameElement
51
+ | YourElement
52
+ ```
53
+
54
+ ## Step 2: Element factory (if creating elements)
55
+
56
+ In `lib/element-factory.ts`, add a factory:
57
+
58
+ ```ts
59
+ export function createYourElement(x: number, y: number, width: number, height: number): YourElement {
60
+ return {
61
+ id: generateId(),
62
+ type: "your-element",
63
+ name: "Your Element",
64
+ x, y, width, height,
65
+ rotation: 0,
66
+ opacity: 1,
67
+ fill: { type: "solid", color: "#6366f1", opacity: 1 },
68
+ stroke: null,
69
+ locked: false,
70
+ visible: true,
71
+ someProperty: 42,
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Step 3: Renderer
77
+
78
+ In `canvas/renderer.ts`, add a rendering case. The renderer uses the Canvas 2D API directly:
79
+
80
+ ```ts
81
+ case "your-element": {
82
+ ctx.save()
83
+ // Transform, draw, restore
84
+ ctx.restore()
85
+ break
86
+ }
87
+ ```
88
+
89
+ ## Step 4: Hit-test (if clickable)
90
+
91
+ In `canvas/hit-test.ts`, add detection logic in the `hitTestElement` function:
92
+
93
+ ```ts
94
+ case "your-element":
95
+ return pointInBounds(localPoint, { x: 0, y: 0, width: el.width, height: el.height })
96
+ ```
97
+
98
+ ## Step 5: Interaction hook
99
+
100
+ In `hooks/use-canvas-interaction.ts`, handle the tool in the pointer event state machine. The key areas are:
101
+
102
+ - **`onPointerDown`** — detect what was clicked, start the appropriate drag type
103
+ - **`onPointerMove`** — update drag state, preview creation
104
+ - **`onPointerUp`** — finalise the action (create element, commit move, etc.)
105
+
106
+ For a creation tool, the pattern matches the existing rectangle/ellipse flow — start a "create" drag on pointer down, preview during move, commit on pointer up.
107
+
108
+ ## Step 6: Keyboard shortcut
109
+
110
+ In `hooks/use-keyboard-shortcuts.ts`, add a key binding in the switch statement:
111
+
112
+ ```ts
113
+ case "y": setTool("your-tool"); return
114
+ ```
115
+
116
+ ## Step 7: Cursor
117
+
118
+ In `canvas/design-canvas.tsx`, add to `CURSOR_MAP`:
119
+
120
+ ```ts
121
+ const CURSOR_MAP: Record<string, string> = {
122
+ // ...existing entries
123
+ "your-tool": "crosshair",
124
+ }
125
+ ```
126
+
127
+ ## Step 8: Toolbar entry
128
+
129
+ In your playground page, add a toggle:
130
+
131
+ ```tsx
132
+ <ToolbarToggle value="your-tool" tooltip="Your Tool" shortcut="Y">
133
+ <YourIcon className="size-4" />
134
+ </ToolbarToggle>
135
+ ```
136
+
137
+ ## Tools that don't create elements
138
+
139
+ Not every tool creates elements. For tools like a measurement tool, colour picker, or zoom tool:
140
+
141
+ - Skip steps 2 (factory) and 4 (hit-test for new element type)
142
+ - In the interaction hook, handle the tool's pointer events directly (e.g. measure distance between pointer down and up)
143
+ - Store any tool-specific state in a dedicated Zustand store if needed
144
+
145
+ ## Testing your tool
146
+
147
+ 1. Select the tool from the toolbar or press its shortcut key
148
+ 2. Verify the cursor changes
149
+ 3. Test the full interaction cycle: pointer down → move → up
150
+ 4. Check undo/redo works correctly (ensure `pushSnapshot()` is called before mutations)
151
+ 5. Verify the element renders and can be selected/moved/resized
152
+
153
+ ## Related reference skills
154
+
155
+ - **substrate-canvas** — useViewport, snap, boundingBox, hit testing, coordinate maths
156
+ - **substrate-scaffold** — Toolbar (for adding your tool button), StatusBar
157
+ - **substrate-interaction** — createKeyboardShortcuts (for binding a shortcut key), useUndoable