create-substrate 0.1.5 → 0.2.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -4
  3. package/dist/index.js +25 -7
  4. package/dist/index.js.map +1 -1
  5. package/dist/prompts.d.ts +0 -2
  6. package/dist/prompts.d.ts.map +1 -1
  7. package/dist/prompts.js +6 -50
  8. package/dist/prompts.js.map +1 -1
  9. package/dist/scaffold.d.ts.map +1 -1
  10. package/dist/scaffold.js +93 -66
  11. package/dist/scaffold.js.map +1 -1
  12. package/dist/surfaces/3d-scene.d.ts.map +1 -1
  13. package/dist/surfaces/3d-scene.js +43 -74
  14. package/dist/surfaces/3d-scene.js.map +1 -1
  15. package/dist/surfaces/canvas-2d.d.ts.map +1 -1
  16. package/dist/surfaces/canvas-2d.js +7 -10
  17. package/dist/surfaces/canvas-2d.js.map +1 -1
  18. package/dist/surfaces/data-vis.d.ts.map +1 -1
  19. package/dist/surfaces/data-vis.js +37 -143
  20. package/dist/surfaces/data-vis.js.map +1 -1
  21. package/dist/surfaces/image-gen.d.ts.map +1 -1
  22. package/dist/surfaces/image-gen.js +15 -35
  23. package/dist/surfaces/image-gen.js.map +1 -1
  24. package/dist/surfaces/node-editor.js +1 -1
  25. package/dist/utils/detect-pm.d.ts +2 -0
  26. package/dist/utils/detect-pm.d.ts.map +1 -1
  27. package/dist/utils/detect-pm.js +9 -0
  28. package/dist/utils/detect-pm.js.map +1 -1
  29. package/dist/utils/package-info.d.ts +5 -0
  30. package/dist/utils/package-info.d.ts.map +1 -0
  31. package/dist/utils/package-info.js +17 -0
  32. package/dist/utils/package-info.js.map +1 -0
  33. package/package.json +4 -3
  34. package/skills/3d-scene/SKILL.md +2 -1
  35. package/skills/animation/SKILL.md +2 -1
  36. package/skills/canvas-2d/SKILL.md +67 -5
  37. package/skills/composing-panels/SKILL.md +1 -4
  38. package/skills/data-visualisation/SKILL.md +2 -1
  39. package/skills/image-generation/SKILL.md +2 -1
  40. package/skills/install-components/SKILL.md +22 -16
  41. package/skills/scaffold-playground/SKILL.md +39 -19
  42. package/skills/substrate-canvas/SKILL.md +5 -0
  43. package/skills/substrate-controls/SKILL.md +57 -2
  44. package/skills/substrate-interaction/SKILL.md +19 -10
  45. package/skills/substrate-scaffold/SKILL.md +4 -4
  46. package/skills/theming/SKILL.md +8 -2
  47. package/skills/wire-interactions/SKILL.md +6 -3
  48. package/templates/agents-section.md +41 -0
@@ -45,7 +45,7 @@ function handleMove(id: string, x: number, y: number) {
45
45
  }
