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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/dist/animations.d.ts +22 -0
- package/dist/animations.js +30 -0
- package/dist/builder.d.ts +55 -0
- package/dist/builder.js +742 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +15 -0
- package/dist/overlays.d.ts +38 -0
- package/dist/overlays.js +139 -0
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +67 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +1 -0
- package/package.json +39 -0
- package/src/animations.ts +53 -0
- package/src/builder.ts +946 -0
- package/src/index.test.ts +17 -0
- package/src/index.ts +5 -0
- package/src/overlays.ts +203 -0
- package/src/styles.ts +67 -0
- package/src/types.ts +83 -0
- package/tsconfig.json +11 -0
|
@@ -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
package/src/overlays.ts
ADDED
|
@@ -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
|
+
};
|