create-vizcraft-playground 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/index.js +171 -0
  2. package/package.json +44 -0
  3. package/template/.github/skills/vizcraft-playground/SKILL.md +573 -0
  4. package/template/README.md +59 -0
  5. package/template/eslint.config.js +23 -0
  6. package/template/index.html +13 -0
  7. package/template/package.json +37 -0
  8. package/template/public/vite.svg +1 -0
  9. package/template/scripts/generate-plugin.js +594 -0
  10. package/template/scripts/init-playground.js +119 -0
  11. package/template/src/App.scss +137 -0
  12. package/template/src/App.tsx +72 -0
  13. package/template/src/assets/react.svg +1 -0
  14. package/template/src/components/InfoModal/InfoModal.scss +211 -0
  15. package/template/src/components/InfoModal/InfoModal.tsx +85 -0
  16. package/template/src/components/Landing/Landing.scss +85 -0
  17. package/template/src/components/Landing/Landing.tsx +55 -0
  18. package/template/src/components/Shell.tsx +144 -0
  19. package/template/src/components/StepIndicator/StepIndicator.scss +151 -0
  20. package/template/src/components/StepIndicator/StepIndicator.tsx +73 -0
  21. package/template/src/components/VizInfoBeacon/VizInfoBeacon.scss +41 -0
  22. package/template/src/components/VizInfoBeacon/VizInfoBeacon.tsx +157 -0
  23. package/template/src/components/plugin-kit/CanvasStage.tsx +30 -0
  24. package/template/src/components/plugin-kit/ConceptPills.tsx +55 -0
  25. package/template/src/components/plugin-kit/PluginLayout.tsx +41 -0
  26. package/template/src/components/plugin-kit/SidePanel.tsx +69 -0
  27. package/template/src/components/plugin-kit/StageHeader.tsx +35 -0
  28. package/template/src/components/plugin-kit/StatBadge.tsx +35 -0
  29. package/template/src/components/plugin-kit/index.ts +42 -0
  30. package/template/src/components/plugin-kit/plugin-kit.scss +241 -0
  31. package/template/src/components/plugin-kit/useConceptModal.tsx +51 -0
  32. package/template/src/index.scss +81 -0
  33. package/template/src/main.tsx +14 -0
  34. package/template/src/playground.config.ts +27 -0
  35. package/template/src/plugins/hello-world/concepts.tsx +70 -0
  36. package/template/src/plugins/hello-world/helloWorldSlice.ts +29 -0
  37. package/template/src/plugins/hello-world/index.ts +48 -0
  38. package/template/src/plugins/hello-world/main.scss +32 -0
  39. package/template/src/plugins/hello-world/main.tsx +144 -0
  40. package/template/src/plugins/hello-world/useHelloWorldAnimation.ts +99 -0
  41. package/template/src/registry.ts +73 -0
  42. package/template/src/store/slices/simulationSlice.ts +47 -0
  43. package/template/src/store/store.ts +13 -0
  44. package/template/src/types/ModelPlugin.ts +55 -0
  45. package/template/src/utils/random.ts +11 -0
  46. package/template/tsconfig.app.json +35 -0
  47. package/template/tsconfig.json +7 -0
  48. package/template/tsconfig.node.json +26 -0
  49. package/template/vite.config.ts +7 -0