46
46
  ```
47
47
 
48
- **Connects to**: `HistoryControls` component, `NumberInput.onStart` (push snapshot before scrub), `createKeyboardShortcuts` (⌘Z / ⌘⇧Z).
48
+ **Connects to**: `NumberInput.onStart` (push snapshot before scrub), `createKeyboardShortcuts` (⌘Z / ⌘⇧Z). The 2D canvas does not use this hook: `useDocumentStore` has its own history (`pushSnapshot`, `undo`, `redo`) that covers elements and their order, not the camera, so pan and zoom survive an undo. Call `pushSnapshot()` before every mutation you want to be undoable, including property edits.
49
49
 
50
50
  ---
51
51
 
@@ -139,7 +139,7 @@ formatShortcut({ key: "z", meta: true, shift: true }) // → "⌘+Shift+Z"
139
139
 
140
140
  Undo/redo button pair. Renders `data-slot="history-controls"` for auto-positioning in PaneHeader and Toolbar.
141
141
 
142
- **Props**: `canUndo`, `canRedo`, `onUndo`, `onRedo`
142
+ **Props**: `size` (`"sm"` or `"md"`). It takes no undo callbacks: it reads `past`/`future` and calls `undo`/`redo` on `useDocumentStore`, so it works with the canvas store out of the box. For any other undo stack (`useUndoable`), build your own pair of buttons from `canUndo`/`undo`/`canRedo`/`redo`.
143
143
 
144
144
  **Typical placement**:
145
145
  - Inside `PaneHeader` – auto-positioned to the right via `**:data-[slot=history-controls]:ml-auto`
@@ -157,7 +157,9 @@ Flat layer panel with selection, visibility, lock, rename, and reorder.
157
157
 
158
158
  **Features**:
159
159
  - `LayerItem` supports `selected`, `hovered`, `depth` props
160
- - `LayerName` enables double-click inline rename
160
+ - `LayerName` takes the name as `children` and enables double-click inline rename via `onRename`
161
+ - `LayerVisibilityToggle` and `LayerLockToggle` are controlled: `visible`/`onVisibleChange` and `locked`/`onLockedChange`
162
+ - Sorting is not built in: spread `useDragReorder`'s `getDragItemProps` on each `LayerItem`
161
163
  - `LayerActions` are hidden by default, visible on row hover
162
164
  - `LayerVisibilityToggle` and `LayerLockToggle` use eye/lock icons
163
165
 
@@ -199,8 +201,8 @@ Hierarchical tree with expand/collapse, depth indentation, and icons.
199
201
  │ → apply to store │
200
202
  ├─────────────────────────────────────────────────┤
201
203
  │ HistoryControls (in PaneHeader or Toolbar) │
202
- │ → reads canUndo/canRedo │
203
- │ → calls onUndo/onRedo │
204
+ │ → reads past/future from useDocumentStore │
205
+ │ → calls the store's undo/redo │
204
206
  ├─────────────────────────────────────────────────┤
205
207
  │ NumberInput.onStart │
206
208
  │ → pushSnapshot before scrub begins │
@@ -231,7 +233,14 @@ Hierarchical tree with expand/collapse, depth indentation, and icons.
231
233
  ```tsx
