vizcraft 0.1.1

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.
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { viz } from './index';
3
+
4
+ describe('vizcraft core', () => {
5
+ it('exports viz builder', () => {
6
+ expect(viz).toBeDefined();
7
+ expect(typeof viz).toBe('function');
8
+ });
9
+
10
+ it('creates a builder instance', () => {
11
+ const builder = viz();
12
+ expect(builder).toBeDefined();
13
+ // Verify default viewbox
14
+ const view = builder._getViewBox();
15
+ expect(view).toEqual({ w: 800, h: 600 });
16
+ });
17
+ });
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './builder';
3
+ export * from './styles';
4
+ export * from './animations';
5
+ export * from './overlays';
@@ -0,0 +1,203 @@
1
+ import type { VizNode, VizEdge, VizOverlaySpec, VizScene } from './types';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export interface CoreOverlayRenderContext<T = any> {
5
+ spec: VizOverlaySpec<T>;
6
+ nodesById: Map<string, VizNode>;
7
+ edgesById: Map<string, VizEdge>;
8
+ scene: VizScene;
9
+ }
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ export interface CoreOverlayRenderer<T = any> {
13
+ render: (ctx: CoreOverlayRenderContext<T>) => string;
14
+ update?: (ctx: CoreOverlayRenderContext<T>, container: SVGGElement) => void;
15
+ }
16
+
17
+ export class CoreOverlayRegistry {
18
+ private overlays = new Map<string, CoreOverlayRenderer>();
19
+
20
+ register(id: string, renderer: CoreOverlayRenderer) {
21
+ this.overlays.set(id, renderer);
22
+ return this;
23
+ }
24
+
25
+ get(id: string) {
26
+ return this.overlays.get(id);
27
+ }
28
+ }
29
+
30
+ // Built-in Overlay: Signal
31
+ export const coreSignalOverlay: CoreOverlayRenderer<{
32
+ from: string;
33
+ to: string;
34
+ progress: number;
35
+ magnitude?: number;
36
+ }> = {
37
+ render: ({ spec, nodesById }) => {
38
+ const { from, to, progress } = spec.params;
39
+ const start = nodesById.get(from);
40
+ const end = nodesById.get(to);
41
+
42
+ if (!start || !end) return '';
43
+
44
+ const x = start.pos.x + (end.pos.x - start.pos.x) * progress;
45
+ const y = start.pos.y + (end.pos.y - start.pos.y) * progress;
46
+
47
+ let v = Math.abs(spec.params.magnitude ?? 1);
48
+ if (v > 1) v = 1;
49
+ const r = 2 + v * 4;
50
+
51
+ const className = spec.className ?? 'viz-signal';
52
+
53
+ return `
54
+ <g transform="translate(${x}, ${y})">
55
+ <g class="${className}">
56
+ <circle r="10" fill="transparent" stroke="none" />
57
+ <circle r="${r}" class="viz-signal-shape" />
58
+ </g>
59
+ </g>
60
+ `;
61
+ },
62
+ };
63
+
64
+ // Built-in Overlay: Grid Labels
65
+ export const coreGridLabelsOverlay: CoreOverlayRenderer<{
66
+ colLabels?: Record<number, string>;
67
+ rowLabels?: Record<number, string>;
68
+ yOffset?: number;
69
+ xOffset?: number;
70
+ }> = {
71
+ render: ({ spec, scene }) => {
72
+ const grid = scene.grid;
73
+ if (!grid) return '';
74
+
75
+ const { w, h } = scene.viewBox;
76
+ const { colLabels, rowLabels, yOffset = 20, xOffset = 20 } = spec.params;
77
+
78
+ // Safer string rendering for overlay to avoid weird spacing if grid missing
79
+ const cellW = (w - grid.padding.x * 2) / grid.cols;
80
+ const cellH = (h - grid.padding.y * 2) / grid.rows;
81
+
82
+ let output = '';
83
+
84
+ if (colLabels) {
85
+ Object.entries(colLabels).forEach(([colStr, text]) => {
86
+ const col = parseInt(colStr, 10);
87
+ const x = grid.padding.x + col * cellW + cellW / 2;
88
+ const cls = spec.className || 'viz-grid-label';
89
+ output += `<text x="${x}" y="${yOffset}" class="${cls}" text-anchor="middle">${text}</text>`;
90
+ });
91
+ }
92
+
93
+ if (rowLabels) {
94
+ Object.entries(rowLabels).forEach(([rowStr, text]) => {
95
+ const row = parseInt(rowStr, 10);
96
+ const y = grid.padding.y + row * cellH + cellH / 2;
97
+ const cls = spec.className || 'viz-grid-label';
98
+ output += `<text x="${xOffset}" y="${y}" dy=".35em" class="${cls}" text-anchor="middle">${text}</text>`;
99
+ });
100
+ }
101
+
102
+ return output;
103
+ },
104
+ };
105
+
106
+ // ... (OverlayRegistry and other exports remain unchanged) ...
107
+
108
+ interface DataPoint {
109
+ id: string;
110
+ currentNodeId: string;
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ [key: string]: any;
113
+ }
114
+
115
+ // Built-in Overlay: Data Points
116
+ export const coreDataPointOverlay: CoreOverlayRenderer<{
117
+ points: DataPoint[];
118
+ }> = {
119
+ render: ({ spec, nodesById }) => {
120
+ const { points } = spec.params;
121
+ let output = '';
122
+
123
+ points.forEach((point) => {
124
+ const node = nodesById.get(point.currentNodeId);
125
+ if (!node) return;
126
+
127
+ const idNum = parseInt(point.id.split('-')[1] || '0', 10);
128
+ const offsetX = ((idNum % 5) - 2) * 10;
129
+ const offsetY = ((idNum % 3) - 1) * 10;
130
+
131
+ const x = node.pos.x + offsetX;
132
+ const y = node.pos.y + offsetY;
133
+
134
+ const cls = spec.className ?? 'viz-data-point';
135
+ // Important: Add data-id so we can find it later in update()
136
+ output += `<circle data-id="${point.id}" cx="${x}" cy="${y}" r="6" class="${cls}" />`;
137
+ });
138
+
139
+ return output;
140
+ },
141
+ update: ({ spec, nodesById }, container) => {
142
+ const { points } = spec.params;
143
+ const svgNS = 'http://www.w3.org/2000/svg';
144
+
145
+ // 1. Map existing elements by data-id
146
+ const existingMap = new Map<string, SVGElement>();
147
+ Array.from(container.children).forEach((child) => {
148
+ if (child.tagName === 'circle') {
149
+ const id = child.getAttribute('data-id');
150
+ if (id) existingMap.set(id, child as SVGElement);
151
+ }
152
+ });
153
+
154
+ const processedIds = new Set<string>();
155
+
156
+ // 2. Create or Update Points
157
+ points.forEach((point) => {
158
+ const node = nodesById.get(point.currentNodeId);
159
+ if (!node) return;
160
+
161
+ processedIds.add(point.id);
162
+
163
+ const idNum = parseInt(point.id.split('-')[1] || '0', 10);
164
+ const offsetX = ((idNum % 5) - 2) * 10;
165
+ const offsetY = ((idNum % 3) - 1) * 10;
166
+
167
+ const x = node.pos.x + offsetX;
168
+ const y = node.pos.y + offsetY;
169
+
170
+ let circle = existingMap.get(point.id);
171
+
172
+ if (!circle) {
173
+ // Create new
174
+ circle = document.createElementNS(svgNS, 'circle');
175
+ circle.setAttribute('data-id', point.id);
176
+ circle.setAttribute('r', '6');
177
+ container.appendChild(circle);
178
+ }
179
+
180
+ // Update attrs (this triggers CSS transition if class has it)
181
+ circle.setAttribute('cx', String(x));
182
+ circle.setAttribute('cy', String(y));
183
+
184
+ const cls = spec.className ?? 'viz-data-point';
185
+ // Only set class if different to avoid potential re-flows (though usually fine)
186
+ if (circle.getAttribute('class') !== cls) {
187
+ circle.setAttribute('class', cls);
188
+ }
189
+ });
190
+
191
+ // 3. Remove stale points
192
+ existingMap.forEach((el, id) => {
193
+ if (!processedIds.has(id)) {
194
+ el.remove();
195
+ }
196
+ });
197
+ },
198
+ };
199
+
200
+ export const defaultCoreOverlayRegistry = new CoreOverlayRegistry()
201
+ .register('signal', coreSignalOverlay)
202
+ .register('grid-labels', coreGridLabelsOverlay)
203
+ .register('data-points', coreDataPointOverlay);
package/src/styles.ts ADDED
@@ -0,0 +1,67 @@
1
+ export const DEFAULT_VIZ_CSS = `
2
+ .viz-canvas {
3
+ width: 100%;
4
+ height: 100%;
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ }
9
+
10
+ .viz-canvas svg {
11
+ width: 100%;
12
+ height: 100%;
13
+ overflow: visible;
14
+ }
15
+
16
+ /* Keyframes */
17
+ @keyframes vizFlow {
18
+ from {
19
+ stroke-dashoffset: 20;
20
+ }
21
+ to {
22
+ stroke-dashoffset: 0;
23
+ }
24
+ }
25
+
26
+ /* Animation Classes */
27
+
28
+ /* Flow Animation (Dashed line moving) */
29
+ .viz-anim-flow .viz-edge {
30
+ stroke-dasharray: 5, 5;
31
+ animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;
32
+ }
33
+
34
+ /* Node Transition */
35
+ .viz-node-group {
36
+ transition: transform 0.3s ease-out, opacity 0.3s ease-out;
37
+ }
38
+
39
+ /* Overlay Classes */
40
+ .viz-grid-label {
41
+ fill: #6B7280;
42
+ font-size: 14px;
43
+ font-weight: 600;
44
+ opacity: 1;
45
+ }
46
+
47
+ .viz-signal {
48
+ fill: #3B82F6;
49
+ cursor: pointer;
50
+ pointer-events: all;
51
+ transition: transform 0.2s ease-out, fill 0.2s ease-out;
52
+ }
53
+
54
+ .viz-signal .viz-signal-shape {
55
+ fill: inherit;
56
+ }
57
+
58
+ .viz-signal:hover {
59
+ fill: #60A5FA;
60
+ transform: scale(1.5);
61
+ }
62
+
63
+ .viz-data-point {
64
+ fill: #F59E0B;
65
+ transition: cx 0.3s ease-out, cy 0.3s ease-out;
66
+ }
67
+ `;
package/src/types.ts ADDED
@@ -0,0 +1,83 @@
1
+ export type Vec2 = { x: number; y: number };
2
+
3
+ export type NodeShape =
4
+ | { kind: 'circle'; r: number }
5
+ | { kind: 'rect'; w: number; h: number; rx?: number }
6
+ | { kind: 'diamond'; w: number; h: number };
7
+
8
+ export type NodeLabel = {
9
+ text: string;
10
+ dx?: number;
11
+ dy?: number;
12
+ className?: string;
13
+ };
14
+
15
+ export type AnimationDuration = `${number}s`;
16
+
17
+ export interface AnimationConfig {
18
+ duration?: AnimationDuration;
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ [key: string]: any;
21
+ }
22
+
23
+ // Generic animation specification (request)
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ export interface VizAnimSpec<T = any> {
26
+ id: string; // e.g. "flow"
27
+ params?: T;
28
+ when?: boolean; // Condition gate
29
+ }
30
+
31
+ export interface VizNode {
32
+ id: string;
33
+ pos: Vec2;
34
+ shape: NodeShape;
35
+ label?: NodeLabel;
36
+ className?: string; // e.g. "active", "input-layer"
37
+ data?: unknown; // User payload
38
+ onClick?: (id: string, node: VizNode) => void;
39
+ animations?: VizAnimSpec[];
40
+ }
41
+
42
+ export interface EdgeLabel {
43
+ text: string;
44
+ position: 'start' | 'mid' | 'end'; // Simplified for now
45
+ className?: string;
46
+ dx?: number;
47
+ dy?: number;
48
+ }
49
+
50
+ export interface VizEdge {
51
+ id: string;
52
+ from: string;
53
+ to: string;
54
+ label?: EdgeLabel;
55
+ markerEnd?: 'arrow' | 'none';
56
+ className?: string;
57
+ hitArea?: number; // width in px
58
+ data?: unknown;
59
+ onClick?: (id: string, edge: VizEdge) => void;
60
+ animations?: VizAnimSpec[];
61
+ }
62
+
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ export type VizOverlaySpec<T = any> = {
65
+ id: string; // overlay kind, e.g. "signal"
66
+ key?: string; // stable key (optional)
67
+ params: T; // overlay data
68
+ className?: string; // e.g. "viz-signal-red"
69
+ };
70
+
71
+ export interface VizGridConfig {
72
+ cols: number;
73
+ rows: number;
74
+ padding: { x: number; y: number };
75
+ }
76
+
77
+ export type VizScene = {
78
+ viewBox: { w: number; h: number };
79
+ grid?: VizGridConfig;
80
+ nodes: VizNode[];
81
+ edges: VizEdge[];
82
+ overlays?: VizOverlaySpec[];
83
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "jsx": "react-jsx",
7
+ "declaration": true
8
+ },
9
+ "include": ["src/**/*"],
10
+ "exclude": ["node_modules", "dist"]
11
+ }