create-substrate 0.1.6 → 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 +82 -80
  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
@@ -3,10 +3,8 @@ export const scene3d = {
3
3
  "toolbar",
4
4
  "panel",
5
5
  "pane",
6
- "action",
7
- "slider",
8
6
  "number-input",
9
- "colour-picker",
7
+ "controls",
10
8
  "status-bar",
11
9
  ],
12
10
  extraDeps: {
@@ -35,10 +33,9 @@ import {
35
33
  PaneAction,
36
34
  PaneContent,
37
35
  } from "@/components/substrate/pane"
38
- import { Action, ActionLabel, ActionControls } from "@/components/substrate/action"
39
36
  import { NumberInput } from "@/components/substrate/number-input"
40
- import { Slider } from "@/components/substrate/slider"
41
- import { ColourPicker } from "@/components/substrate/colour-picker"
37
+ import { Controls } from "@/components/substrate/controls"
38
+ import { useControls, type ControlsConfig } from "@/hooks/substrate/use-controls"
42
39
  import {
43
40
  StatusBar,
44
41
  StatusBarSection,
@@ -107,7 +104,7 @@ const INITIAL_OBJECTS: SceneObject[] = [
107
104
  ]
108
105
 
109
106
  const toolbarButton =
110
- "inline-flex h-7 min-w-7 items-center justify-center rounded-md px-2 outline-none transition-all hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
107
+ "inline-flex h-7 min-w-7 items-center justify-center rounded-md px-2 outline-none transition-[color,background-color,box-shadow,transform] active:scale-[0.96] hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
111
108
 
112
109
  function isMode(value: string): value is Mode {
113
110
  return MODES.some((mode) => mode === value)
@@ -184,11 +181,43 @@ function SceneMesh({
184
181
  )
185
182
  }
186
183
 
184
+ type Material = Pick<SceneObject, "colour" | "roughness" | "metalness">
185
+
186
+ // Material values belong to the selected object, not to the panel. Mounting
187
+ // this with key={object.id} gives each selection its own panel: the object's
188
+ // values seed the config, and edits are written back through onChange.
189
+ function MaterialControls({
190
+ object,
191
+ onChange,
192
+ }: {
193
+ object: SceneObject
194
+ onChange: (material: Material) => void
195
+ }) {
196
+ const [config] = useState(
197
+ () =>
198
+ ({
199
+ colour: object.colour,
200
+ roughness: [object.roughness, 0, 1, 0.01],
201
+ metalness: [object.metalness, 0, 1, 0.01],
202
+ }) satisfies ControlsConfig,
203
+ )
204
+ const material = useControls("Material", config)
205
+
206
+ const onChangeRef = useRef(onChange)
207
+ onChangeRef.current = onChange
208
+ useEffect(() => {
209
+ onChangeRef.current(material)
210
+ }, [material])
211
+
212
+ return <Controls panel="Material" />
213
+ }
214
+
187
215
  export default function App() {
188
216
  const [mode, setMode] = useState<Mode>("select")
189
217
  const [objects, setObjects] = useState(INITIAL_OBJECTS)
190
218
  const [selectedId, setSelectedId] = useState<string | null>("box-1")
191
- const [lightIntensity, setLightIntensity] = useState(1.5)
219
+ const lighting = useControls("Lighting", { intensity: [1.5, 0, 4, 0.05] })
220
+ const lightIntensity = lighting.intensity
192
221
  const nextIndex = useRef(INITIAL_OBJECTS.length + 1)
193
222
 
194
223
  const selected = objects.find((object) => object.id === selectedId) ?? null
@@ -362,25 +391,7 @@ export default function App() {
362
391
  </PaneContent>
363
392
  </Pane>
364
393
 
365
- <Pane>
366
- <PaneHeader>
367
- <PaneLabel>Lighting</PaneLabel>
368
- </PaneHeader>
369
- <PaneContent>
370
- <Action>
371
- <ActionLabel>Intensity</ActionLabel>
372
- <ActionControls>
373
- <Slider
374
- min={0}
375
- max={4}
376
- step={0.05}
377
- value={[lightIntensity]}
378
- onValueChange={([value]) => setLightIntensity(value)}
379
- />
380
- </ActionControls>
381
- </Action>
382
- </PaneContent>
383
- </Pane>
394
+ <Controls panel="Lighting" />
384
395
 
385
396
  {selected ? (
386
397
  <>
@@ -451,53 +462,11 @@ export default function App() {
451
462
  </PaneContent>
452
463
  </Pane>
453
464
 
454
- <Pane>
455
- <PaneHeader>
456
- <PaneLabel>Material</PaneLabel>
457
- </PaneHeader>
458
- <PaneContent className="space-y-2">
459
- <Action>
460
- <ActionLabel>Colour</ActionLabel>
461
- <ActionControls>
462
- <ColourPicker
463
- value={selected.colour}
464
- onChange={(colour) => updateObject(selected.id, { colour })}
465
- />
466
- <span className="font-mono text-xs text-muted-foreground">
467
- {selected.colour}
468
- </span>
469
- </ActionControls>
470
- </Action>
471
- <Action>
472
- <ActionLabel>Roughness</ActionLabel>
473
- <ActionControls>
474
- <Slider
475
- min={0}
476
- max={1}
477
- step={0.01}
478
- value={[selected.roughness]}
479
- onValueChange={([value]) =>
480
- updateObject(selected.id, { roughness: value })
481
- }
482
- />
483
- </ActionControls>
484
- </Action>
485
- <Action>
486
- <ActionLabel>Metalness</ActionLabel>
487
- <ActionControls>
488
- <Slider
489
- min={0}
490
- max={1}
491
- step={0.01}
492
- value={[selected.metalness]}
493
- onValueChange={([value]) =>
494
- updateObject(selected.id, { metalness: value })
495
- }
496
- />
497
- </ActionControls>
498
- </Action>
499
- </PaneContent>
500
- </Pane>
465
+ <MaterialControls
466
+ key={selected.id}
467
+ object={selected}
468
+ onChange={(patch) => updateObject(selected.id, patch)}
469
+ />
501
470
  </>
502
471
  ) : (
503
472
  <Pane>
@@ -1 +1 @@
1
- {"version":3,"file":"3d-scene.js","sourceRoot":"","sources":["../../src/surfaces/3d-scene.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,QAAQ;QACR,QAAQ;QACR,cAAc;QACd,eAAe;QACf,YAAY;KACb;IACD,SAAS,EAAE;QACT,KAAK,EAAE,UAAU;QACjB,oBAAoB,EAAE,QAAQ;QAC9B,mBAAmB,EAAE,SAAS;KAC/B;IACD,YAAY,EAAE;QACZ,cAAc,EAAE,UAAU;KAC3B;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6fZ;CACA,CAAA"}
1
+ {"version":3,"file":"3d-scene.js","sourceRoot":"","sources":["../../src/surfaces/3d-scene.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,cAAc;QACd,UAAU;QACV,YAAY;KACb;IACD,SAAS,EAAE;QACT,KAAK,EAAE,UAAU;QACjB,oBAAoB,EAAE,QAAQ;QAC9B,mBAAmB,EAAE,SAAS;KAC/B;IACD,YAAY,EAAE;QACZ,cAAc,EAAE,UAAU;KAC3B;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgeZ;CACA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-2d.d.ts","sourceRoot":"","sources":["../../src/surfaces/canvas-2d.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,QAAQ,EAAE,aAmatB,CAAA"}
1
+ {"version":3,"file":"canvas-2d.d.ts","sourceRoot":"","sources":["../../src/surfaces/canvas-2d.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,QAAQ,EAAE,aAgatB,CAAA"}
@@ -45,6 +45,7 @@ import {
45
45
  } from "@/components/substrate/status-bar"
46
46
  import { ZoomControls } from "@/components/substrate/zoom-controls"
47
47
  import { useDocumentStore } from "@/stores/substrate/document-store"
48
+ import { useCanvasTransform } from "@/hooks/substrate/use-canvas-transform"
48
49
  import { useToolStore } from "@/stores/substrate/tool-store"
49
50
  import { createRectangle, createEllipse } from "@/lib/substrate/element-factory"
50
51
  import type { DesignElement, ToolType } from "@/lib/substrate/types"
@@ -82,11 +83,8 @@ const SHAPE_TOOLS: ToolDefinition[] = [
82
83
 
83
84
  const TOOLS = [...POINTER_TOOLS, ...SHAPE_TOOLS]
84
85
 
85
- // The colour picker edits hex values. Shapes drawn with the tools start with
86
- // an oklch fill, so the picker opens on this grey until a colour is chosen.
87
- const PICKER_FALLBACK = "#d4d4d4"
88
86
  const ACTION_BUTTON =
89
- "inline-flex h-7 min-w-7 items-center justify-center rounded-md px-2 outline-none transition-all hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent data-active:text-accent-foreground"
87
+ "inline-flex h-7 min-w-7 items-center justify-center rounded-md px-2 outline-none transition-[color,background-color,box-shadow,transform] active:scale-[0.96] hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent data-active:text-accent-foreground"
90
88
 
91
89
  // Seed the document with a couple of shapes so the canvas is not empty.
92
90
  // Everything on the canvas lives in the document store; replace this with
@@ -279,7 +277,6 @@ function PropertiesPanel() {
279
277
 
280
278
  const id = element.id
281
279
  const fill = element.fill
282
- const fillHex = fill && fill.color.startsWith("#") ? fill.color : PICKER_FALLBACK
283
280
 
284
281
  return (
285
282
  <Panel position="right" className="shrink-0">
@@ -342,8 +339,8 @@ function PropertiesPanel() {
342
339
  <PaneContent className="flex items-center gap-2">
343
340
  {/* One undo step per drag: snapshot when the picker is pressed */}
344
341
  <ColourPicker
345
- value={fillHex}
346
- onPointerDown={pushSnapshot}
342
+ value={fill ? fill.color : "transparent"}
343
+ onStart={pushSnapshot}
347
344
  onChange={(color) =>
348
345
  updateElement(id, {
349
346
  fill: { type: "solid", color, opacity: fill?.opacity ?? 1 },
@@ -367,7 +364,7 @@ function PropertiesPanel() {
367
364
  function EditorStatusBar() {
368
365
  const activeTool = useToolStore((s) => s.activeTool)
369
366
  const zoom = useDocumentStore((s) => s.camera.zoom)
370
- const setCamera = useDocumentStore((s) => s.setCamera)
367
+ const { setZoom, zoomToFit } = useCanvasTransform()
371
368
  const selectedCount = useDocumentStore((s) => s.selectedIds.size)
372
369
  const elementCount = useDocumentStore((s) => s.elementOrder.length)
373
370
 
@@ -390,8 +387,8 @@ function EditorStatusBar() {
390
387
  <StatusBarSection align="right">
391
388
  <ZoomControls
392
389
  zoom={zoom}
393
- onZoomChange={(next) => setCamera({ zoom: next })}
394
- onZoomToFit={() => setCamera({ x: 0, y: 0, zoom: 1 })}
390
+ onZoomChange={setZoom}
391
+ onZoomToFit={zoomToFit}
395
392
  min={0.1}
396
393
  max={10}
397
394
  step={0.1}
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-2d.js","sourceRoot":"","sources":["../../src/surfaces/canvas-2d.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAkB;IACrC,UAAU,EAAE;QACV,OAAO;QACP,gBAAgB;QAChB,YAAY;QACZ,iBAAiB;QACjB,eAAe;QACf,SAAS;QACT,OAAO;QACP,MAAM;QACN,YAAY;QACZ,cAAc;QACd,eAAe;QACf,kBAAkB;QAClB,YAAY;QACZ,eAAe;KAChB;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiZZ;CACA,CAAA"}
1
+ {"version":3,"file":"canvas-2d.js","sourceRoot":"","sources":["../../src/surfaces/canvas-2d.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAkB;IACrC,UAAU,EAAE;QACV,OAAO;QACP,gBAAgB;QAChB,YAAY;QACZ,iBAAiB;QACjB,eAAe;QACf,SAAS;QACT,OAAO;QACP,MAAM;QACN,YAAY;QACZ,cAAc;QACd,eAAe;QACf,kBAAkB;QAClB,YAAY;QACZ,eAAe;KAChB;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8YZ;CACA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"data-vis.d.ts","sourceRoot":"","sources":["../../src/surfaces/data-vis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,OAAO,EAAE,aAiVrB,CAAA"}
1
+ {"version":3,"file":"data-vis.d.ts","sourceRoot":"","sources":["../../src/surfaces/data-vis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,OAAO,EAAE,aAuOrB,CAAA"}
@@ -2,12 +2,7 @@ export const dataVis = {
2
2
  components: [
3
3
  "toolbar",
4
4
  "panel",
5
- "pane",
6
- "action",
7
- "select",
8
- "number-slider",
9
- "colour-picker",
10
- "toggle-group",
5
+ "controls",
11
6
  "status-bar",
12
7
  ],
13
8
  extraDeps: {
@@ -33,22 +28,8 @@ import {
33
28
  ToolbarToggleGroup,
34
29
  } from "@/components/substrate/toolbar"
35
30
  import { Panel } from "@/components/substrate/panel"
36
- import { Pane, PaneHeader, PaneLabel } from "@/components/substrate/pane"
37
- import {
38
- Action,
39
- ActionControls,
40
- ActionLabel,
41
- } from "@/components/substrate/action"
42
- import {
43
- Select,
44
- SelectContent,
45
- SelectItem,
46
- SelectTrigger,
47
- SelectValue,
48
- } from "@/components/substrate/select"
49
- import { NumberSlider } from "@/components/substrate/number-slider"
50
- import { ColourPicker } from "@/components/substrate/colour-picker"
51
- import { ToggleGroup, ToggleGroupItem } from "@/components/substrate/toggle-group"
31
+ import { Controls } from "@/components/substrate/controls"
32
+ import { useControls } from "@/hooks/substrate/use-controls"
52
33
  import {
53
34
  StatusBar,
54
35
  StatusBarItem,
@@ -76,17 +57,17 @@ const SAMPLE_DATA = [
76
57
  { month: "Dec", revenue: 9100, users: 7400, orders: 830 },
77
58
  ]
78
59
 
79
- const METRICS = [
80
- { value: "revenue", label: "Revenue" },
81
- { value: "users", label: "Users" },
82
- { value: "orders", label: "Orders" },
83
- ] as const
84
-
85
60
  const CHART_TYPES = ["line", "area", "bar"] as const
86
61
 
87
- type Metric = (typeof METRICS)[number]["value"]
62
+ type Metric = "revenue" | "users" | "orders"
88
63
  type ChartType = (typeof CHART_TYPES)[number]
89
64
 
65
+ const METRICS: { value: Metric; label: string }[] = [
66
+ { value: "revenue", label: "Revenue" },
67
+ { value: "users", label: "Users" },
68
+ { value: "orders", label: "Orders" },
69
+ ]
70
+
90
71
  function isMetric(value: string): value is Metric {
91
72
  return METRICS.some((metric) => metric.value === value)
92
73
  }
@@ -106,12 +87,22 @@ const TOOLTIP_STYLE = {
106
87
 
107
88
  export default function App() {
108
89
  const [chartType, setChartType] = useState<ChartType>("line")
109
- const [metric, setMetric] = useState<Metric>("revenue")
110
- const [months, setMonths] = useState(12)
111
- const [colour, setColour] = useState("#6366f1")
112
- const [strokeWidth, setStrokeWidth] = useState(2)
113
- const [fillOpacity, setFillOpacity] = useState(30)
114
- const [grid, setGrid] = useState("on")
90
+
91
+ // Each useControls call is one panel; <Controls panel="..." /> renders it.
92
+ // Edit these objects to add, remove or retune a control.
93
+ const dataControls = useControls("Data", {
94
+ metric: { type: "select", options: METRICS, default: "revenue" },
95
+ months: [12, 3, SAMPLE_DATA.length, 1],
96
+ })
97
+ const style = useControls("Style", {
98
+ colour: "#6366f1",
99
+ stroke: [2, 1, 6, 0.5],
100
+ fill: [0.3, 0, 1, 0.05],
101
+ grid: true,
102
+ })
103
+
104
+ const metric = isMetric(dataControls.metric) ? dataControls.metric : "revenue"
105
+ const { months } = dataControls
115
106
 
116
107
  const data = SAMPLE_DATA.slice(0, months)
117
108
  const values = data.map((row) => row[metric])
@@ -120,7 +111,7 @@ export default function App() {
120
111
 
121
112
  const axes = (
122
113
  <>
123
- {grid === "on" && (
114
+ {style.grid && (
124
115
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
125
116
  )}
126
117
  <XAxis dataKey="month" tick={AXIS_TICK} stroke="var(--border)" tickLine={false} />
@@ -139,46 +130,7 @@ export default function App() {
139
130
  <div className="flex h-screen flex-col bg-background text-foreground">
140
131
  <div className="flex min-h-0 flex-1">
141
132
  <Panel position="left" className="shrink-0">
142
- <Pane>
143
- <PaneHeader>
144
- <PaneLabel>Data</PaneLabel>
145
- </PaneHeader>
146
- <Action>
147
- <ActionLabel>Metric</ActionLabel>
148
- <ActionControls>
149
- <Select
150
- value={metric}
151
- onValueChange={(value) => {
152
- if (isMetric(value)) setMetric(value)
153
- }}
154
- >
155
- <SelectTrigger aria-label="Metric">
156
- <SelectValue />
157
- </SelectTrigger>
158
- <SelectContent>
159
- {METRICS.map((item) => (
160
- <SelectItem key={item.value} value={item.value}>
161
- {item.label}
162
- </SelectItem>
163
- ))}
164
- </SelectContent>
165
- </Select>
166
- </ActionControls>
167
- </Action>
168
- <Action>
169
- <ActionLabel>Months</ActionLabel>
170
- <ActionControls>
171
- <NumberSlider
172
- value={months}
173
- onChange={(value) => setMonths(Math.round(value))}
174
- min={3}
175
- max={SAMPLE_DATA.length}
176
- step={1}
177
- precision={0}
178
- />
179
- </ActionControls>
180
- </Action>
181
- </Pane>
133
+ <Controls panel="Data" />
182
134
  </Panel>
183
135
 
184
136
  {/* Chart surface */}
@@ -199,7 +151,7 @@ export default function App() {
199
151
  <Bar
200
152
  dataKey={metric}
201
153
  name={metricLabel}
202
- fill={colour}
154
+ fill={style.colour}
203
155
  radius={[4, 4, 0, 0]}
204
156
  maxBarSize={32}
205
157
  isAnimationActive={false}
@@ -212,10 +164,10 @@ export default function App() {
212
164
  type="monotone"
213
165
  dataKey={metric}
214
166
  name={metricLabel}
215
- stroke={colour}
216
- strokeWidth={strokeWidth}
217
- fill={colour}
218
- fillOpacity={fillOpacity / 100}
167
+ stroke={style.colour}
168
+ strokeWidth={style.stroke}
169
+ fill={style.colour}
170
+ fillOpacity={style.fill}
219
171
  isAnimationActive={false}
220
172
  />
221
173
  </AreaChart>
@@ -226,9 +178,9 @@ export default function App() {
226
178
  type="monotone"
227
179
  dataKey={metric}
228
180
  name={metricLabel}
229
- stroke={colour}
230
- strokeWidth={strokeWidth}
231
- dot={{ fill: colour, r: 3 }}
181
+ stroke={style.colour}
182
+ strokeWidth={style.stroke}
183
+ dot={{ fill: style.colour, r: 3 }}
232
184
  isAnimationActive={false}
233
185
  />
234
186
  </LineChart>
@@ -238,65 +190,7 @@ export default function App() {
238
190
  </main>
239
191
 
240
192
  <Panel position="right" className="shrink-0">
241
- <Pane>
242
- <PaneHeader>
243
- <PaneLabel>Style</PaneLabel>
244
- </PaneHeader>
245
- <Action>
246
- <ActionLabel>Colour</ActionLabel>
247
- <ActionControls>
248
- <ColourPicker value={colour} onChange={setColour} />
249
- <span className="font-mono text-xs text-muted-foreground">{colour}</span>
250
- </ActionControls>
251
- </Action>
252
- {chartType !== "bar" && (
253
- <Action>
254
- <ActionLabel>Stroke</ActionLabel>
255
- <ActionControls>
256
- <NumberSlider
257
- value={strokeWidth}
258
- onChange={setStrokeWidth}
259
- min={1}
260
- max={6}
261
- step={0.5}
262
- suffix="px"
263
- />
264
- </ActionControls>
265
- </Action>
266
- )}
267
- {chartType === "area" && (
268
- <Action>
269
- <ActionLabel>Fill</ActionLabel>
270
- <ActionControls>
271
- <NumberSlider
272
- value={fillOpacity}
273
- onChange={setFillOpacity}
274
- min={0}
275
- max={100}
276
- step={1}
277
- precision={0}
278
- suffix="%"
279
- />
280
- </ActionControls>
281
- </Action>
282
- )}
283
- <Action>
284
- <ActionLabel>Grid</ActionLabel>
285
- <ActionControls>
286
- <ToggleGroup
287
- type="single"
288
- value={grid}
289
- onValueChange={(value) => {
290
- if (value) setGrid(value)
291
- }}
292
- aria-label="Grid lines"
293
- >
294
- <ToggleGroupItem value="on">On</ToggleGroupItem>
295
- <ToggleGroupItem value="off">Off</ToggleGroupItem>
296
- </ToggleGroup>
297
- </ActionControls>
298
- </Action>
299
- </Pane>
193
+ <Controls panel="Style" />
300
194
  </Panel>
301
195
  </div>
302
196
 
@@ -1 +1 @@
1
- {"version":3,"file":"data-vis.js","sourceRoot":"","sources":["../../src/surfaces/data-vis.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,QAAQ;QACR,QAAQ;QACR,eAAe;QACf,eAAe;QACf,cAAc;QACd,YAAY;KACb;IACD,SAAS,EAAE;QACT,QAAQ,EAAE,QAAQ;KACnB;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiUZ;CACA,CAAA"}
1
+ {"version":3,"file":"data-vis.js","sourceRoot":"","sources":["../../src/surfaces/data-vis.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,UAAU;QACV,YAAY;KACb;IACD,SAAS,EAAE;QACT,QAAQ,EAAE,QAAQ;KACnB;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4NZ;CACA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"image-gen.d.ts","sourceRoot":"","sources":["../../src/surfaces/image-gen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,QAAQ,EAAE,aAqctB,CAAA"}
1
+ {"version":3,"file":"image-gen.d.ts","sourceRoot":"","sources":["../../src/surfaces/image-gen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,QAAQ,EAAE,aAibtB,CAAA"}
@@ -6,7 +6,7 @@ export const imageGen = {
6
6
  "action",
7
7
  "gallery",
8
8
  "text-area",
9
- "slider",
9
+ "controls",
10
10
  "number-input",
11
11
  "progress-bar",
12
12
  "status-bar",
@@ -36,7 +36,8 @@ import {
36
36
  GalleryEmpty,
37
37
  } from "@/components/substrate/gallery"
38
38
  import { TextArea } from "@/components/substrate/text-area"
39
- import { Slider } from "@/components/substrate/slider"
39
+ import { Controls } from "@/components/substrate/controls"
40
+ import { useControlsController } from "@/hooks/substrate/use-controls"
40
41
  import { NumberInput } from "@/components/substrate/number-input"
41
42
  import { ProgressBar } from "@/components/substrate/progress-bar"
42
43
  import {
@@ -171,12 +172,18 @@ const INITIAL_GENERATIONS: Generation[] = [
171
172
  ]
172
173
 
173
174
  const TOOLBAR_BUTTON =
174
- "inline-flex h-7 items-center justify-center gap-1.5 rounded-md px-2 text-xs font-medium outline-none transition-all hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
175
+ "inline-flex h-7 items-center justify-center gap-1.5 rounded-md px-2 text-xs font-medium outline-none transition-[color,background-color,box-shadow,transform] active:scale-[0.96] hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
175
176
 
176
177
  export default function App() {
177
178
  const [prompt, setPrompt] = useState("Misty mountain valley at sunrise")
178
- const [steps, setSteps] = useState(30)
179
- const [cfg, setCfg] = useState(7)
179
+ // The Parameters pane is generated from this config. The controller form
180
+ // of the hook adds setValues, used below to load a past result's settings.
181
+ const parameters = useControlsController("Parameters", {
182
+ steps: [30, 1, 100, 1],
183
+ cfgScale: [7, 1, 20, 0.5],
184
+ })
185
+ const steps = parameters.values.steps
186
+ const cfg = parameters.values.cfgScale
180
187
  const [seed, setSeed] = useState(-1)
181
188
  const [aspect, setAspect] = useState<AspectRatio>("1:1")
182
189
  const [generating, setGenerating] = useState(false)
@@ -230,8 +237,7 @@ export default function App() {
230
237
  function handleSelect(generation: Generation) {
231
238
  setSelectedId(generation.id)
232
239
  setPrompt(generation.prompt)
233
- setSteps(generation.steps)
234
- setCfg(generation.cfg)
240
+ parameters.setValues({ steps: generation.steps, cfgScale: generation.cfg })
235
241
  setSeed(generation.seed)
236
242
  setAspect(generation.aspect)
237
243
  }
@@ -270,9 +276,10 @@ export default function App() {
270
276
  {generating && <ProgressBar value={progress} aria-label="Generation progress" />}
271
277
  </PaneContent>
272
278
  </Pane>
279
+ <Controls panel="Parameters" />
273
280
  <Pane>
274
281
  <PaneHeader>
275
- <PaneLabel>Parameters</PaneLabel>
282
+ <PaneLabel>Seed</PaneLabel>
276
283
  <PaneAction
277
284
  type="button"
278
285
  title="Random seed"
@@ -283,33 +290,6 @@ export default function App() {
283
290
  </PaneAction>
284
291
  </PaneHeader>
285
292
  <PaneContent className="space-y-1">
286
- <Action>
287
- <ActionLabel>Steps</ActionLabel>
288
- <ActionControls>
289
- <Slider
290
- aria-label="Steps"
291
- min={1}
292
- max={100}
293
- value={[steps]}
294
- onValueChange={(value) => setSteps(value[0] ?? steps)}
295
- />
296
- <span className="w-8 text-right text-xs tabular-nums">{steps}</span>
297
- </ActionControls>
298
- </Action>
299
- <Action>
300
- <ActionLabel>CFG scale</ActionLabel>
301
- <ActionControls>
302
- <Slider
303
- aria-label="CFG scale"
304
- min={1}
305
- max={20}
306
- step={0.5}
307
- value={[cfg]}
308
- onValueChange={(value) => setCfg(value[0] ?? cfg)}
309
- />
310
- <span className="w-8 text-right text-xs tabular-nums">{cfg}</span>
311
- </ActionControls>
312
- </Action>
313
293
  <Action>
314
294
  <ActionLabel>Seed</ActionLabel>
315
295
  <ActionControls>
@@ -1 +1 @@
1
- {"version":3,"file":"image-gen.js","sourceRoot":"","sources":["../../src/surfaces/image-gen.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAkB;IACrC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,QAAQ;QACR,SAAS;QACT,WAAW;QACX,QAAQ;QACR,cAAc;QACd,cAAc;QACd,YAAY;QACZ,aAAa;KACd;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsbZ;CACA,CAAA"}
1
+ {"version":3,"file":"image-gen.js","sourceRoot":"","sources":["../../src/surfaces/image-gen.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAkB;IACrC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,QAAQ;QACR,SAAS;QACT,WAAW;QACX,UAAU;QACV,cAAc;QACd,cAAc;QACd,YAAY;QACZ,aAAa;KACd;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkaZ;CACA,CAAA"}
@@ -260,7 +260,7 @@ type Drag =
260
260
  | { kind: "pan"; startX: number; startY: number; originX: number; originY: number }
261
261
 
262
262
  const actionButton =
263
- "inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs font-medium outline-none transition-all hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3.5"
263
+ "inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs font-medium outline-none transition-[color,background-color,box-shadow,transform] active:scale-[0.96] hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3.5"
264
264
 
265
265
  export default function App() {
266
266
  const [tool, setTool] = useState<Tool>("select")
@@ -1,4 +1,6 @@
1
1
  export type PackageManager = "npm" | "pnpm" | "bun";
2
+ export declare function isPackageManager(value: string): value is PackageManager;
3
+ export declare function detectRunningPackageManager(userAgent?: string | undefined): PackageManager | null;
2
4
  export declare function detectPackageManager(cwd: string): PackageManager;
3
5
  export declare function getInstallCommand(pm: PackageManager): string;
4
6
  export declare function getRunCommand(pm: PackageManager, script: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"detect-pm.d.ts","sourceRoot":"","sources":["../../src/utils/detect-pm.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;AAEnD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAQhE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxE"}
1
+ {"version":3,"file":"detect-pm.d.ts","sourceRoot":"","sources":["../../src/utils/detect-pm.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;AAEnD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc,CAEvE;AAID,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,MAAM,GAAG,SAA6C,GAChE,cAAc,GAAG,IAAI,CAGvB;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAQhE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxE"}
@@ -1,5 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ export function isPackageManager(value) {
4
+ return value === "npm" || value === "pnpm" || value === "bun";
5
+ }
6
+ // npm, pnpm and bun all set npm_config_user_agent to e.g. "pnpm/9.1.0 npm/? ...",
7
+ // so the tool that ran `create-substrate` is the one the user prefers.
8
+ export function detectRunningPackageManager(userAgent = process.env.npm_config_user_agent) {
9
+ const name = userAgent?.split(" ")[0]?.split("/")[0];
10
+ return name && isPackageManager(name) ? name : null;
11
+ }
3
12
  export function detectPackageManager(cwd) {
4
13
  if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
5
14
  return "bun";