232
234
  function LayerPanel() {
233
235
  const { selectedIds, select, isSelected } = useSelection({ items: elementIds })
234
- const { getDragItemProps } = useDragReorder({ items: elements, onReorder: reorderElements })
236
+ const setElementOrder = useDocumentStore((s) => s.setElementOrder)
237
+ const { getDragItemProps } = useDragReorder({
238
+ items: elements,
239
+ onReorder: (next) => {
240
+ pushSnapshot()
241
+ setElementOrder(next.map((el) => el.id))
242
+ },
243
+ })
235
244
 
236
245
  return (
237
246
  <LayerList>
@@ -244,21 +253,21 @@ function LayerPanel() {
244
253
  {...getDragItemProps(i)}
245
254
  >
246
255
  <LayerIcon colour={el.fill?.color} />
247
- <LayerName value={el.name} onRename={(name) => updateElement(el.id, { name })} />
256
+ <LayerName onRename={(name) => updateElement(el.id, { name })}>{el.name}</LayerName>
248
257
  <LayerActions>
249
258
  <LayerVisibilityToggle
250
259
  visible={el.visible}
251
- onToggle={() => updateElement(el.id, { visible: !el.visible })}
260
+ onVisibleChange={(visible) => updateElement(el.id, { visible })}
252
261
  />
253
262
  <LayerLockToggle
254
263
  locked={el.locked}
255
- onToggle={() => updateElement(el.id, { locked: !el.locked })}
264
+ onLockedChange={(locked) => updateElement(el.id, { locked })}
256
265
  />
257
266
  </LayerActions>
258
267
  </LayerItem>
259
268
  </ContextMenuTrigger>
260
269
  <ContextMenuContent>
261
- <ContextMenuItem onSelect={() => duplicateElement(el.id)}>
270
+ <ContextMenuItem onSelect={() => duplicateElements([el.id])}>
262
271
  Duplicate <ContextMenuShortcut>⌘D</ContextMenuShortcut>
263
272
  </ContextMenuItem>
264
273
  <ContextMenuItem onSelect={() => deleteElement(el.id)}>
@@ -39,9 +39,9 @@ Floating bottom bar. Renders tool buttons, separators, and groups. Uses `data-sl
39
39
  | `slider` / `slider-track` | h-6 |
40
40
  | `history-controls` | gap-0 |
41
41
 
42
- **Exports**: `Toolbar`, `ToolbarButton`, `ToolbarSeparator`, `ToolbarGroup`
42
+ **Exports**: `Toolbar`, `ToolbarToggleGroup`, `ToolbarToggle`, `ToolbarSeparator`, `ToolbarDropdown`
43
43
 
44
- **Typical use**: One per playground, positioned at the bottom. Connect each `ToolbarButton` to `useToolStore` for active-tool state.
44
+ **Typical use**: One per playground, positioned at the bottom. Put the tool buttons in a `ToolbarToggleGroup` of `ToolbarToggle`s and connect it to `useToolStore` for active-tool state.
45
45
 
46
46
  **Related**: substrate-controls (for inputs inside the toolbar), substrate-interaction (for keyboard shortcuts bound to tools)
47
47
 
@@ -181,11 +181,11 @@ Breadcrumbs show the path into nested node groups. Right panel shows properties
181
181
  <div className="flex h-screen flex-col">
182
182
  <DocumentTabList>{/* tabs */}</DocumentTabList>
183
183
  <div className="flex flex-1 overflow-hidden">
184
- <Panel side="left">{/* layers/tree */}</Panel>
184
+ <Panel position="left">{/* layers/tree */}</Panel>
185
185
  <div className="relative flex-1">
186
186
  <Surface />{/* central area */}
187
187
  </div>
188
- <Panel side="right">{/* property panes */}</Panel>
188
+ <Panel position="right">{/* property panes */}</Panel>
189
189
  </div>
190
190
  <StatusBar>{/* metadata */}</StatusBar>
191
191
  <Toolbar>{/* tools */}</Toolbar>
@@ -113,9 +113,15 @@ const CURSOR_MAP: Record<string, string> = {
113
113
  }
114
114
  ```
115
115
 
116
- ## 5. Dark mode
116
+ ## 5. Light and dark mode
117
117
 
118
- Substrate UI components automatically respect dark mode when using `next-themes` or a `.dark` class on the document. The canvas renderer needs manual dark mode handling — check a CSS variable or a store flag and swap your colour constants accordingly.
118
+ Every Substrate component works in both themes. Colours come from tokens, and the theme is chosen by a `.dark` class on `<html>`: light values live in `:root`, dark values in `.dark`.
119
+
120
+ Projects made with `create-substrate` follow the system theme through a small script in `index.html`. To force a theme, remove that script and either set `class="dark"` on `<html>` or leave it off. To add a switch, toggle that class yourself (or use `next-themes` in a Next.js app).
121
+
122
+ Canvas-drawn parts follow the theme too: the design canvas reads `--canvas` and `--canvas-foreground`, the easing curve editor draws in its text colour, and both repaint when the theme changes. In your own canvas code, read colours with `getComputedStyle` at draw time instead of hardcoding them, and use mid-tones for defaults that must read on both backgrounds.
123
+
124
+ When writing a component, use tokens (`bg-panel`, `text-muted-foreground`, `bg-foreground/10`) and check it in both themes. Avoid `bg-background` for a control's track or well: it matches the page in light mode and disappears.
119
125
 
120
126
  ## Related reference skills
121
127
 
@@ -39,6 +39,7 @@ Keyboard events (DOM)
39
39
  | `creating` | Drag with shape tool on empty canvas | Preview element dimensions | Commit new element to store |
40
40
  | `moving` | Drag on selected element | Update element positions | Commit positions |
41
41
  | `resizing` | Drag on resize handle | Update element bounds | Commit bounds |
42
+ | `rotating` | Drag on the rotate handle above a single selection | Turn the element about its centre (hold Shift to snap to 15°) | Commit rotation |
42
43
  | `marquee_selecting` | Drag on empty canvas with select tool | Draw selection rectangle | Select contained elements |
43
44
 
44
45
  ### Pointer down logic
@@ -66,7 +67,8 @@ Keyboard events (DOM)
66
67
  - "pan": update camera
67
68
  - "create": preview element (stored on drag ref, rendered by renderer)
68
69
  - "move": update element positions (delta from start)
69
- - "resize": update element bounds respecting handle position
70
+ - "resize": update element bounds with `resizeBounds`, which respects the handle and the element's rotation
71
+ - "rotate": set `rotation` from the pointer's angle around the element centre
70
72
  - "marquee": update selection rectangle
71
73
  5. Call markDirty() to trigger repaint
72
74
  ```
@@ -77,7 +79,7 @@ Keyboard events (DOM)
77
79
  1. End drag (useCanvasDrag.endDrag)
78
80
  2. Based on drag type:
79
81
  - "create": push snapshot, add element to store, select it, reset tool to select
80
- - "move"/"resize": push snapshot (positions already updated during drag)
82
+ - "move"/"resize"/"rotate": nothing to push; the snapshot was taken when the drag first moved, so a click without a drag leaves no undo step
81
83
  - "marquee": select all elements within the marquee bounds
82
84
  - "pan": nothing to commit
83
85
  ```
@@ -104,12 +106,13 @@ Always convert pointer event coordinates to world space before doing hit-testing
104
106
 
105
107
  ```ts
106
108
  interface DragInfo {
107
- type: DragType // "create" | "move" | "resize" | "marquee" | "pan"
109
+ type: DragType // "create" | "move" | "resize" | "rotate" | "marquee" | "pan"
108
110
  startWorld: Point // Where the drag began (world space)
109
111
  startScreen: Point // Where the drag began (screen space)
110
112
  currentWorld: Point // Current position (updated on every move)
111
113
  currentScreen: Point
112
114
  handle?: HandlePosition // Which resize handle, if resizing
115
+ rotate?: { id, center, startAngle, startRotation } // Set for a rotate drag
113
116
  elementStartPositions?: Record<string, { x, y, width, height }> // Snapshot for move/resize
114
117
  }
115
118
  ```
@@ -0,0 +1,41 @@
1
+ # Substrate
2
+
3
+ This project uses [Substrate](https://substrate.georgedrury.co.uk), a set of primitives for building creative tools: scaffold (toolbar, panels, panes, shortcuts), canvas and node helpers, and config-driven controls.
4
+
5
+ ## Use Substrate components first
6
+
7
+ Components are installed into the project with the Substrate CLI, which wraps shadcn. Never run `npx shadcn@latest add` for them directly.
8
+
9
+ ```bash
10
+ npx substrate-kit add panel slider toolbar # install by name, in one call
11
+ npx substrate-kit add # interactive picker
12
+ npx substrate-kit docs <component> # props, CSS variables, usage
13
+ npx substrate-kit docs <component> --open # open its docs page in the browser
14
+ npx substrate-kit docs --list # every component
15
+ ```
16
+
17
+ Before writing a custom slider, input, panel, timeline or colour picker, check `docs --list`. If the Substrate MCP server is connected, use `search_components`, `get_component` and `get_docs` instead of the commands above, and `install_components` to install.
18
+
19
+ ## Tune values with Controls
20
+
21
+ For parameters you want to adjust live (sizes, colours, springs, toggles), describe them as an object and render them with `Controls` instead of building a form:
22
+
23
+ ```tsx
24
+ const card = useControls("Card", {
25
+ radius: [16, 0, 48], // [default, min, max, step?] slider
26
+ colour: "#a78bfa", // colour picker
27
+ visible: true, // toggle
28
+ motion: { type: "spring", visualDuration: 0.3, bounce: 0.2 },
29
+ })
30
+ // <Panel><Controls /></Panel> renders every registered panel
31
+ ```
32
+
33
+ The returned values keep the shape of the config. Panels have versions, reset, keyboard shortcuts (`shortcuts` option) and a copy button that gives an agent the tuned values to paste back in as the new defaults. Install with `npx substrate-kit add controls`, and read `docs controls` for every control type and option. Controls and the timeline are built on [DialKit](https://github.com/joshpuckett/dialkit) (MIT, by Josh Puckett), which installs as the `dialkit` dependency.
34
+
35
+ ## Tune animation with the timeline
36
+
37
+ For motion, define clips with `useControlsTimeline` and bind `clip.current` to your UI. `ControlsTimeline` is the editor: scrub, move and resize clips, edit curves, and copy the tuned timing back as defaults. Install with `npx substrate-kit add controls-timeline`, and read `docs controls-timeline` for clip types, looping and zoom.
38
+
39
+ ## Skills
40
+
41
+ Substrate skills are in `.claude/skills/` and `.agents/skills/`. Surface guides explain how to build each kind of tool (`scaffold-playground`, `canvas-2d`, `3d-scene`, `animation`, `image-generation`, `data-visualisation`) and workflows (`composing-panels`, `wire-interactions`, `create-custom-tool`, `theming`). Component references (`substrate-scaffold`, `substrate-controls`, `substrate-canvas`, `substrate-nodes`, `substrate-feedback`, `substrate-interaction`) say what to use. `install-components` lists everything available.