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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +6 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +127 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +395 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/surfaces/3d-scene.d.ts +3 -0
- package/dist/surfaces/3d-scene.d.ts.map +1 -0
- package/dist/surfaces/3d-scene.js +184 -0
- package/dist/surfaces/3d-scene.js.map +1 -0
- package/dist/surfaces/animation.d.ts +3 -0
- package/dist/surfaces/animation.d.ts.map +1 -0
- package/dist/surfaces/animation.js +211 -0
- package/dist/surfaces/animation.js.map +1 -0
- package/dist/surfaces/blank.d.ts +3 -0
- package/dist/surfaces/blank.d.ts.map +1 -0
- package/dist/surfaces/blank.js +72 -0
- package/dist/surfaces/blank.js.map +1 -0
- package/dist/surfaces/canvas-2d.d.ts +3 -0
- package/dist/surfaces/canvas-2d.d.ts.map +1 -0
- package/dist/surfaces/canvas-2d.js +139 -0
- package/dist/surfaces/canvas-2d.js.map +1 -0
- package/dist/surfaces/data-vis.d.ts +3 -0
- package/dist/surfaces/data-vis.d.ts.map +1 -0
- package/dist/surfaces/data-vis.js +175 -0
- package/dist/surfaces/data-vis.js.map +1 -0
- package/dist/surfaces/image-gen.d.ts +3 -0
- package/dist/surfaces/image-gen.d.ts.map +1 -0
- package/dist/surfaces/image-gen.js +193 -0
- package/dist/surfaces/image-gen.js.map +1 -0
- package/dist/surfaces/index.d.ts +4 -0
- package/dist/surfaces/index.d.ts.map +1 -0
- package/dist/surfaces/index.js +17 -0
- package/dist/surfaces/index.js.map +1 -0
- package/dist/surfaces/node-editor.d.ts +3 -0
- package/dist/surfaces/node-editor.d.ts.map +1 -0
- package/dist/surfaces/node-editor.js +211 -0
- package/dist/surfaces/node-editor.js.map +1 -0
- package/dist/surfaces/types.d.ts +22 -0
- package/dist/surfaces/types.d.ts.map +1 -0
- package/dist/surfaces/types.js +10 -0
- package/dist/surfaces/types.js.map +1 -0
- package/dist/utils/detect-pm.d.ts +5 -0
- package/dist/utils/detect-pm.d.ts.map +1 -0
- package/dist/utils/detect-pm.js +20 -0
- package/dist/utils/detect-pm.js.map +1 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +52 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +15 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/shell.d.ts +7 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +28 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +35 -0
- package/skills/3d-scene/SKILL.md +172 -0
- package/skills/animation/SKILL.md +194 -0
- package/skills/canvas-2d/SKILL.md +132 -0
- package/skills/composing-panels/SKILL.md +309 -0
- package/skills/create-custom-tool/SKILL.md +157 -0
- package/skills/data-visualisation/SKILL.md +228 -0
- package/skills/image-generation/SKILL.md +211 -0
- package/skills/scaffold-playground/SKILL.md +141 -0
- package/skills/substrate-canvas/SKILL.md +217 -0
- package/skills/substrate-controls/SKILL.md +242 -0
- package/skills/substrate-feedback/SKILL.md +219 -0
- package/skills/substrate-interaction/SKILL.md +286 -0
- package/skills/substrate-nodes/SKILL.md +208 -0
- package/skills/substrate-scaffold/SKILL.md +206 -0
- package/skills/theming/SKILL.md +117 -0
- package/skills/wire-interactions/SKILL.md +155 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: substrate-interaction
|
|
3
|
+
description: "Cross-cutting interaction patterns for Substrate: selection, undo/redo, clipboard, keyboard shortcuts, history controls, layer lists, and tree lists. Use when wiring up user interaction flows that span multiple components. Triggers on: selection, undo, redo, clipboard, copy, paste, keyboard shortcut, history, layer list, tree list, drag reorder, undo stack."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Substrate interaction
|
|
8
|
+
|
|
9
|
+
Interaction patterns wire together hooks, utilities, and components to create cohesive editing flows – selection, undo/redo, clipboard, keyboard shortcuts, and structural navigation (layers/trees).
|
|
10
|
+
|
|
11
|
+
## When to use this skill
|
|
12
|
+
|
|
13
|
+
Use the interaction skill when you need to:
|
|
14
|
+
|
|
15
|
+
- Wire up undo/redo across a surface
|
|
16
|
+
- Add clipboard (copy/cut/paste) support
|
|
17
|
+
- Register and manage keyboard shortcuts
|
|
18
|
+
- Build a layer list or tree list with selection and reordering
|
|
19
|
+
- Connect HistoryControls to an undo stack
|
|
20
|
+
|
|
21
|
+
For spatial interaction (pan, zoom, snap), see **substrate-canvas**.
|
|
22
|
+
For the controls that display properties, see **substrate-controls**.
|
|
23
|
+
For notifications and command palette, see **substrate-feedback**.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Hooks
|
|
28
|
+
|
|
29
|
+
### useUndoable
|
|
30
|
+
|
|
31
|
+
`registry/substrate/hooks/use-undoable.ts`
|
|
32
|
+
|
|
33
|
+
Lightweight undo/redo stack using refs for performance. Stores snapshots, not diffs.
|
|
34
|
+
|
|
35
|
+
**Returns**: `pushSnapshot`, `undo`, `redo`, `canUndo`, `canRedo`, `undoCount`, `redoCount`, `clear`
|
|
36
|
+
|
|
37
|
+
**Pattern**: Call `pushSnapshot(currentState)` *before* mutating. Call `undo()` / `redo()` to get the previous/next state, then apply it.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const { pushSnapshot, undo, redo, canUndo, canRedo } = useUndoable<State>({ maxHistory: 100 })
|
|
41
|
+
|
|
42
|
+
function handleMove(id: string, x: number, y: number) {
|
|
43
|
+
pushSnapshot(state)
|
|
44
|
+
setState(prev => updateElement(prev, id, { x, y }))
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Connects to**: `HistoryControls` component, `NumberInput.onStart` (push snapshot before scrub), `createKeyboardShortcuts` (⌘Z / ⌘⇧Z).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### useClipboard
|
|
53
|
+
|
|
54
|
+
`registry/substrate/hooks/use-clipboard.ts`
|
|
55
|
+
|
|
56
|
+
Copy/cut/paste with ⌘C/⌘X/⌘V keyboard listeners. Writes to system clipboard where available, falls back to internal ref.
|
|
57
|
+
|
|
58
|
+
**Returns**: `copy`, `cut`, `paste`
|
|
59
|
+
|
|
60
|
+
**Pattern**: Provide `onCopy` (returns items to copy), `onPaste` (receives items), and optionally `onCut`.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
useClipboard({
|
|
64
|
+
onCopy: () => selectedElements.map(el => ({ ...el })),
|
|
65
|
+
onPaste: (items) => {
|
|
66
|
+
pushSnapshot(state)
|
|
67
|
+
addElements(items.map(offset))
|
|
68
|
+
},
|
|
69
|
+
onCut: () => {
|
|
70
|
+
const items = selectedElements.map(el => ({ ...el }))
|
|
71
|
+
pushSnapshot(state)
|
|
72
|
+
removeSelected()
|
|
73
|
+
return items
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Auto-skips**: Input, textarea, and contentEditable elements.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### useSelection
|
|
83
|
+
|
|
84
|
+
`registry/substrate/hooks/use-selection.ts`
|
|
85
|
+
|
|
86
|
+
Multi-select with modifier keys. See **substrate-canvas** for spatial selection use.
|
|
87
|
+
|
|
88
|
+
**Also useful for**: Layer lists and tree lists where items are selected by clicking rows.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### useDragReorder
|
|
93
|
+
|
|
94
|
+
`registry/substrate/hooks/use-drag-reorder.ts`
|
|
95
|
+
|
|
96
|
+
Pointer-based list reordering. See **substrate-canvas** for details.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Utilities
|
|
101
|
+
|
|
102
|
+
### createKeyboardShortcuts
|
|
103
|
+
|
|
104
|
+
`registry/substrate/lib/keyboard-shortcuts.ts`
|
|
105
|
+
|
|
106
|
+
Declarative shortcut registration. Replaces imperative switch statements.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const dispose = createKeyboardShortcuts([
|
|
110
|
+
{ key: "z", meta: true, handler: handleUndo, label: "Undo", group: "Edit" },
|
|
111
|
+
{ key: "z", meta: true, shift: true, handler: handleRedo, label: "Redo", group: "Edit" },
|
|
112
|
+
{ key: "c", meta: true, handler: copy, label: "Copy", group: "Edit" },
|
|
113
|
+
{ key: "v", meta: true, handler: paste, label: "Paste", group: "Edit" },
|
|
114
|
+
{ key: "Delete", handler: deleteSelected, label: "Delete", group: "Edit" },
|
|
115
|
+
{ key: "a", meta: true, handler: selectAll, label: "Select all", group: "Selection" },
|
|
116
|
+
{ key: "v", handler: () => setTool("select"), label: "Select tool", group: "Tools" },
|
|
117
|
+
{ key: "r", handler: () => setTool("rectangle"), label: "Rectangle tool", group: "Tools" },
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
// Clean up on unmount
|
|
121
|
+
useEffect(() => dispose, [dispose])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**formatShortcut**: Converts entries to display strings for context menus and shortcut overlay.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
formatShortcut({ key: "z", meta: true, shift: true }) // → "⌘+Shift+Z"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Connects to**: `ShortcutOverlay` (substrate-feedback) – pass the same groups. `ContextMenuShortcut` – use `formatShortcut` for display.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Components
|
|
135
|
+
|
|
136
|
+
### HistoryControls
|
|
137
|
+
|
|
138
|
+
`registry/substrate/components/history-controls.tsx`
|
|
139
|
+
|
|
140
|
+
Undo/redo button pair. Renders `data-slot="history-controls"` for auto-positioning in PaneHeader and Toolbar.
|
|
141
|
+
|
|
142
|
+
**Props**: `canUndo`, `canRedo`, `onUndo`, `onRedo`
|
|
143
|
+
|
|
144
|
+
**Typical placement**:
|
|
145
|
+
- Inside `PaneHeader` – auto-positioned to the right via `**:data-[slot=history-controls]:ml-auto`
|
|
146
|
+
- Inside `Toolbar` – auto-compacted via `**:data-[slot=history-controls]:gap-0`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### LayerList
|
|
151
|
+
|
|
152
|
+
`registry/substrate/components/layer-list.tsx`
|
|
153
|
+
|
|
154
|
+
Flat layer panel with selection, visibility, lock, rename, and reorder.
|
|
155
|
+
|
|
156
|
+
**Exports**: `LayerList`, `LayerItem`, `LayerIcon`, `LayerName`, `LayerActions`, `LayerToggle`, `LayerVisibilityToggle`, `LayerLockToggle`
|
|
157
|
+
|
|
158
|
+
**Features**:
|
|
159
|
+
- `LayerItem` supports `selected`, `hovered`, `depth` props
|
|
160
|
+
- `LayerName` enables double-click inline rename
|
|
161
|
+
- `LayerActions` are hidden by default, visible on row hover
|
|
162
|
+
- `LayerVisibilityToggle` and `LayerLockToggle` use eye/lock icons
|
|
163
|
+
|
|
164
|
+
**When to use**: 2D canvas surfaces, image editors, animation layers – any flat list of elements. For hierarchical structures, use TreeList instead.
|
|
165
|
+
|
|
166
|
+
**Connects to**: `useSelection` for multi-select, `useDragReorder` for reordering, `ContextMenu` (substrate-feedback) for right-click actions.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### TreeList
|
|
171
|
+
|
|
172
|
+
`registry/substrate/components/tree-list.tsx`
|
|
173
|
+
|
|
174
|
+
Hierarchical tree with expand/collapse, depth indentation, and icons.
|
|
175
|
+
|
|
176
|
+
**Exports**: `TreeList`, `TreeItem`, `TreeExpandButton`, `TreeItemContent`, `TreeItemIcon`, `TreeItemActions` + `useTreeExpansion` hook
|
|
177
|
+
|
|
178
|
+
**Features**:
|
|
179
|
+
- 16px depth indentation per level
|
|
180
|
+
- `TreeExpandButton` rotates chevron on expand
|
|
181
|
+
- `useTreeExpansion` manages expanded state: `isExpanded`, `toggle`, `expandAll`, `collapseAll`
|
|
182
|
+
|
|
183
|
+
**When to use**: Scene graph (3D), frame hierarchy (2D), file trees, component trees, node groups. For flat lists, use LayerList instead.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Wiring it all together
|
|
188
|
+
|
|
189
|
+
### Complete undo/redo flow
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
┌─────────────────────────────────────────────────┐
|
|
193
|
+
│ User action (move, resize, delete, etc.) │
|
|
194
|
+
│ → pushSnapshot(currentState) │
|
|
195
|
+
│ → mutate state │
|
|
196
|
+
├─────────────────────────────────────────────────┤
|
|
197
|
+
│ ⌘Z pressed (via createKeyboardShortcuts) │
|
|
198
|
+
│ → undo() returns previous state │
|
|
199
|
+
│ → apply to store │
|
|
200
|
+
├─────────────────────────────────────────────────┤
|
|
201
|
+
│ HistoryControls (in PaneHeader or Toolbar) │
|
|
202
|
+
│ → reads canUndo/canRedo │
|
|
203
|
+
│ → calls onUndo/onRedo │
|
|
204
|
+
├─────────────────────────────────────────────────┤
|
|
205
|
+
│ NumberInput.onStart │
|
|
206
|
+
│ → pushSnapshot before scrub begins │
|
|
207
|
+
└─────────────────────────────────────────────────┘
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Complete shortcut flow
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
┌─────────────────────────────────────────────────┐
|
|
214
|
+
│ createKeyboardShortcuts(entries) │
|
|
215
|
+
│ → registers keydown listener │
|
|
216
|
+
│ → entries include label + group metadata │
|
|
217
|
+
├─────────────────────────────────────────────────┤
|
|
218
|
+
│ ShortcutOverlay (substrate-feedback) │
|
|
219
|
+
│ → reads same groups for display │
|
|
220
|
+
├─────────────────────────────────────────────────┤
|
|
221
|
+
│ ContextMenuShortcut (substrate-feedback) │
|
|
222
|
+
│ → formatShortcut(entry) for inline display │
|
|
223
|
+
├─────────────────────────────────────────────────┤
|
|
224
|
+
│ CommandPalette (substrate-feedback) │
|
|
225
|
+
│ → CommandShortcut shows the key binding │
|
|
226
|
+
└─────────────────────────────────────────────────┘
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Layer list with full interaction
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
function LayerPanel() {
|
|
233
|
+
const { selectedIds, select, isSelected } = useSelection({ items: elementIds })
|
|
234
|
+
const { getDragItemProps } = useDragReorder({ items: elements, onReorder: reorderElements })
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<LayerList>
|
|
238
|
+
{elements.map((el, i) => (
|
|
239
|
+
<ContextMenu key={el.id}>
|
|
240
|
+
<ContextMenuTrigger asChild>
|
|
241
|
+
<LayerItem
|
|
242
|
+
selected={isSelected(el.id)}
|
|
243
|
+
onClick={(e) => select(el.id, e)}
|
|
244
|
+
{...getDragItemProps(i)}
|
|
245
|
+
>
|
|
246
|
+
<LayerIcon colour={el.fill?.color} />
|
|
247
|
+
<LayerName value={el.name} onRename={(name) => updateElement(el.id, { name })} />
|
|
248
|
+
<LayerActions>
|
|
249
|
+
<LayerVisibilityToggle
|
|
250
|
+
visible={el.visible}
|
|
251
|
+
onToggle={() => updateElement(el.id, { visible: !el.visible })}
|
|
252
|
+
/>
|
|
253
|
+
<LayerLockToggle
|
|
254
|
+
locked={el.locked}
|
|
255
|
+
onToggle={() => updateElement(el.id, { locked: !el.locked })}
|
|
256
|
+
/>
|
|
257
|
+
</LayerActions>
|
|
258
|
+
</LayerItem>
|
|
259
|
+
</ContextMenuTrigger>
|
|
260
|
+
<ContextMenuContent>
|
|
261
|
+
<ContextMenuItem onSelect={() => duplicateElement(el.id)}>
|
|
262
|
+
Duplicate <ContextMenuShortcut>⌘D</ContextMenuShortcut>
|
|
263
|
+
</ContextMenuItem>
|
|
264
|
+
<ContextMenuItem onSelect={() => deleteElement(el.id)}>
|
|
265
|
+
Delete <ContextMenuShortcut>⌫</ContextMenuShortcut>
|
|
266
|
+
</ContextMenuItem>
|
|
267
|
+
</ContextMenuContent>
|
|
268
|
+
</ContextMenu>
|
|
269
|
+
))}
|
|
270
|
+
</LayerList>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Cross-references
|
|
278
|
+
|
|
279
|
+
| Need | Skill |
|
|
280
|
+
| --- | --- |
|
|
281
|
+
| Panes and panels hosting layer/tree lists | **substrate-scaffold** |
|
|
282
|
+
| Property controls for selected items | **substrate-controls** |
|
|
283
|
+
| Viewport, snap, hit testing | **substrate-canvas** |
|
|
284
|
+
| Toast for delete-with-undo notifications | **substrate-feedback** |
|
|
285
|
+
| Node selection and graph undo | **substrate-nodes** |
|
|
286
|
+
| Surface-specific interaction models | **substrate-surfaces** |
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: substrate-nodes
|
|
3
|
+
description: "Substrate node graph system: node cards, typed ports, port colour theming, and node editor patterns. Use when building node-based editors, visual programming tools, shader graphs, or data flow UIs. Triggers on: node, port, graph, visual programming, shader graph, data flow, pipeline, wire, connection."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Substrate nodes
|
|
8
|
+
|
|
9
|
+
The node system provides card-based nodes with typed ports, a CSS variable colour scheme for port types, and compound components for building node editor UIs.
|
|
10
|
+
|
|
11
|
+
## When to use this skill
|
|
12
|
+
|
|
13
|
+
Use the nodes skill when you need to:
|
|
14
|
+
|
|
15
|
+
- Build a node-based editor (shader graph, data flow, automation pipeline)
|
|
16
|
+
- Add typed input/output ports to nodes
|
|
17
|
+
- Theme port types with custom colours
|
|
18
|
+
- Render node previews, badges, or dividers
|
|
19
|
+
- Plan a node graph architecture
|
|
20
|
+
|
|
21
|
+
For the canvas that hosts nodes (pan/zoom/selection), see **substrate-canvas**.
|
|
22
|
+
For the scaffold shell around the editor, see **substrate-scaffold**.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Components
|
|
27
|
+
|
|
28
|
+
### Node compound component
|
|
29
|
+
|
|
30
|
+
`registry/substrate/components/node.tsx`
|
|
31
|
+
|
|
32
|
+
10 exports forming a complete node card:
|
|
33
|
+
|
|
34
|
+
| Component | Slot | Purpose |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `NodeCard` | `node-card` | Outer card. Selected state adds accent ring. |
|
|
37
|
+
| `NodeHeader` | `node-header` | Optional coloured header strip. Pass `colour` for a category tint. |
|
|
38
|
+
| `NodeTitle` | `node-title` | Node name inside the header. |
|
|
39
|
+
| `NodeBadge` | `node-badge` | Small label (e.g. "GPU", "Deprecated"). |
|
|
40
|
+
| `NodeBody` | `node-body` | Content area below the header. |
|
|
41
|
+
| `NodePorts` | `node-ports` | Container for a column of ports (left or right side). |
|
|
42
|
+
| `NodePort` | `node-port` | A single port row: dot + label. Props: `type`, `label`, `direction`, `connected`. |
|
|
43
|
+
| `NodePortDot` | `node-port-dot` | The coloured circle. Filled when connected, hollow when not. |
|
|
44
|
+
| `NodePreview` | `node-preview` | Thumbnail/preview area inside the node body. |
|
|
45
|
+
| `NodeDivider` | `node-divider` | Thin horizontal line between body sections. |
|
|
46
|
+
|
|
47
|
+
### Port type system
|
|
48
|
+
|
|
49
|
+
7 built-in port types, each with a themed CSS variable:
|
|
50
|
+
|
|
51
|
+
| Type | CSS variable | Default colour |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `number` | `--node-port-number` | `#4a9eff` (blue) |
|
|
54
|
+
| `colour` | `--node-port-colour` | `#e84393` (pink) |
|
|
55
|
+
| `image` | `--node-port-image` | `#00b894` (green) |
|
|
56
|
+
| `geometry` | `--node-port-geometry` | `#fdcb6e` (amber) |
|
|
57
|
+
| `text` | `--node-port-text` | `#a29bfe` (violet) |
|
|
58
|
+
| `boolean` | `--node-port-boolean` | `#fd79a8` (rose) |
|
|
59
|
+
| `any` | `--node-port-any` | `#636e72` (grey) |
|
|
60
|
+
|
|
61
|
+
These follow the standard Substrate CSS variable fallback chain in `globals.css`:
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
--color-node-port-number: var(--node-port-number, #4a9eff);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Consumers override `--node-port-number` to change the colour across their entire app.
|
|
68
|
+
|
|
69
|
+
### Node chrome variables
|
|
70
|
+
|
|
71
|
+
4 additional CSS variables for the node card itself:
|
|
72
|
+
|
|
73
|
+
| Variable | Default | Purpose |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `--node-bg` | `var(--popover)` | Node card background |
|
|
76
|
+
| `--node-border` | `var(--border)` | Node card border |
|
|
77
|
+
| `--node-header-bg` | `var(--muted)` | Header strip background |
|
|
78
|
+
| `--node-selected-ring` | `var(--accent)` | Selected ring colour |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### Basic node
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<NodeCard selected={isSelected}>
|
|
88
|
+
<NodeHeader colour="#4a9eff">
|
|
89
|
+
<NodeTitle>Add</NodeTitle>
|
|
90
|
+
<NodeBadge>Math</NodeBadge>
|
|
91
|
+
</NodeHeader>
|
|
92
|
+
<NodeBody>
|
|
93
|
+
<div className="flex justify-between">
|
|
94
|
+
<NodePorts>
|
|
95
|
+
<NodePort type="number" label="A" direction="input" connected />
|
|
96
|
+
<NodePort type="number" label="B" direction="input" />
|
|
97
|
+
</NodePorts>
|
|
98
|
+
<NodePorts>
|
|
99
|
+
<NodePort type="number" label="Result" direction="output" connected />
|
|
100
|
+
</NodePorts>
|
|
101
|
+
</div>
|
|
102
|
+
</NodeBody>
|
|
103
|
+
</NodeCard>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Node with preview
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<NodeCard>
|
|
110
|
+
<NodeHeader colour="#00b894">
|
|
111
|
+
<NodeTitle>Blur</NodeTitle>
|
|
112
|
+
</NodeHeader>
|
|
113
|
+
<NodeBody>
|
|
114
|
+
<NodePreview>
|
|
115
|
+
<img src={previewUrl} alt="Preview" />
|
|
116
|
+
</NodePreview>
|
|
117
|
+
<NodeDivider />
|
|
118
|
+
<div className="flex justify-between">
|
|
119
|
+
<NodePorts>
|
|
120
|
+
<NodePort type="image" label="Input" direction="input" connected />
|
|
121
|
+
<NodePort type="number" label="Radius" direction="input" />
|
|
122
|
+
</NodePorts>
|
|
123
|
+
<NodePorts>
|
|
124
|
+
<NodePort type="image" label="Output" direction="output" />
|
|
125
|
+
</NodePorts>
|
|
126
|
+
</div>
|
|
127
|
+
</NodeBody>
|
|
128
|
+
</NodeCard>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Architecture recommendations
|
|
134
|
+
|
|
135
|
+
### Node graph store
|
|
136
|
+
|
|
137
|
+
A node editor typically needs a Zustand store with:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
interface NodeGraphState {
|
|
141
|
+
nodes: Record<string, NodeData>
|
|
142
|
+
connections: Connection[]
|
|
143
|
+
selectedNodeIds: Set<string>
|
|
144
|
+
|
|
145
|
+
// CRUD
|
|
146
|
+
addNode: (type: string, position: Point) => void
|
|
147
|
+
updateNode: (id: string, changes: Partial<NodeData>) => void
|
|
148
|
+
removeNodes: (ids: string[]) => void
|
|
149
|
+
|
|
150
|
+
// Connections
|
|
151
|
+
connect: (from: PortRef, to: PortRef) => void
|
|
152
|
+
disconnect: (connectionId: string) => void
|
|
153
|
+
|
|
154
|
+
// History
|
|
155
|
+
pushSnapshot: () => void
|
|
156
|
+
undo: () => void
|
|
157
|
+
redo: () => void
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface NodeData {
|
|
161
|
+
id: string
|
|
162
|
+
type: string
|
|
163
|
+
position: Point
|
|
164
|
+
inputs: PortDefinition[]
|
|
165
|
+
outputs: PortDefinition[]
|
|
166
|
+
properties: Record<string, unknown>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface Connection {
|
|
170
|
+
id: string
|
|
171
|
+
from: PortRef
|
|
172
|
+
to: PortRef
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface PortRef {
|
|
176
|
+
nodeId: string
|
|
177
|
+
portId: string
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Rendering connections
|
|
182
|
+
|
|
183
|
+
Connection wires between ports are typically rendered as SVG `<path>` elements (cubic beziers) in a layer above the nodes. The path control points depend on port direction:
|
|
184
|
+
|
|
185
|
+
- Output ports: bezier exits rightward
|
|
186
|
+
- Input ports: bezier enters from the left
|
|
187
|
+
|
|
188
|
+
Use `useViewport` (substrate-canvas) for the pan/zoom transform that applies to both nodes and wires.
|
|
189
|
+
|
|
190
|
+
### Port type validation
|
|
191
|
+
|
|
192
|
+
When the user drags a wire from an output port to an input port, validate type compatibility:
|
|
193
|
+
|
|
194
|
+
- Same type → allow
|
|
195
|
+
- `any` type → allow
|
|
196
|
+
- Different types → show visual feedback (red wire, shake animation) and reject
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Cross-references
|
|
201
|
+
|
|
202
|
+
| Need | Skill |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| Pan/zoom canvas hosting nodes | **substrate-canvas** |
|
|
205
|
+
| App shell, breadcrumbs for nested groups | **substrate-scaffold** |
|
|
206
|
+
| Property controls for selected node | **substrate-controls** |
|
|
207
|
+
| Selection, undo, keyboard shortcuts | **substrate-interaction** |
|
|
208
|
+
| Command palette for "Add Node" | **substrate-feedback** |
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: substrate-scaffold
|
|
3
|
+
description: "Substrate app shell: toolbar, panels, panes, document tabs, status bar, zoom controls, breadcrumbs, and resizer. Use when building the frame around a surface – layout, navigation, and structural chrome. Triggers on: app shell, layout, toolbar, panel, sidebar, tabs, status bar, zoom, breadcrumbs, split pane, resizer."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Substrate scaffold
|
|
8
|
+
|
|
9
|
+
The scaffold is the structural shell around a Substrate surface – everything that isn't the central creative area itself. It provides toolbar, panels, panes, tabs, status bar, and layout primitives.
|
|
10
|
+
|
|
11
|
+
## When to use this skill
|
|
12
|
+
|
|
13
|
+
Use the scaffold skill when you need to:
|
|
14
|
+
|
|
15
|
+
- Build or modify the app shell around a surface
|
|
16
|
+
- Add toolbar items or customise toolbar behaviour
|
|
17
|
+
- Create or restructure side panels
|
|
18
|
+
- Add property panes inside panels
|
|
19
|
+
- Wire up document tabs, breadcrumbs, or status bar
|
|
20
|
+
- Add resizable split regions
|
|
21
|
+
|
|
22
|
+
For the controls *inside* panes, see **substrate-controls**.
|
|
23
|
+
For the central surface area, see **substrate-surfaces** and **substrate-canvas**.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Components in this domain
|
|
28
|
+
|
|
29
|
+
### Toolbar
|
|
30
|
+
|
|
31
|
+
`registry/substrate/components/toolbar.tsx`
|
|
32
|
+
|
|
33
|
+
Floating bottom bar. Renders tool buttons, separators, and groups. Uses `data-slot="toolbar"` and provides **compaction styling** – children with `data-slot` attributes are automatically resized:
|
|
34
|
+
|
|
35
|
+
| Child slot | Compacted to |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `number-input` | h-6, w-16 |
|
|
38
|
+
| `colour-swatch` | size-6 |
|
|
39
|
+
| `slider` / `slider-track` | h-6 |
|
|
40
|
+
| `history-controls` | gap-0 |
|
|
41
|
+
|
|
42
|
+
**Exports**: `Toolbar`, `ToolbarButton`, `ToolbarSeparator`, `ToolbarGroup`
|
|
43
|
+
|
|
44
|
+
**Typical use**: One per playground, positioned at the bottom. Connect each `ToolbarButton` to `useToolStore` for active-tool state.
|
|
45
|
+
|
|
46
|
+
**Related**: substrate-controls (for inputs inside the toolbar), substrate-interaction (for keyboard shortcuts bound to tools)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### Panel
|
|
51
|
+
|
|
52
|
+
`registry/substrate/components/panel.tsx`
|
|
53
|
+
|
|
54
|
+
Side panel (left or right). Controlled by `usePanelStore`. Renders as a fixed-width column with a header and scrollable body.
|
|
55
|
+
|
|
56
|
+
**Exports**: `Panel`, `PanelHeader`, `PanelBody`
|
|
57
|
+
|
|
58
|
+
**Typical use**: Left panel for structure (layers, tree), right panel for properties (panes with controls). Wire visibility to `usePanelStore`.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### Pane and CollapsiblePane
|
|
63
|
+
|
|
64
|
+
`registry/substrate/components/pane.tsx`
|
|
65
|
+
`registry/substrate/components/collapsible-pane.tsx`
|
|
66
|
+
|
|
67
|
+
Sections within a panel. A pane is a labelled group of controls. CollapsiblePane wraps Radix Collapsible for expand/collapse with animated content.
|
|
68
|
+
|
|
69
|
+
**PaneHeader** is slot-aware – it auto-positions `HistoryControls` via:
|
|
70
|
+
```
|
|
71
|
+
**:data-[slot=history-controls]:ml-auto
|
|
72
|
+
**:data-[slot=history-button]:size-6
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Pane exports**: `Pane`, `PaneHeader`, `PaneLabel`, `PaneAction`
|
|
76
|
+
**CollapsiblePane exports**: `CollapsiblePane`, `CollapsiblePaneHeader`, `CollapsiblePaneLabel`, `CollapsiblePaneContent`
|
|
77
|
+
|
|
78
|
+
**Typical use**: Group related controls (Transform, Fill, Stroke, Typography) inside the right panel.
|
|
79
|
+
|
|
80
|
+
**Related**: substrate-controls (for controls inside panes)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### DocumentTabs
|
|
85
|
+
|
|
86
|
+
`registry/substrate/components/document-tabs.tsx`
|
|
87
|
+
|
|
88
|
+
Tab bar for multiple open documents. Built on Radix Tabs. Close button uses `<span role="button">` to avoid nested-button HTML violation.
|
|
89
|
+
|
|
90
|
+
**Exports**: `DocumentTabs`, `DocumentTabList`, `DocumentTab`, `DocumentTabContent`
|
|
91
|
+
|
|
92
|
+
**Typical use**: Top of the layout, above the surface. Each tab maps to a document/file.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### StatusBar
|
|
97
|
+
|
|
98
|
+
`registry/substrate/components/status-bar.tsx`
|
|
99
|
+
|
|
100
|
+
Thin bottom bar (h-7) for metadata display. Sections align left/centre/right via flexbox auto-margins. Uses `text-[11px]` and `tabular-nums`.
|
|
101
|
+
|
|
102
|
+
**Exports**: `StatusBar`, `StatusBarSection`, `StatusBarItem`, `StatusBarSeparator`
|
|
103
|
+
|
|
104
|
+
**Typical use**: Below the surface. Show cursor position, zoom level, selection count, element type.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### ZoomControls
|
|
109
|
+
|
|
110
|
+
`registry/substrate/components/zoom-controls.tsx`
|
|
111
|
+
|
|
112
|
+
Zoom in/out/fit/reset buttons with percentage display. Click the percentage to reset to 100%.
|
|
113
|
+
|
|
114
|
+
**Exports**: `ZoomControls`, `ZoomButton`
|
|
115
|
+
|
|
116
|
+
**Props**: `zoom`, `onZoomChange`, `onZoomToFit`, `min`, `max`, `step`
|
|
117
|
+
|
|
118
|
+
**Typical use**: Inside the StatusBar or floating over the surface. Wire to `useViewport` from substrate-canvas.
|
|
119
|
+
|
|
120
|
+
**Related**: substrate-canvas (`useViewport` hook)
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### Breadcrumbs
|
|
125
|
+
|
|
126
|
+
`registry/substrate/components/breadcrumbs.tsx`
|
|
127
|
+
|
|
128
|
+
Navigation trail for hierarchical contexts (e.g. Frame → Group → Element). Auto-truncates labels at `max-w-32`.
|
|
129
|
+
|
|
130
|
+
**Exports**: `Breadcrumbs`, `BreadcrumbItem`
|
|
131
|
+
|
|
132
|
+
**Typical use**: Above the surface or in a panel header when navigating into nested frames/groups.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### Resizer
|
|
137
|
+
|
|
138
|
+
`registry/substrate/components/resizer.tsx`
|
|
139
|
+
|
|
140
|
+
Draggable divider between regions. 1px visual line with a wider hit area. Three grab dots appear on hover.
|
|
141
|
+
|
|
142
|
+
**Exports**: `Resizer`
|
|
143
|
+
|
|
144
|
+
**Props**: `orientation` (`"vertical"` | `"horizontal"`)
|
|
145
|
+
|
|
146
|
+
**Typical use**: Between the panel and the surface, or between two panes in a split layout.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Recommended combinations
|
|
151
|
+
|
|
152
|
+
### Standard creative app shell
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Toolbar + Panel (left + right) + StatusBar + DocumentTabs
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Panels contain Panes/CollapsiblePanes. StatusBar contains ZoomControls. Toolbar is connected to useToolStore.
|
|
159
|
+
|
|
160
|
+
### Minimal single-document tool
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Toolbar + Panel (right only) + StatusBar
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
No tabs, no left panel. Good for focused single-surface tools like an image editor or easing curve designer.
|
|
167
|
+
|
|
168
|
+
### Node editor shell
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Toolbar + Panel (right) + StatusBar + Breadcrumbs
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Breadcrumbs show the path into nested node groups. Right panel shows properties of the selected node. See **substrate-nodes** for node components.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Layout pattern
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<div className="flex h-screen flex-col">
|
|
182
|
+
<DocumentTabList>{/* tabs */}</DocumentTabList>
|
|
183
|
+
<div className="flex flex-1 overflow-hidden">
|
|
184
|
+
<Panel side="left">{/* layers/tree */}</Panel>
|
|
185
|
+
<div className="relative flex-1">
|
|
186
|
+
<Surface />{/* central area */}
|
|
187
|
+
</div>
|
|
188
|
+
<Panel side="right">{/* property panes */}</Panel>
|
|
189
|
+
</div>
|
|
190
|
+
<StatusBar>{/* metadata */}</StatusBar>
|
|
191
|
+
<Toolbar>{/* tools */}</Toolbar>
|
|
192
|
+
</div>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Cross-references
|
|
198
|
+
|
|
199
|
+
| Need | Skill |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| Controls inside panes (sliders, inputs, selects) | **substrate-controls** |
|
|
202
|
+
| The central rendering surface | **substrate-surfaces** |
|
|
203
|
+
| Spatial hooks (viewport, snap, bounds) | **substrate-canvas** |
|
|
204
|
+
| Selection, undo, clipboard, shortcuts | **substrate-interaction** |
|
|
205
|
+
| Toasts, progress, empty states | **substrate-feedback** |
|
|
206
|
+
| Node cards and port wiring | **substrate-nodes** |
|