@substrate-kit/cli 0.1.2 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +21 -0
- package/README.md +25 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -41
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +51 -109
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/docs.d.ts +4 -2
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +33 -54
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +158 -122
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +8 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/get-component.d.ts +1 -1
- package/dist/mcp/tools/get-component.d.ts.map +1 -1
- package/dist/mcp/tools/get-component.js +10 -6
- package/dist/mcp/tools/get-component.js.map +1 -1
- package/dist/mcp/tools/get-docs.d.ts +17 -0
- package/dist/mcp/tools/get-docs.d.ts.map +1 -0
- package/dist/mcp/tools/get-docs.js +23 -0
- package/dist/mcp/tools/get-docs.js.map +1 -0
- package/dist/mcp/tools/install.d.ts +2 -1
- package/dist/mcp/tools/install.d.ts.map +1 -1
- package/dist/mcp/tools/install.js +18 -27
- package/dist/mcp/tools/install.js.map +1 -1
- package/dist/mcp/tools/search.d.ts +1 -1
- package/dist/mcp/tools/search.d.ts.map +1 -1
- package/dist/mcp/tools/search.js +2 -4
- package/dist/mcp/tools/search.js.map +1 -1
- package/dist/{mcp/types.d.ts → registry.d.ts} +6 -1
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +48 -0
- package/dist/registry.js.map +1 -0
- package/dist/utils/agents-file.d.ts +4 -0
- package/dist/utils/agents-file.d.ts.map +1 -0
- package/dist/utils/agents-file.js +36 -0
- package/dist/utils/agents-file.js.map +1 -0
- package/dist/utils/docs.d.ts +7 -0
- package/dist/utils/docs.d.ts.map +1 -0
- package/dist/utils/docs.js +28 -0
- package/dist/utils/docs.js.map +1 -0
- package/dist/utils/shadcn.d.ts +6 -1
- package/dist/utils/shadcn.d.ts.map +1 -1
- package/dist/utils/shadcn.js +25 -12
- package/dist/utils/shadcn.js.map +1 -1
- package/dist/utils/version.d.ts +3 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +8 -0
- package/dist/utils/version.js.map +1 -0
- package/docs/components/action.mdx +30 -0
- package/docs/components/alignment-controls.mdx +30 -0
- package/docs/components/badge.mdx +22 -0
- package/docs/components/breadcrumbs.mdx +25 -0
- package/docs/components/collapsible-pane.mdx +35 -0
- package/docs/components/colour-picker.mdx +66 -0
- package/docs/components/command-palette.mdx +36 -0
- package/docs/components/context-menu.mdx +31 -0
- package/docs/components/controls-timeline.mdx +107 -0
- package/docs/components/controls.mdx +168 -0
- package/docs/components/design-canvas.mdx +51 -0
- package/docs/components/document-tabs.mdx +25 -0
- package/docs/components/easing-curve-editor.mdx +85 -0
- package/docs/components/empty-state.mdx +26 -0
- package/docs/components/gallery.mdx +60 -0
- package/docs/components/history-controls.mdx +47 -0
- package/docs/components/image-picker.mdx +50 -0
- package/docs/components/layer-list.mdx +36 -0
- package/docs/components/node.mdx +85 -0
- package/docs/components/number-input.mdx +44 -0
- package/docs/components/number-slider.mdx +32 -0
- package/docs/components/pane.mdx +39 -0
- package/docs/components/panel.mdx +28 -0
- package/docs/components/popover.mdx +32 -0
- package/docs/components/progress-bar.mdx +24 -0
- package/docs/components/resizer.mdx +24 -0
- package/docs/components/select.mdx +31 -0
- package/docs/components/shortcut-overlay.mdx +43 -0
- package/docs/components/slider.mdx +20 -0
- package/docs/components/spring-editor.mdx +85 -0
- package/docs/components/status-bar.mdx +42 -0
- package/docs/components/surface.mdx +47 -0
- package/docs/components/swatch-palette.mdx +36 -0
- package/docs/components/text-area.mdx +26 -0
- package/docs/components/timeline.mdx +96 -0
- package/docs/components/toast.mdx +42 -0
- package/docs/components/toggle-group.mdx +40 -0
- package/docs/components/toggle.mdx +27 -0
- package/docs/components/toolbar.mdx +24 -0
- package/docs/components/tree-list.mdx +46 -0
- package/docs/components/xy-pad.mdx +49 -0
- package/docs/hooks/use-clipboard.mdx +70 -0
- package/docs/hooks/use-drag-reorder.mdx +60 -0
- package/docs/hooks/use-selection.mdx +68 -0
- package/docs/hooks/use-undoable.mdx +83 -0
- package/docs/hooks/use-viewport.mdx +75 -0
- package/docs/utilities/bounding-box.mdx +76 -0
- package/docs/utilities/keyboard-shortcuts.mdx +64 -0
- package/docs/utilities/snap.mdx +73 -0
- package/package.json +6 -4
- package/registry.json +274 -6
- package/skills/3d-scene/SKILL.md +2 -1
- package/skills/animation/SKILL.md +2 -1
- package/skills/canvas-2d/SKILL.md +67 -5
- package/skills/composing-panels/SKILL.md +1 -4
- package/skills/data-visualisation/SKILL.md +2 -1
- package/skills/image-generation/SKILL.md +2 -1
- package/skills/install-components/SKILL.md +22 -16
- package/skills/scaffold-playground/SKILL.md +39 -19
- package/skills/substrate-canvas/SKILL.md +5 -0
- package/skills/substrate-controls/SKILL.md +57 -2
- package/skills/substrate-interaction/SKILL.md +19 -10
- package/skills/substrate-scaffold/SKILL.md +4 -4
- package/skills/theming/SKILL.md +8 -2
- package/skills/wire-interactions/SKILL.md +6 -3
- package/templates/agents-section.md +41 -0
- package/dist/commands/browse.d.ts +0 -2
- package/dist/commands/browse.d.ts.map +0 -1
- package/dist/commands/browse.js +0 -25
- package/dist/commands/browse.js.map +0 -1
- package/dist/commands/create.d.ts +0 -9
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -27
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/variables.d.ts +0 -4
- package/dist/commands/variables.d.ts.map +0 -1
- package/dist/commands/variables.js +0 -111
- package/dist/commands/variables.js.map +0 -1
- package/dist/mcp/registry.d.ts +0 -3
- package/dist/mcp/registry.d.ts.map +0 -1
- package/dist/mcp/registry.js +0 -43
- package/dist/mcp/registry.js.map +0 -1
- package/dist/mcp/tools/theme.d.ts +0 -10
- package/dist/mcp/tools/theme.d.ts.map +0 -1
- package/dist/mcp/tools/theme.js +0 -37
- package/dist/mcp/tools/theme.js.map +0 -1
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/mcp/types.js +0 -2
- package/dist/mcp/types.js.map +0 -1
|
@@ -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 |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useViewport
|
|
3
|
+
description: Pan and zoom state management for canvas surfaces.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx shadcn@latest add https://substrate.georgedrury.co.uk/r/use-viewport
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { useViewport } from "@/hooks/substrate/use-viewport"
|
|
16
|
+
|
|
17
|
+
function Canvas() {
|
|
18
|
+
const {
|
|
19
|
+
viewport,
|
|
20
|
+
handleWheel,
|
|
21
|
+
toWorld,
|
|
22
|
+
toScreen,
|
|
23
|
+
zoomIn,
|
|
24
|
+
zoomOut,
|
|
25
|
+
resetZoom,
|
|
26
|
+
zoomToFit,
|
|
27
|
+
} = useViewport({ minZoom: 0.1, maxZoom: 10 })
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div onWheel={handleWheel} style={{ overflow: "hidden" }}>
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
|
34
|
+
transformOrigin: "0 0",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{/* Canvas content */}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
This is a surface-agnostic extraction of pan/zoom logic. It manages `{ x, y, zoom }` state and provides coordinate transforms between screen and world space. The `handleWheel` callback supports two modes – plain scroll for panning and Ctrl/⌘ + scroll for pinch-zoom – keeping the world point under the cursor stationary during zoom.
|
|
47
|
+
|
|
48
|
+
The hook uses refs internally so that callbacks like `toWorld` and `toScreen` remain referentially stable across renders.
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### Options
|
|
53
|
+
|
|
54
|
+
| Option | Type | Default | Description |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `initial` | `Partial<Viewport>` | `{ x: 0, y: 0, zoom: 1 }` | Initial viewport state |
|
|
57
|
+
| `minZoom` | `number` | `0.1` | Minimum zoom level |
|
|
58
|
+
| `maxZoom` | `number` | `10` | Maximum zoom level |
|
|
59
|
+
| `zoomFactor` | `number` | `1.2` | Multiplier for `zoomIn` / `zoomOut` |
|
|
60
|
+
|
|
61
|
+
### Returns
|
|
62
|
+
|
|
63
|
+
| Property | Type | Description |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `viewport` | `Viewport` | Current `{ x, y, zoom }` state |
|
|
66
|
+
| `setViewport` | `(v: Viewport) => void` | Set viewport directly |
|
|
67
|
+
| `toWorld` | `(screenX, screenY) => Point` | Convert screen coordinates to world |
|
|
68
|
+
| `toScreen` | `(worldX, worldY) => Point` | Convert world coordinates to screen |
|
|
69
|
+
| `pan` | `(dx, dy) => void` | Pan by a screen-space delta |
|
|
70
|
+
| `zoomToPoint` | `(screenX, screenY, zoom) => void` | Zoom keeping a screen point stationary |
|
|
71
|
+
| `zoomIn` | `(screenX?, screenY?) => void` | Zoom in by `zoomFactor` |
|
|
72
|
+
| `zoomOut` | `(screenX?, screenY?) => void` | Zoom out by `zoomFactor` |
|
|
73
|
+
| `resetZoom` | `() => void` | Reset to `{ x: 0, y: 0, zoom: 1 }` |
|
|
74
|
+
| `zoomToFit` | `(bounds, padding?) => void` | Fit a bounding box into the viewport |
|
|
75
|
+
| `handleWheel` | `(e: WheelEvent) => void` | Attach to your container's `onWheel` |
|