@substrate-kit/cli 0.1.1 → 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 (146) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/LICENSE +21 -0
  3. package/README.md +25 -8
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +10 -60
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/add.d.ts +1 -0
  8. package/dist/commands/add.d.ts.map +1 -1
  9. package/dist/commands/add.js +51 -106
  10. package/dist/commands/add.js.map +1 -1
  11. package/dist/commands/docs.d.ts +4 -2
  12. package/dist/commands/docs.d.ts.map +1 -1
  13. package/dist/commands/docs.js +33 -54
  14. package/dist/commands/docs.js.map +1 -1
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +158 -122
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/index.js +0 -0
  19. package/dist/mcp/server.d.ts.map +1 -1
  20. package/dist/mcp/server.js +8 -7
  21. package/dist/mcp/server.js.map +1 -1
  22. package/dist/mcp/tools/get-component.d.ts +1 -1
  23. package/dist/mcp/tools/get-component.d.ts.map +1 -1
  24. package/dist/mcp/tools/get-component.js +10 -6
  25. package/dist/mcp/tools/get-component.js.map +1 -1
  26. package/dist/mcp/tools/get-docs.d.ts +17 -0
  27. package/dist/mcp/tools/get-docs.d.ts.map +1 -0
  28. package/dist/mcp/tools/get-docs.js +23 -0
  29. package/dist/mcp/tools/get-docs.js.map +1 -0
  30. package/dist/mcp/tools/install.d.ts +2 -1
  31. package/dist/mcp/tools/install.d.ts.map +1 -1
  32. package/dist/mcp/tools/install.js +19 -30
  33. package/dist/mcp/tools/install.js.map +1 -1
  34. package/dist/mcp/tools/search.d.ts +1 -1
  35. package/dist/mcp/tools/search.d.ts.map +1 -1
  36. package/dist/mcp/tools/search.js +2 -4
  37. package/dist/mcp/tools/search.js.map +1 -1
  38. package/dist/{mcp/types.d.ts → registry.d.ts} +6 -1
  39. package/dist/registry.d.ts.map +1 -0
  40. package/dist/registry.js +48 -0
  41. package/dist/registry.js.map +1 -0
  42. package/dist/utils/agents-file.d.ts +4 -0
  43. package/dist/utils/agents-file.d.ts.map +1 -0
  44. package/dist/utils/agents-file.js +36 -0
  45. package/dist/utils/agents-file.js.map +1 -0
  46. package/dist/utils/docs.d.ts +7 -0
  47. package/dist/utils/docs.d.ts.map +1 -0
  48. package/dist/utils/docs.js +28 -0
  49. package/dist/utils/docs.js.map +1 -0
  50. package/dist/utils/shadcn.d.ts +12 -0
  51. package/dist/utils/shadcn.d.ts.map +1 -0
  52. package/dist/utils/shadcn.js +29 -0
  53. package/dist/utils/shadcn.js.map +1 -0
  54. package/dist/utils/version.d.ts +3 -0
  55. package/dist/utils/version.d.ts.map +1 -0
  56. package/dist/utils/version.js +8 -0
  57. package/dist/utils/version.js.map +1 -0
  58. package/docs/components/action.mdx +30 -0
  59. package/docs/components/alignment-controls.mdx +30 -0
  60. package/docs/components/badge.mdx +22 -0
  61. package/docs/components/breadcrumbs.mdx +25 -0
  62. package/docs/components/collapsible-pane.mdx +35 -0
  63. package/docs/components/colour-picker.mdx +66 -0
  64. package/docs/components/command-palette.mdx +36 -0
  65. package/docs/components/context-menu.mdx +31 -0
  66. package/docs/components/controls-timeline.mdx +107 -0
  67. package/docs/components/controls.mdx +168 -0
  68. package/docs/components/design-canvas.mdx +51 -0
  69. package/docs/components/document-tabs.mdx +25 -0
  70. package/docs/components/easing-curve-editor.mdx +85 -0
  71. package/docs/components/empty-state.mdx +26 -0
  72. package/docs/components/gallery.mdx +60 -0
  73. package/docs/components/history-controls.mdx +47 -0
  74. package/docs/components/image-picker.mdx +50 -0
  75. package/docs/components/layer-list.mdx +36 -0
  76. package/docs/components/node.mdx +85 -0
  77. package/docs/components/number-input.mdx +44 -0
  78. package/docs/components/number-slider.mdx +32 -0
  79. package/docs/components/pane.mdx +39 -0
  80. package/docs/components/panel.mdx +28 -0
  81. package/docs/components/popover.mdx +32 -0
  82. package/docs/components/progress-bar.mdx +24 -0
  83. package/docs/components/resizer.mdx +24 -0
  84. package/docs/components/select.mdx +31 -0
  85. package/docs/components/shortcut-overlay.mdx +43 -0
  86. package/docs/components/slider.mdx +20 -0
  87. package/docs/components/spring-editor.mdx +85 -0
  88. package/docs/components/status-bar.mdx +42 -0
  89. package/docs/components/surface.mdx +47 -0
  90. package/docs/components/swatch-palette.mdx +36 -0
  91. package/docs/components/text-area.mdx +26 -0
  92. package/docs/components/timeline.mdx +96 -0
  93. package/docs/components/toast.mdx +42 -0
  94. package/docs/components/toggle-group.mdx +40 -0
  95. package/docs/components/toggle.mdx +27 -0
  96. package/docs/components/toolbar.mdx +24 -0
  97. package/docs/components/tree-list.mdx +46 -0
  98. package/docs/components/xy-pad.mdx +49 -0
  99. package/docs/hooks/use-clipboard.mdx +70 -0
  100. package/docs/hooks/use-drag-reorder.mdx +60 -0
  101. package/docs/hooks/use-selection.mdx +68 -0
  102. package/docs/hooks/use-undoable.mdx +83 -0
  103. package/docs/hooks/use-viewport.mdx +75 -0
  104. package/docs/utilities/bounding-box.mdx +76 -0
  105. package/docs/utilities/keyboard-shortcuts.mdx +64 -0
  106. package/docs/utilities/snap.mdx +73 -0
  107. package/package.json +6 -3
  108. package/registry.json +331 -16
  109. package/skills/3d-scene/SKILL.md +2 -1
  110. package/skills/animation/SKILL.md +2 -1
  111. package/skills/canvas-2d/SKILL.md +68 -6
  112. package/skills/composing-panels/SKILL.md +9 -12
  113. package/skills/data-visualisation/SKILL.md +2 -1
  114. package/skills/image-generation/SKILL.md +2 -1
  115. package/skills/install-components/SKILL.md +22 -16
  116. package/skills/scaffold-playground/SKILL.md +42 -22
  117. package/skills/substrate-canvas/SKILL.md +5 -0
  118. package/skills/substrate-controls/SKILL.md +57 -2
  119. package/skills/substrate-interaction/SKILL.md +19 -10
  120. package/skills/substrate-scaffold/SKILL.md +4 -4
  121. package/skills/theming/SKILL.md +8 -2
  122. package/skills/wire-interactions/SKILL.md +6 -3
  123. package/templates/agents-section.md +41 -0
  124. package/dist/commands/browse.d.ts +0 -2
  125. package/dist/commands/browse.d.ts.map +0 -1
  126. package/dist/commands/browse.js +0 -25
  127. package/dist/commands/browse.js.map +0 -1
  128. package/dist/commands/create.d.ts +0 -9
  129. package/dist/commands/create.d.ts.map +0 -1
  130. package/dist/commands/create.js +0 -415
  131. package/dist/commands/create.js.map +0 -1
  132. package/dist/commands/variables.d.ts +0 -4
  133. package/dist/commands/variables.d.ts.map +0 -1
  134. package/dist/commands/variables.js +0 -111
  135. package/dist/commands/variables.js.map +0 -1
  136. package/dist/mcp/registry.d.ts +0 -3
  137. package/dist/mcp/registry.d.ts.map +0 -1
  138. package/dist/mcp/registry.js +0 -43
  139. package/dist/mcp/registry.js.map +0 -1
  140. package/dist/mcp/tools/theme.d.ts +0 -10
  141. package/dist/mcp/tools/theme.d.ts.map +0 -1
  142. package/dist/mcp/tools/theme.js +0 -37
  143. package/dist/mcp/tools/theme.js.map +0 -1
  144. package/dist/mcp/types.d.ts.map +0 -1
  145. package/dist/mcp/types.js +0 -2
  146. package/dist/mcp/types.js.map +0 -1
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: Text area
3
+ description: Auto-resizing textarea with minimal styling. Fits inside node bodies and property panels.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/text-area
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### Default
15
+
16
+ <ComponentPreview name="text-area-demo" />
17
+
18
+ ## Props
19
+
20
+ Extends all native `<textarea>` props.
21
+
22
+ | Prop | Type | Default | Description |
23
+ | ------------ | --------- | ------- | --------------------------------------- |
24
+ | `autoResize` | `boolean` | `true` | Automatically adjusts height to content |
25
+ | `minRows` | `number` | `2` | Minimum visible rows |
26
+ | `maxRows` | `number` | `10` | Maximum height before scrolling |
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Timeline
3
+ description: Compound timeline for keyframe animation editing with playhead, tracks, and transport controls.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/timeline
10
+ ```
11
+
12
+ This also installs the `animation-store` dependency (Zustand).
13
+
14
+ ## Examples
15
+
16
+ <ComponentPreview name="timeline-demo" />
17
+
18
+ ## Theme variables
19
+
20
+ | Variable | Tailwind class | Fallback | Description |
21
+ | --------------------- | ---------------------- | -------------------------- | ----------------------- |
22
+ | `--timeline-playhead` | `bg-timeline-playhead` | `--color-destructive` | Playhead colour |
23
+ | `--timeline-keyframe` | `bg-timeline-keyframe` | `--color-muted-foreground` | Keyframe diamond colour |
24
+ | `--timeline-track` | `bg-timeline-track` | `--color-panel` | Track background |
25
+
26
+ ## Components
27
+
28
+ | Component | Description |
29
+ | ------------------- | --------------------------------------------------------------------------------- |
30
+ | `Timeline` | Flex-column container with panel background and top border |
31
+ | `TimelineRuler` | Second-interval tick marks with time labels. Fixed label column at 128px width |
32
+ | `TimelinePlayhead` | Draggable vertical line with triangular handle. Updates `animation-store` on drag |
33
+ | `TimelineTrack` | Single track row with a label column and a content area for keyframes |
34
+ | `TimelineKeyframe` | Diamond-shaped marker positioned by `time * zoomLevel` pixels |
35
+ | `TimelineTransport` | Play/pause, loop, and time display. Reads directly from `animation-store` |
36
+
37
+ ## Props
38
+
39
+ ### Timeline
40
+
41
+ | Prop | Type | Default | Description |
42
+ | ---------- | -------- | ------- | ----------------------------------------------------- |
43
+ | `duration` | `number` | `5` | Total duration (unused internally — pass to children) |
44
+
45
+ ### TimelineRuler
46
+
47
+ | Prop | Type | Default | Description |
48
+ | ----------- | -------- | ------- | ------------------------- |
49
+ | `duration` | `number` | — | Total duration in seconds |
50
+ | `zoomLevel` | `number` | — | Pixels per second |
51
+
52
+ ### TimelinePlayhead
53
+
54
+ | Prop | Type | Default | Description |
55
+ | ----------- | -------- | ------- | ------------------------- |
56
+ | `time` | `number` | — | Current time in seconds |
57
+ | `duration` | `number` | — | Total duration in seconds |
58
+ | `zoomLevel` | `number` | — | Pixels per second |
59
+
60
+ ### TimelineKeyframe
61
+
62
+ | Prop | Type | Default | Description |
63
+ | ----------- | ------------ | ------- | ---------------------------- |
64
+ | `time` | `number` | — | Keyframe time in seconds |
65
+ | `duration` | `number` | — | Total duration in seconds |
66
+ | `zoomLevel` | `number` | — | Pixels per second |
67
+ | `selected` | `boolean` | `false` | Whether keyframe is selected |
68
+ | `onSelect` | `() => void` | — | Click handler for selection |
69
+
70
+ ### TimelineTransport
71
+
72
+ No additional props. Reads state from `useAnimationStore`.
73
+
74
+ ## Animation store
75
+
76
+ The timeline uses a shared Zustand store (`animation-store`) for playback state:
77
+
78
+ ```tsx
79
+ import { useAnimationStore } from "@/stores/substrate/animation-store"
80
+
81
+ const { currentTime, isPlaying, togglePlayback } = useAnimationStore()
82
+ ```
83
+
84
+ | Property | Type | Description |
85
+ | ------------------------ | ------------------------------------------ | --------------------------- |
86
+ | `duration` | `number` | Total duration (default 5s) |
87
+ | `currentTime` | `number` | Current playhead time |
88
+ | `isPlaying` | `boolean` | Playback state |
89
+ | `fps` | `number` | Frames per second (60) |
90
+ | `zoomLevel` | `number` | Pixels per second (100) |
91
+ | `selectedKeyframeIds` | `Set<string>` | Selected keyframe IDs |
92
+ | `setCurrentTime` | `(time: number) => void` | Set time (clamped) |
93
+ | `togglePlayback` | `() => void` | Toggle play/pause |
94
+ | `setZoomLevel` | `(level: number) => void` | Set zoom (clamped 20–500) |
95
+ | `selectKeyframe` | `(id: string, additive?: boolean) => void` | Select keyframe |
96
+ | `clearKeyframeSelection` | `() => void` | Clear selection |
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: Toast
3
+ description: Lightweight notifications for creative app actions – export, delete, copy, save.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ Requires the `sonner` package and the `Toaster` component mounted in your layout (already included in the Substrate CLI scaffold).
9
+
10
+ ```bash
11
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/toast
12
+ ```
13
+
14
+ ## Examples
15
+
16
+ ### Default
17
+
18
+ <ComponentPreview name="toast-demo" />
19
+
20
+ ## API
21
+
22
+ ```ts
23
+ import { toast } from "@/components/substrate/toast"
24
+
25
+ toast("3 elements duplicated")
26
+ toast.success("Exported to PNG")
27
+ toast.error("Failed to save", { description: "Network unavailable" })
28
+ toast.warning("Unsaved changes")
29
+ toast.info("Tip: hold Shift for larger increments")
30
+ toast.undo("Deleted 2 elements", () => undo())
31
+ toast.dismiss()
32
+ ```
33
+
34
+ | Method | Default duration | Description |
35
+ | --------------- | ---------------- | ---------------------------------- |
36
+ | `toast()` | 3s | Neutral notification |
37
+ | `toast.success` | 3s | Success with checkmark icon |
38
+ | `toast.error` | 5s | Error with longer display time |
39
+ | `toast.warning` | 4s | Warning |
40
+ | `toast.info` | 3s | Informational |
41
+ | `toast.undo` | 5s | Message with an Undo action button |
42
+ | `toast.dismiss` | — | Dismiss all toasts |
@@ -0,0 +1,40 @@
1
+ ---
2
+ title: Toggle Group
3
+ description: Grouped set of toggles for single or multi-select choices. Built on Radix ToggleGroup.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/toggle-group
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### Default
15
+
16
+ <ComponentPreview name="toggle-group-demo" />
17
+
18
+ ## Props
19
+
20
+ ### ToggleGroup
21
+
22
+ Extends all Radix `ToggleGroup.Root` props.
23
+
24
+ | Prop | Type | Default | Description |
25
+ | --------------- | ---------------------------------------------------- | -------------- | ---------------------------------------------- |
26
+ | `type` | `"single" \| "multiple"` | — | Single or multi-select behaviour |
27
+ | `value` | `string \| string[]` | — | Controlled selected value(s) |
28
+ | `defaultValue` | `string \| string[]` | — | Uncontrolled initial value(s) |
29
+ | `onValueChange` | `(value: string) => void \| (value: string[]) => void` | — | Called when selection changes |
30
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction and arrow-key navigation axis |
31
+ | `disabled` | `boolean` | — | Disables all items |
32
+
33
+ ### ToggleGroupItem
34
+
35
+ Extends all Radix `ToggleGroup.Item` props.
36
+
37
+ | Prop | Type | Default | Description |
38
+ | ---------- | --------- | ------- | ---------------------- |
39
+ | `value` | `string` | — | Unique value of item |
40
+ | `disabled` | `boolean` | — | Disables this item |
@@ -0,0 +1,27 @@
1
+ ---
2
+ title: Toggle
3
+ description: Pressable boolean toggle button built on Radix Toggle. Use for snap, grid, rulers, and other on/off states.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/toggle
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### Default
15
+
16
+ <ComponentPreview name="toggle-demo" />
17
+
18
+ ## Props
19
+
20
+ Extends all Radix `Toggle.Root` props.
21
+
22
+ | Prop | Type | Default | Description |
23
+ | ----------------- | ---------------------------- | ------- | --------------------------------- |
24
+ | `pressed` | `boolean` | — | Controlled pressed state |
25
+ | `onPressedChange` | `(pressed: boolean) => void` | — | Called when pressed state changes |
26
+ | `defaultPressed` | `boolean` | — | Uncontrolled initial state |
27
+ | `disabled` | `boolean` | — | Disables the toggle |
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: Toolbar
3
+ description: Floating toolbar with toggle groups, dropdowns, and tooltips built on Radix primitives.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/toolbar
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ <ComponentPreview name="toolbar-demo" />
15
+
16
+ ## Components
17
+
18
+ | Component | Description |
19
+ | -------------------- | ----------------------------------------------------------------------------- |
20
+ | `Toolbar` | Fixed-position container with `role="toolbar"` and built-in `TooltipProvider` |
21
+ | `ToolbarToggleGroup` | Single-select toggle group (wraps Radix ToggleGroup) |
22
+ | `ToolbarToggle` | Toggle button with optional tooltip and keyboard shortcut display |
23
+ | `ToolbarSeparator` | Vertical divider between tool groups |
24
+ | `ToolbarDropdown` | Button with dropdown menu for tool variant selection, active state highlight |
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: Tree list
3
+ description: Hierarchical list with expand/collapse, selection, and depth indentation. Ideal for layer panels with nested frames.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/tree-list
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### Default
15
+
16
+ <ComponentPreview name="tree-list-demo" />
17
+
18
+ ## Components
19
+
20
+ | Component | Description |
21
+ | ------------------ | ------------------------------------------ |
22
+ | `TreeList` | Root container with `role="tree"` |
23
+ | `TreeItem` | Row with depth, selection, expand/collapse |
24
+ | `TreeExpandButton` | Chevron toggle that rotates when expanded |
25
+ | `TreeItemContent` | Truncated text label |
26
+ | `TreeItemIcon` | Colour dot or icon |
27
+ | `TreeItemActions` | Right-aligned actions, visible on hover |
28
+
29
+ ## Hooks
30
+
31
+ ### useTreeExpansion
32
+
33
+ Manages expand/collapse state for a set of node IDs.
34
+
35
+ ```ts
36
+ const { isExpanded, toggle, expandAll, collapseAll } = useTreeExpansion([
37
+ "root",
38
+ ])
39
+ ```
40
+
41
+ | Method | Description |
42
+ | ------------- | ------------------------------ |
43
+ | `isExpanded` | Check if a node is expanded |
44
+ | `toggle` | Toggle a node's expanded state |
45
+ | `expandAll` | Expand a list of node IDs |
46
+ | `collapseAll` | Collapse all nodes |
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: XY pad
3
+ description: Two-dimensional pad for setting two values at once.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/xy-pad
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### Default
15
+
16
+ <ComponentPreview name="xy-pad-demo" />
17
+
18
+ ## Usage
19
+
20
+ Drag anywhere on the pad, or focus it and use the arrow keys. Hold Shift for steps ten times larger. Positive y points up.
21
+
22
+ ```tsx
23
+ const [offset, setOffset] = useState({ x: 0, y: 0 })
24
+
25
+ <XYPad
26
+ aria-label="Shadow offset"
27
+ value={offset}
28
+ onChange={setOffset}
29
+ x={{ min: -40, max: 40, step: 1 }}
30
+ y={{ min: -40, max: 40, step: 1 }}
31
+ />
32
+ ```
33
+
34
+ ## Theme variables
35
+
36
+ | Variable | Tailwind class | Fallback | Description |
37
+ | ----------------- | ------------------- | ----------- | ------------------------ |
38
+ | `--xy-pad-handle` | `bg-xy-pad-handle` | Text colour | Handle and crosshair |
39
+
40
+ ## Props
41
+
42
+ | Prop | Type | Default | Description |
43
+ | ---------- | ------------------------------ | ------------------------------- | ---------------------------------------------------- |
44
+ | `value` | `{ x: number, y: number }` | — | Current point |
45
+ | `onChange` | `(value: XYPadValue) => void` | — | Called while dragging and on each key press |
46
+ | `onStart` | `() => void` | — | Fires when a drag or key press begins, for undo |
47
+ | `x` | `{ min, max, step? }` | `{ min: -1, max: 1, step: 0.01 }` | Horizontal axis |
48
+ | `y` | `{ min, max, step? }` | `{ min: -1, max: 1, step: 0.01 }` | Vertical axis, positive upwards |
49
+ | `disabled` | `boolean` | `false` | Disables dragging and keys |
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: useClipboard
3
+ description: Copy, cut, and paste with keyboard shortcuts and system clipboard integration.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/use-clipboard
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { useClipboard } from "@/hooks/substrate/use-clipboard"
16
+
17
+ function Canvas() {
18
+ const { copy, cut, paste } = useClipboard({
19
+ onCopy: () => {
20
+ // Return the items to be copied
21
+ return selectedElements.map((el) => ({ ...el }))
22
+ },
23
+ onPaste: (items) => {
24
+ // Offset pasted items slightly so they don't overlap
25
+ const offset = items.map((el) => ({
26
+ ...el,
27
+ id: generateId(),
28
+ x: el.x + 20,
29
+ y: el.y + 20,
30
+ }))
31
+ addElements(offset)
32
+ },
33
+ onCut: () => {
34
+ const items = selectedElements.map((el) => ({ ...el }))
35
+ removeSelected()
36
+ return items
37
+ },
38
+ })
39
+
40
+ return <div>...</div>
41
+ }
42
+ ```
43
+
44
+ ## Features
45
+
46
+ The hook automatically listens for ⌘C / ⌘X / ⌘V (Ctrl on Windows/Linux) and ignores shortcuts when the focus is inside an `<input>`, `<textarea>`, or `contentEditable` element. It writes to the system clipboard when the browser permits, falling back to an internal ref-based clipboard otherwise.
47
+
48
+ Custom `serialize` and `deserialize` functions can be provided to support non-JSON data formats. The `dataType` option sets the clipboard MIME type key.
49
+
50
+ ## API
51
+
52
+ ### Options
53
+
54
+ | Option | Type | Default | Description |
55
+ | --- | --- | --- | --- |
56
+ | `onCopy` | `() => T[]` | — | Return items to copy from the current selection |
57
+ | `onPaste` | `(items: T[]) => void` | — | Called with deserialized items when pasting |
58
+ | `onCut` | `() => T[]` | `onCopy` | Return items to copy, then remove them |
59
+ | `serialize` | `(items: T[]) => string` | `JSON.stringify` | Custom serialization |
60
+ | `deserialize` | `(data: string) => T[]` | `JSON.parse` | Custom deserialization |
61
+ | `dataType` | `string` | `"application/x-substrate-clipboard"` | Clipboard data type key |
62
+ | `disabled` | `boolean` | `false` | Disable the hook |
63
+
64
+ ### Returns
65
+
66
+ | Property | Type | Description |
67
+ | --- | --- | --- |
68
+ | `copy` | `() => void` | Copy the current selection |
69
+ | `cut` | `() => void` | Cut the current selection |
70
+ | `paste` | `() => Promise<void>` | Paste from clipboard |
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: useDragReorder
3
+ description: Pointer-based list reordering with visual feedback.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/use-drag-reorder
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { useDragReorder } from "@/hooks/substrate/use-drag-reorder"
16
+
17
+ function SortableList() {
18
+ const [items, setItems] = useState(["Layer 1", "Layer 2", "Layer 3"])
19
+
20
+ const { getDragItemProps, isDragging } = useDragReorder({
21
+ items,
22
+ onReorder: setItems,
23
+ })
24
+
25
+ return (
26
+ <ul>
27
+ {items.map((item, i) => (
28
+ <li key={item} {...getDragItemProps(i)}>
29
+ {item}
30
+ </li>
31
+ ))}
32
+ </ul>
33
+ )
34
+ }
35
+ ```
36
+
37
+ ## How it works
38
+
39
+ The hook uses pointer capture to track drag movement without relying on the HTML drag-and-drop API. It calculates drop position from the vertical distance moved divided by item height, giving smooth index-based reordering. A configurable threshold (default 3px) prevents accidental drags from firing on normal clicks.
40
+
41
+ The `getDragItemProps` function returns `onPointerDown`, inline `style` for opacity and CSS transforms, and a `data-dragging` attribute – so non-dragged items visually shift out of the way while the dragged item becomes translucent.
42
+
43
+ ## API
44
+
45
+ ### Options
46
+
47
+ | Option | Type | Default | Description |
48
+ | --- | --- | --- | --- |
49
+ | `items` | `T[]` | — | The current ordered list |
50
+ | `onReorder` | `(items: T[]) => void` | — | Called with the new order on drop |
51
+ | `threshold` | `number` | `3` | Pixels to move before a drag starts |
52
+
53
+ ### Returns
54
+
55
+ | Property | Type | Description |
56
+ | --- | --- | --- |
57
+ | `getDragItemProps` | `(index: number) => DragItemProps` | Spread on each draggable item |
58
+ | `dragIndex` | `number \| null` | Index of the item being dragged |
59
+ | `dropIndex` | `number \| null` | Index where it would be dropped |
60
+ | `isDragging` | `boolean` | Whether a drag is in progress |
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: useSelection
3
+ description: Multi-select hook with cmd-click toggle and shift-click range selection.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/use-selection
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { useSelection } from "@/hooks/substrate/use-selection"
16
+
17
+ const itemIds = ["a", "b", "c", "d", "e"]
18
+
19
+ function LayerPanel() {
20
+ const { selectedIds, select, selectAll, deselectAll, isSelected } =
21
+ useSelection({
22
+ items: itemIds,
23
+ onSelectionChange: (ids) => console.log("Selected:", ids),
24
+ })
25
+
26
+ return (
27
+ <ul>
28
+ {itemIds.map((id) => (
29
+ <li
30
+ key={id}
31
+ onClick={(e) => select(id, e)}
32
+ style={{ background: isSelected(id) ? "var(--accent)" : undefined }}
33
+ >
34
+ {id}
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ )
39
+ }
40
+ ```
41
+
42
+ ## Features
43
+
44
+ The hook handles three interaction modes automatically based on modifier keys held during a click:
45
+
46
+ - **Plain click** – selects only the clicked item, deselecting everything else.
47
+ - **⌘/Ctrl + click** – toggles the clicked item without affecting the rest of the selection.
48
+ - **Shift + click** – selects a contiguous range from the last-clicked item to the current one. When combined with ⌘/Ctrl, the range is added to the existing selection rather than replacing it.
49
+
50
+ ## API
51
+
52
+ ### Options
53
+
54
+ | Option | Type | Description |
55
+ | --- | --- | --- |
56
+ | `items` | `T[]` | All item IDs in display order – needed for shift-click range calculation |
57
+ | `onSelectionChange` | `(ids: Set<T>) => void` | Called whenever the selection changes |
58
+
59
+ ### Returns
60
+
61
+ | Property | Type | Description |
62
+ | --- | --- | --- |
63
+ | `selectedIds` | `Set<T>` | The current selection |
64
+ | `select` | `(id: T, event?) => void` | Handle a click – reads `metaKey`, `ctrlKey`, `shiftKey` from the event |
65
+ | `setSelection` | `(ids: T[]) => void` | Replace the selection with specific IDs |
66
+ | `selectAll` | `() => void` | Select every item |
67
+ | `deselectAll` | `() => void` | Clear the selection |
68
+ | `isSelected` | `(id: T) => boolean` | Check whether an item is selected |
@@ -0,0 +1,83 @@
1
+ ---
2
+ title: useUndoable
3
+ description: Lightweight undo/redo stack using refs for performance.
4
+ ---
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npx shadcn@latest add https://substrate.georgedrury.co.uk/r/use-undoable
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { useUndoable } from "@/hooks/substrate/use-undoable"
16
+
17
+ type CanvasState = { elements: Element[] }
18
+
19
+ function Editor() {
20
+ const [state, setState] = useState<CanvasState>({ elements: [] })
21
+
22
+ const { pushSnapshot, undo, redo, canUndo, canRedo } =
23
+ useUndoable<CanvasState>({ maxHistory: 100 })
24
+
25
+ function handleMove(id: string, x: number, y: number) {
26
+ pushSnapshot(state) // save before mutating
27
+ setState((prev) => ({
28
+ elements: prev.elements.map((el) =>
29
+ el.id === id ? { ...el, x, y } : el
30
+ ),
31
+ }))
32
+ }
33
+
34
+ function handleUndo() {
35
+ const previous = undo()
36
+ if (previous) setState(previous)
37
+ }
38
+
39
+ function handleRedo() {
40
+ const next = redo()
41
+ if (next) setState(next)
42
+ }
43
+
44
+ return (
45
+ <>
46
+ <HistoryControls
47
+ canUndo={canUndo}
48
+ canRedo={canRedo}
49
+ onUndo={handleUndo}
50
+ onRedo={handleRedo}
51
+ />
52
+ {/* Canvas */}
53
+ </>
54
+ )
55
+ }
56
+ ```
57
+
58
+ ## Design
59
+
60
+ The hook stores history in refs rather than state to avoid re-renders on every snapshot push. It only triggers a re-render (via a counter state) when `canUndo` or `canRedo` changes. The `undo()` and `redo()` functions return the restored state (or `null` if empty) – the caller is responsible for applying it.
61
+
62
+ This pattern keeps the hook generic and framework-agnostic. It works equally well with `useState`, Zustand, or any external state management.
63
+
64
+ ## API
65
+
66
+ ### Options
67
+
68
+ | Option | Type | Default | Description |
69
+ | --- | --- | --- | --- |
70
+ | `maxHistory` | `number` | `50` | Maximum number of undo snapshots to keep |
71
+
72
+ ### Returns
73
+
74
+ | Property | Type | Description |
75
+ | --- | --- | --- |
76
+ | `pushSnapshot` | `(state: T) => void` | Save the current state before a mutation |
77
+ | `undo` | `() => T \| null` | Pop the previous state, or `null` |
78
+ | `redo` | `() => T \| null` | Pop the next state, or `null` |
79
+ | `canUndo` | `boolean` | Whether undo is available |
80
+ | `canRedo` | `boolean` | Whether redo is available |
81
+ | `undoCount` | `number` | Number of undo states |
82
+ | `redoCount` | `number` | Number of redo states |
83
+ | `clear` | `() => void` | Clear all history |