@@ -0,0 +1,573 @@
1
+ ---
2
+ name: vizcraft-playground
3
+ description: "Build, extend, and debug VizCraft interactive visualization playgrounds. USE WHEN: the workspace depends on 'vizcraft' in package.json, has src/registry.ts with PluginCategory[], has src/components/plugin-kit/, or has scripts/generate-plugin.js. USE FOR: creating new plugins, building VizCraft scenes (nodes, edges, signals, overlays), writing animation hooks, defining concept pills and InfoModal content, configuring Redux slices for plugin state, styling plugins with dark-theme SCSS, managing signal persistence and multi-hop chains, wiring plugins into the category registry. DO NOT USE FOR: non-VizCraft React projects, general Redux questions unrelated to plugins."
4
+ ---
5
+
6
+ # VizCraft Playground Skill
7
+
8
+ Use this skill whenever working inside a VizCraft playground — any project scaffolded by `create-vizcraft-playground` or matching these markers:
9
+
10
+ - `package.json` depends on `vizcraft`
11
+ - `src/registry.ts` exports `PluginCategory[]`
12
+ - `src/components/plugin-kit/` exists
13
+ - `src/types/ModelPlugin.ts` defines `DemoPlugin`
14
+ - `scripts/generate-plugin.js` exists
15
+
16
+ ## Playground architecture
17
+
18
+ A VizCraft playground is a **plugin-based interactive visualization platform**.
19
+
20
+ | Layer | Files | Purpose |
21
+ |-------|-------|---------|
22
+ | Config | `src/playground.config.ts` | Branding: title, subtitle, accent |
23
+ | Registry | `src/registry.ts` | Single source of truth for categories + plugins |
24
+ | Store | `src/store/store.ts` | Redux store from `pluginReducerMap` |
25
+ | Simulation | `src/store/slices/simulationSlice.ts` | Shared `currentStep`, `passCount`, `isPlaying` |
26
+ | Shell | `src/components/Shell.tsx` | Step lifecycle, restart, nav |
27
+ | Plugin Kit | `src/components/plugin-kit/` | Reusable UI building blocks |
28
+ | Plugins | `src/plugins/{name}/` | Self-contained demos |
29
+
30
+ ## Plugin anatomy
31
+
32
+ Every plugin lives in `src/plugins/{kebab-name}/` with exactly six files.
33
+
34
+ | File | Naming | Purpose |
35
+ |------|--------|---------|
36
+ | `index.ts` | — | DemoPlugin export, steps, restart config |
37
+ | `{camelName}Slice.ts` | `eventStreamingSlice.ts` | Redux slice: state type, reducers, actions |
38
+ | `use{PascalName}Animation.ts` | `useEventStreamingAnimation.ts` | Step-driven animation orchestration |
39
+ | `main.tsx` | — | React component rendering VizCraft scene |
40
+ | `concepts.tsx` | — | ConceptKey type + ConceptDefinition record |
41
+ | `main.scss` | — | Plugin-specific dark-theme styles |
42
+
43
+ ### Scaffolding
44
+
45
+ ```bash
46
+ npm run generate my-plugin --category "Category Name"
47
+ # or shorthand:
48
+ npm run generate my-plugin -c "Category Name"
49
+ ```
50
+
51
+ If the category exists, the plugin is appended into its `plugins` array.
52
+ If the category does not exist, a new entry is created in the `categories` array.
53
+
54
+ ## DemoPlugin interface
55
+
56
+ ```typescript
57
+ interface DemoPlugin<State, Actions, TRootState, TDispatch> {
58
+ id: string; // kebab-case
59
+ name: string; // Display name
60
+ description: string; // One-liner
61
+ initialState: State;
62
+ reducer: Reducer<State, Actions>;
63
+ Component: React.FC<{ onAnimationComplete?: () => void }>;
64
+ Controls?: React.FC;
65
+ restartConfig?: { text?: string; color?: string };
66
+ getSteps: (state: State) => (string | DemoStep)[];
67
+ init: (dispatch: TDispatch) => void;
68
+ selector: (state: TRootState) => State;
69
+ }
70
+
71
+ interface DemoStep {
72
+ label: string;
73
+ autoAdvance?: boolean;
74
+ nextButtonText?: string;
75
+ processingText?: string;
76
+ nextButtonColor?: string;
77
+ }
78
+ ```
79
+
80
+ The `selector` uses a local root type: `type LocalRootState = { [camelName]: State }`.
81
+
82
+ ## Plugin-kit components
83
+
84
+ Import from `../../components/plugin-kit`. All classes use the `vc-` prefix.
85
+
86
+ ### PluginLayout
87
+
88
+ Two-column layout: toolbar → body (canvas + optional sidebar).
89
+
90
+ ```tsx
91
+ <PluginLayout
92
+ toolbar={<ConceptPills pills={pills} onOpen={openConcept} />}
93
+ canvas={<div className="my-stage"><StageHeader .../><CanvasStage canvasRef={ref} /></div>}
94
+ sidebar={<SidePanel>...</SidePanel>}
95
+ />
96
+ ```
97
+
98
+ Props: `toolbar?: ReactNode`, `canvas: ReactNode`, `sidebar?: ReactNode`, `className?`.
99
+ When `sidebar` is omitted, the canvas takes full width.
100
+
101
+ ### CanvasStage
102
+
103
+ Wrapper for the VizCraft mount point with a dot-grid background.
104
+
105
+ ```tsx
106
+ <CanvasStage canvasRef={containerRef}>
107
+ <VizInfoBeacon ... /> {/* Optional overlays */}
108
+ </CanvasStage>
109
+ ```
110
+
111
+ Props: `canvasRef?: Ref<HTMLDivElement>`, `children?: ReactNode`, `className?`.
112
+
113
+ ### ConceptPills
114
+
115
+ Horizontal row of clickable info pills.
116
+
117
+ ```tsx
118
+ const pills: PillDef[] = [
119
+ { key: "kafka", label: "Kafka", color: "#7dd3fc", borderColor: "#0ea5e9" },
120
+ ];
121
+ <ConceptPills pills={pills} onOpen={openConcept} />
122
+ ```
123
+
124
+ Props: `pills: PillDef[]`, `onOpen: (key: string) => void`, `className?`.
125
+
126
+ **PillDef:** `{ key, label, variant?, color?, borderColor? }`.
127
+ Use inline `color`/`borderColor` for pill colours (not variant classes).
128
+
129
+ ### useConceptModal
130
+
131
+ Hook that manages the InfoModal lifecycle.
132
+
133
+ ```tsx
134
+ const { openConcept, closeConcept, ConceptModal } = useConceptModal<ConceptKey>(concepts);
135
+ // Render <ConceptModal /> anywhere in JSX
136
+ ```
137
+
138
+ Generic over `K extends string`. Returns `{ activeConcept, openConcept, closeConcept, ConceptModal }`.
139
+
140
+ ### StageHeader
141
+
142
+ Title + subtitle + right-aligned stats area.
143
+
144
+ ```tsx
145
+ <StageHeader title="ECS Autoscaling" subtitle="Watch containers scale">
146
+ <StatBadge label="Phase" value="alarm" color="#fda4af" />
147
+ </StageHeader>
148
+ ```
149
+
150
+ Props: `title: string`, `subtitle?: string`, `children?: ReactNode` (stat badges), `className?`.
151
+
152
+ ### StatBadge
153
+
154
+ Small stat chip: uppercase label + bold coloured value.
155
+
156
+ ```tsx
157
+ <StatBadge label="Tasks" value="3/5" color="#86efac" />
158
+ ```
159
+
160
+ Props: `label: string`, `value: ReactNode`, `color?: string`, `className?`.
161
+
162
+ ### SidePanel and SideCard
163
+
164
+ Scrollable sidebar with labelled cards.
165
+
166
+ ```tsx
167
+ <SidePanel>
168
+ <SideCard label="What's happening" variant="explanation">
169
+ <p>{explanation}</p>
170
+ </SideCard>
171
+ </SidePanel>
172
+ ```
173
+
174
+ SideCard props: `label?`, `heading?`, `sub?`, `variant?`, `children`, `className?`.
175
+
176
+ ### VizInfoBeacon
177
+
178
+ Floating info indicator over a scene region. Opens concepts on click.
179
+
180
+ ```tsx
181
+ <VizInfoBeacon
182
+ viewWidth={900} viewHeight={600}
183
+ hoverRegion={{ x: 100, y: 200, width: 140, height: 60 }}
184
+ indicatorPosition={{ x: 170, y: 195 }}
185
+ ariaLabel="Learn about Producer"
186
+ onActivate={() => openConcept("producer")}
187
+ />
188
+ ```
189
+
190
+ ### InfoModal
191
+
192
+ Full-screen modal for concept definitions. Typically managed by `useConceptModal`, not used directly.
193
+
194
+ ```typescript
195
+ interface InfoModalSection {
196
+ title: string;
197
+ content: ReactNode;
198
+ accent?: string;
199
+ }
200
+ ```
201
+
202
+ ## VizCraft builder API
203
+
204
+ ### Scene setup
205
+
206
+ ```typescript
207
+ import { viz, type PanZoomController, type SignalOverlayParams } from "vizcraft";
208
+
209
+ const b = viz().view(width, height);
210
+ ```
211
+
212
+ ### Nodes
213
+
214
+ ```typescript
215
+ b.node("my-node")
216
+ .at(x, y)
217
+ .rect(w, h, cornerRadius) // or .circle(radius)
218
+ .fill(color)
219
+ .stroke(color, width)
220
+ .label(text, { fill, fontSize, fontWeight, dy })
221
+ .tooltip({ title, sections: [{ label, value }] })
222
+ .badge(label, { position })
223
+ .onClick(handler);
224
+ ```
225
+
226
+ ### Edges
227
+
228
+ ```typescript
229
+ b.edge("from-id", "to-id", "edge-id")
230
+ .stroke(color, width)
231
+ .arrow(true)
232
+ .label(text, { fill, fontSize })
233
+ .dashed()
234
+ .animate("flow", { duration: "2s" });
235
+ ```
236
+
237
+ ### Overlays
238
+
239
+ ```typescript
240
+ b.overlay((o) => {
241
+ o.add("rect", { x, y, w, h, rx, ry, fill, stroke, strokeWidth, opacity }, { key, className });
242
+ o.add("text", { x, y, text, fill, fontSize, fontWeight }, { key });
243
+ o.add("circle", { x, y, r, fill, stroke }, { key });
244
+ o.add("signal", signalParams, { key });
245
+ });
246
+ ```
247
+
248
+ ### Mounting
249
+
250
+ ```typescript
251
+ const pz = scene.mount(containerEl, {
252
+ autoplay: true,
253
+ panZoom: true,
254
+ initialZoom: saved?.zoom ?? 1,
255
+ initialPan: saved?.pan ?? { x: 0, y: 0 },
256
+ });
257
+ // pz.getState() → { zoom, pan: { x, y } }
258
+ ```
259
+
260
+ Always preserve pan/zoom across rebuilds:
261
+
262
+ ```typescript
263
+ const pzRef = useRef<PanZoomController | null>(null);
264
+
265
+ useLayoutEffect(() => {
266
+ const saved = pzRef.current?.getState() ?? null;
267
+ builderRef.current?.destroy();
268
+ builderRef.current = scene;
269
+ pzRef.current = scene.mount(containerRef.current, {
270
+ autoplay: true, panZoom: true,
271
+ initialZoom: saved?.zoom ?? 1,
272
+ initialPan: saved?.pan ?? { x: 0, y: 0 },
273
+ }) ?? null;
274
+ }, [scene]);
275
+
276
+ useEffect(() => {
277
+ return () => { builderRef.current?.destroy(); };
278
+ }, []);
279
+ ```
280
+
281
+ ## Signals: the core animation primitive
282
+
283
+ A signal is an animated ball traveling along an edge between nodes.
284
+
285
+ ```typescript
286
+ type Signal = { id: string } & SignalOverlayParams;
287
+
288
+ // SignalOverlayParams:
289
+ {
290
+ from: string; // start node
291
+ to: string; // end node
292
+ progress: number; // 0 = at from, 1 = at to
293
+ magnitude?: number; // ball size multiplier
294
+ color?: string;
295
+ }
296
+ ```
297
+
298
+ ### Persistent vs transient signals
299
+
300
+ **This is critical.** When signals represent persistent state (events sitting in a partition, messages stored in a queue, data at rest), they must remain visible after animation completes. When signals represent transient events (a request in flight), they disappear after reaching their destination.
301
+
302
+ **Transient** — disappear after arrival:
303
+ ```typescript
304
+ animateChain(hops, 500, () => setSignals([]));
305
+ ```
306
+
307
+ **Persistent** — stay visible at destination:
308
+ ```typescript
309
+ // After animation, keep as resting circles
310
+ persistedRef.current.push({
311
+ id: `rest-${Date.now()}`,
312
+ nodeId: "partition-0",
313
+ offsetX: 8, offsetY: -4,
314
+ });
315
+ // Render resting signals as circle overlays
316
+ b.overlay((o) => {
317
+ persistedRef.current.forEach(s => {
318
+ const pos = nodePositions[s.nodeId];
319
+ o.add("circle", {
320
+ x: pos.x + s.offsetX, y: pos.y + s.offsetY,
321
+ r: 5, fill: "#fbbf24",
322
+ }, { key: s.id });
323
+ });
324
+ });
325
+ ```
326
+
327
+ Use `useRef` to carry persistent signal state across step changes. Maintain a position map for nodes where signals rest.
328
+
329
+ ### Multi-hop signal chains
330
+
331
+ Signals natively travel one edge. For multi-hop chains, use `requestAnimationFrame`:
332
+
333
+ ```typescript
334
+ const animateSignalChain = useCallback((
335
+ hops: { from: string; to: string }[],
336
+ durationPerHop: number,
337
+ onDone: () => void,
338
+ options?: { keepFinal?: boolean; extra?: Signal[] },
339
+ ) => {
340
+ const extra = options?.extra ?? [];
341
+ const totalDuration = hops.length * durationPerHop;
342
+ const sigId = `chain-${Date.now()}`;
343
+ const startTime = performance.now();
344
+
345
+ const step = (now: number) => {
346
+ const rawP = Math.min((now - startTime) / totalDuration, 1);
347
+ const progress = rawP * hops.length;
348
+ setSignals([...extra, { id: sigId, chain: hops, progress, magnitude: 1 }]);
349
+ if (rawP < 1) {
350
+ rafRef.current = requestAnimationFrame(step);
351
+ } else {
352
+ if (!options?.keepFinal) setSignals([...extra]);
353
+ onDone();
354
+ }
355
+ };
356
+ rafRef.current = requestAnimationFrame(step);
357
+ }, []);
358
+ ```
359
+
360
+ ### Parallel signals (fan-out / fan-in)
361
+
362
+ ```typescript
363
+ const animateSignalsParallel = useCallback((
364
+ pairs: { from: string; to: string }[],
365
+ duration: number,
366
+ onDone: () => void,
367
+ options?: { extra?: Signal[] },
368
+ ) => {
369
+ const start = performance.now();
370
+ const sigs = pairs.map((p, i) => ({ id: `par-${i}`, from: p.from, to: p.to, progress: 0 }));
371
+ const step = (now: number) => {
372
+ const p = Math.min((now - start) / duration, 1);
373
+ setSignals([...(options?.extra ?? []), ...sigs.map(s => ({ ...s, progress: p }))]);
374
+ if (p < 1) rafRef.current = requestAnimationFrame(step);
375
+ else onDone();
376
+ };
377
+ rafRef.current = requestAnimationFrame(step);
378
+ }, []);
379
+ ```
380
+
381
+ ## Animation hook pattern
382
+
383
+ Every plugin has a `use{PascalName}Animation` hook. Follow this exact structure:
384
+
385
+ ```typescript
386
+ export const useMyPluginAnimation = (onAnimationComplete?: () => void) => {
387
+ const dispatch = useDispatch();
388
+ const { currentStep } = useSelector((s: RootState) => s.simulation);
389
+ const runtime = useSelector((s: RootState) => s.myPlugin);
390
+ const [signals, setSignals] = useState<Signal[]>([]);
391
+ const [animPhase, setAnimPhase] = useState<string>("idle");
392
+
393
+ // Refs
394
+ const rafRef = useRef<number>(undefined!);
395
+ const timeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
396
+ const onCompleteRef = useRef(onAnimationComplete);
397
+ onCompleteRef.current = onAnimationComplete;
398
+
399
+ const cleanup = useCallback(() => {
400
+ cancelAnimationFrame(rafRef.current);
401
+ timeoutsRef.current.forEach(clearTimeout);
402
+ timeoutsRef.current = [];
403
+ setSignals([]);
404
+ }, []);
405
+
406
+ const sleep = useCallback((ms: number) =>
407
+ new Promise<void>(resolve => {
408
+ const id = setTimeout(resolve, ms);
409
+ timeoutsRef.current.push(id);
410
+ }), []);
411
+
412
+ const finish = useCallback(() => onCompleteRef.current?.(), []);
413
+
414
+ // Step orchestration
415
+ useEffect(() => {
416
+ cleanup();
417
+ const run = async () => {
418
+ switch (currentStep) {
419
+ case 0:
420
+ dispatch(reset());
421
+ finish();
422
+ break;
423
+ case 1:
424
+ dispatch(patchState({ phase: "running", explanation: "..." }));
425
+ // animate...
426
+ await sleep(1200);
427
+ finish();
428
+ break;
429
+ // more steps...
430
+ default:
431
+ finish();
432
+ }
433
+ };
434
+ run();
435
+ return cleanup;
436
+ }, [currentStep]);
437
+
438
+ return { runtime, currentStep, signals, animPhase };
439
+ };
440
+ ```
441
+
442
+ Key rules:
443
+ - Always call `cleanup()` at the start of each step.
444
+ - Always call `finish()` when a step's animation is done — this enables the Next button.
445
+ - Use `onCompleteRef` to avoid stale closure issues.
446
+ - The `useEffect` dependency should be `[currentStep]`.
447
+ - For steps with `autoAdvance: true`, the Shell auto-advances when `finish()` fires.
448
+ - Return `signals` so the component can pass them to the overlay.
449
+
450
+ ## Redux slice pattern
451
+
452
+ Use the `patchState` pattern for flexible updates:
453
+
454
+ ```typescript
455
+ interface MyPluginState {
456
+ phase: "idle" | "running" | "done";
457
+ explanation: string;
458
+ hotZones: string[];
459
+ // domain-specific fields
460
+ }
461
+
462
+ const slice = createSlice({
463
+ name: "myPlugin",
464
+ initialState,
465
+ reducers: {
466
+ patchState(state, action: PayloadAction<Partial<MyPluginState>>) {
467
+ Object.assign(state, action.payload);
468
+ },
469
+ reset() { return initialState; },
470
+ // domain-specific reducers as needed
471
+ },
472
+ });
473
+ ```
474
+
475
+ Always export `initialState` and `reset` — the plugin registration and Shell need them.
476
+
477
+ ## Concepts file pattern
478
+
479
+ ```typescript
480
+ export type ConceptKey = "concept-a" | "concept-b";
481
+
482
+ export const concepts: Record<ConceptKey, ConceptDefinition> = {
483
+ "concept-a": {
484
+ title: "Concept A",
485
+ subtitle: "Brief description",
486
+ accentColor: "#60a5fa",
487
+ sections: [
488
+ { title: "Section heading", accent: "#60a5fa", content: <p>...</p> },
489
+ ],
490
+ aside: <div>Optional sidebar content</div>, // optional
491
+ },
492
+ };
493
+ ```
494
+
495
+ ## SCSS conventions
496
+
497
+ ```scss
498
+ .{plugin-name}-root {
499
+ // CSS custom properties for the plugin
500
+ --{plugin-name}-bg: #020617;
501
+ --{plugin-name}-panel: rgba(7, 17, 34, 0.88);
502
+ --{plugin-name}-border: rgba(148, 163, 184, 0.18);
503
+ --{plugin-name}-text: #e2e8f0;
504
+
505
+ display: flex;
506
+ flex-direction: column;
507
+ width: 100%;
508
+ height: 100%;
509
+ overflow: hidden;
510
+ color: var(--{plugin-name}-text);
511
+ background:
512
+ radial-gradient(circle at top left, rgba(59, 130, 246, 0.14), transparent 28%),
513
+ radial-gradient(circle at bottom right, rgba(20, 184, 166, 0.12), transparent 30%),
514
+ linear-gradient(180deg, #020617 0%, #071325 100%);
515
+ }
516
+
517
+ .{plugin-name}-stage {
518
+ background: var(--{plugin-name}-panel);
519
+ border: 1px solid var(--{plugin-name}-border);
520
+ border-radius: 24px;
521
+ padding: 1rem;
522
+ display: flex;
523
+ flex-direction: column;
524
+ min-height: 0;
525
+ }
526
+ ```
527
+
528
+ Dark theme defaults: bg `#0f172a`, text `#e2e8f0`, muted `#94a3b8`, borders `rgba(148,163,184,0.1)`.
529
+ Shared plugin-kit classes use `vc-` prefix. Per-plugin classes use the plugin name prefix.
530
+
531
+ ## Hot zone pattern
532
+
533
+ Use `hotZones: string[]` in state to highlight active nodes:
534
+
535
+ ```typescript
536
+ const hot = (zone: string) => runtime.hotZones.includes(zone);
537
+
538
+ b.node("broker")
539
+ .fill(hot("broker") ? "#1e40af" : "#0f172a")
540
+ .stroke(hot("broker") ? "#60a5fa" : "#334155", 2);
541
+ ```
542
+
543
+ This makes the active part of the architecture visually dominant in each step.
544
+
545
+ ## Registry wiring
546
+
547
+ Plugins are registered in `src/registry.ts`. The `generate-plugin` script handles this automatically with `--category`.
548
+
549
+ ```typescript
550
+ import MyPlugin from "./plugins/my-plugin";
551
+
552
+ export const categories: PluginCategory[] = [
553
+ {
554
+ id: "my-category", // URL slug
555
+ name: "My Category", // Display name
556
+ description: "...",
557
+ accent: "#3b82f6", // Card colour
558
+ plugins: [MyPlugin],
559
+ },
560
+ ];
561
+ ```
562
+
563
+ The store, routes, and landing page derive from this single source of truth.
564
+
565
+ ## Common mistakes to avoid
566
+
567
+ - Forgetting to call `finish()` — step will never advance.
568
+ - Not cleaning up RAF/timeouts — animations bleed across steps.
569
+ - Using stale closures for `onAnimationComplete` — use `.current` ref.
570
+ - Signals disappearing when they should persist — use `useRef` for resting signals.
571
+ - Hardcoding node positions twice (in builder and overlay) — maintain a single position map.
572
+ - Not preserving pan/zoom on scene rebuild — save/restore via `pzRef`.
573
+ - Using `variant` for pill colours instead of inline `color`/`borderColor`.
@@ -0,0 +1,59 @@
1
+ # VizCraft Playground
2
+
3
+ Explore interactive visualizations.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ Open [http://localhost:5173](http://localhost:5173) in your browser.
13
+
14
+ ## Personalise
15
+
16
+ Run the init wizard to set your playground's title, subtitle, and accent colour:
17
+
18
+ ```bash
19
+ npm run init
20
+ ```
21
+
22
+ Or edit `src/playground.config.ts` directly.
23
+
24
+ ## Adding a Plugin
25
+
26
+ Generate a new plugin with the scaffolding tool:
27
+
28
+ ```bash
29
+ npm run generate <plugin-name> --category "Category Name"
30
+ ```
31
+
32
+ For example:
33
+
34
+ ```bash
35
+ npm run generate supply-demand --category "Microeconomics"
36
+ ```
37
+
38
+ This creates `src/plugins/<plugin-name>/` with six files and wires it into the registry automatically. If the category doesn't exist yet, it will be created.
39
+
40
+ ### Plugin structure
41
+
42
+ | File | Purpose |
43
+ |------|---------|
44
+ | `index.ts` | Plugin registration (id, name, steps, reducer) |
45
+ | `*Slice.ts` | Redux Toolkit slice for local state |
46
+ | `use*Animation.ts` | Step orchestration & signal animation |
47
+ | `main.tsx` | React component using plugin-kit |
48
+ | `main.scss` | Plugin-specific styles |
49
+ | `concepts.tsx` | Clickable info-pill definitions |
50
+
51
+ See the **hello-world** plugin for a minimal working example.
52
+
53
+ ## Tech Stack
54
+
55
+ - [React](https://react.dev/) + [TypeScript](https://www.typescriptlang.org/)
56
+ - [Vite](https://vitejs.dev/)
57
+ - [Redux Toolkit](https://redux-toolkit.js.org/)
58
+ - [vizcraft](https://www.npmjs.com/package/vizcraft) (SVG visualization engine)
59
+ - SCSS
@@ -0,0 +1,23 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>VizCraft Playground</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "vizcraft-playground",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "generate": "node scripts/generate-plugin.js",
12
+ "init": "node scripts/init-playground.js"
13
+ },
14
+ "dependencies": {
15
+ "@reduxjs/toolkit": "^2.11.1",
16
+ "react": "^19.2.0",
17
+ "react-dom": "^19.2.0",
18
+ "react-redux": "^9.2.0",
19
+ "react-router-dom": "^7.10.1",
20
+ "vizcraft": "^1.15.0"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.39.1",
24
+ "@types/node": "^24.10.1",
25
+ "@types/react": "^19.2.5",
26
+ "@types/react-dom": "^19.2.3",
27
+ "@vitejs/plugin-react": "^5.1.1",
28
+ "eslint": "^9.39.1",
29
+ "eslint-plugin-react-hooks": "^7.0.1",
30
+ "eslint-plugin-react-refresh": "^0.4.24",
31
+ "globals": "^16.5.0",
32
+ "sass-embedded": "^1.88.0",
33
+ "typescript": "~5.8.3",
34
+ "typescript-eslint": "^8.29.0",
35
+ "vite": "^7.2.7"
36
+ }
37
+ }