vizcraft 0.2.2 → 1.0.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/dist/styles.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n text-anchor: middle;\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(0);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n";
1
+ export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n text-anchor: middle;\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(0);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Edge base styling (path elements need explicit fill:none) */\n.viz-edge {\n fill: none;\n stroke: currentColor;\n}\n\n.viz-edge-hit {\n fill: none;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n\n/* Connection ports (hidden by default, shown on node hover) */\n.viz-port {\n fill: #3B82F6;\n stroke: white;\n stroke-width: 1.5;\n opacity: 0;\n pointer-events: all;\n cursor: crosshair;\n transition: opacity 0.15s ease-out;\n}\n.viz-node-group:hover .viz-port {\n opacity: 1;\n}\n";
package/dist/styles.js CHANGED
@@ -39,6 +39,16 @@ export const DEFAULT_VIZ_CSS = `
39
39
  animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;
40
40
  }
41
41
 
42
+ /* Edge base styling (path elements need explicit fill:none) */
43
+ .viz-edge {
44
+ fill: none;
45
+ stroke: currentColor;
46
+ }
47
+
48
+ .viz-edge-hit {
49
+ fill: none;
50
+ }
51
+
42
52
  /* Node Transition */
43
53
  .viz-node-group {
44
54
  transition: transform 0.3s ease-out, opacity 0.3s ease-out;
@@ -72,4 +82,18 @@ export const DEFAULT_VIZ_CSS = `
72
82
  fill: #F59E0B;
73
83
  transition: cx 0.3s ease-out, cy 0.3s ease-out;
74
84
  }
85
+
86
+ /* Connection ports (hidden by default, shown on node hover) */
87
+ .viz-port {
88
+ fill: #3B82F6;
89
+ stroke: white;
90
+ stroke-width: 1.5;
91
+ opacity: 0;
92
+ pointer-events: all;
93
+ cursor: crosshair;
94
+ transition: opacity 0.15s ease-out;
95
+ }
96
+ .viz-node-group:hover .viz-port {
97
+ opacity: 1;
98
+ }
75
99
  `;
package/dist/types.d.ts CHANGED
@@ -15,6 +15,89 @@ export type NodeShape = {
15
15
  kind: 'diamond';
16
16
  w: number;
17
17
  h: number;
18
+ } | {
19
+ kind: 'cylinder';
20
+ w: number;
21
+ h: number;
22
+ arcHeight?: number;
23
+ } | {
24
+ kind: 'hexagon';
25
+ r: number;
26
+ orientation?: 'pointy' | 'flat';
27
+ } | {
28
+ kind: 'ellipse';
29
+ rx: number;
30
+ ry: number;
31
+ } | {
32
+ kind: 'arc';
33
+ r: number;
34
+ startAngle: number;
35
+ endAngle: number;
36
+ closed?: boolean;
37
+ } | {
38
+ kind: 'blockArrow';
39
+ length: number;
40
+ bodyWidth: number;
41
+ headWidth: number;
42
+ headLength: number;
43
+ direction?: 'right' | 'left' | 'up' | 'down';
44
+ } | {
45
+ kind: 'callout';
46
+ w: number;
47
+ h: number;
48
+ rx?: number;
49
+ pointerSide?: 'bottom' | 'top' | 'left' | 'right';
50
+ pointerHeight?: number;
51
+ pointerWidth?: number;
52
+ pointerPosition?: number;
53
+ } | {
54
+ kind: 'cloud';
55
+ w: number;
56
+ h: number;
57
+ } | {
58
+ kind: 'cross';
59
+ size: number;
60
+ barWidth?: number;
61
+ } | {
62
+ kind: 'cube';
63
+ w: number;
64
+ h: number;
65
+ depth?: number;
66
+ } | {
67
+ kind: 'path';
68
+ d: string;
69
+ w: number;
70
+ h: number;
71
+ } | {
72
+ kind: 'document';
73
+ w: number;
74
+ h: number;
75
+ waveHeight?: number;
76
+ } | {
77
+ kind: 'note';
78
+ w: number;
79
+ h: number;
80
+ foldSize?: number;
81
+ } | {
82
+ kind: 'parallelogram';
83
+ w: number;
84
+ h: number;
85
+ skew?: number;
86
+ } | {
87
+ kind: 'star';
88
+ points: number;
89
+ outerR: number;
90
+ innerR?: number;
91
+ } | {
92
+ kind: 'trapezoid';
93
+ topW: number;
94
+ bottomW: number;
95
+ h: number;
96
+ } | {
97
+ kind: 'triangle';
98
+ w: number;
99
+ h: number;
100
+ direction?: 'up' | 'down' | 'left' | 'right';
18
101
  };
19
102
  export type NodeLabel = {
20
103
  text: string;
@@ -48,6 +131,48 @@ export type VizRuntimeEdgeProps = Partial<{
48
131
  strokeDashoffset: number;
49
132
  opacity: number;
50
133
  }>;
134
+ export interface ContainerConfig {
135
+ /** Layout direction for children (default 'free') */
136
+ layout?: 'free' | 'vertical' | 'horizontal';
137
+ /** Padding inside the container */
138
+ padding?: {
139
+ top: number;
140
+ right: number;
141
+ bottom: number;
142
+ left: number;
143
+ };
144
+ /** Whether the container auto-resizes to fit children */
145
+ autoSize?: boolean;
146
+ /** Header height for swimlane-style headers */
147
+ headerHeight?: number;
148
+ }
149
+ /**
150
+ * A named connection port (anchor point) on a node.
151
+ *
152
+ * Ports let edges connect to specific positions on a shape rather than
153
+ * the generic boundary intersection.
154
+ */
155
+ export interface NodePort {
156
+ /** Unique port id within the node (e.g. `'top'`, `'left'`, `'out-1'`). */
157
+ id: string;
158
+ /**
159
+ * Position **relative to the node center** (absolute pixel offset).
160
+ *
161
+ * For example, on a 120×60 rect centered at the node's `pos`:
162
+ * - top port: `{ x: 0, y: -30 }`
163
+ * - right port: `{ x: 60, y: 0 }`
164
+ */
165
+ offset: Vec2;
166
+ /**
167
+ * Optional direction hint for edge routing (outgoing tangent angle in **degrees**).
168
+ *
169
+ * - `0` = right
170
+ * - `90` = down
171
+ * - `180` = left
172
+ * - `270` = up
173
+ */
174
+ direction?: number;
175
+ }
51
176
  export interface VizNode {
52
177
  id: string;
53
178
  pos: Vec2;
@@ -64,6 +189,21 @@ export interface VizNode {
64
189
  data?: unknown;
65
190
  onClick?: (id: string, node: VizNode) => void;
66
191
  animations?: VizAnimSpec[];
192
+ /**
193
+ * Named connection ports on this node.
194
+ *
195
+ * When an edge references a port via `fromPort` / `toPort`, the endpoint
196
+ * is resolved to `node.pos + port.offset` instead of the generic
197
+ * boundary intersection.
198
+ *
199
+ * If omitted, default ports for the node's shape are available automatically
200
+ * (see `getDefaultPorts`). Explicit ports override defaults entirely.
201
+ */
202
+ ports?: NodePort[];
203
+ /** If set, this node is a child of the node with this id. */
204
+ parentId?: string;
205
+ /** Container-specific configuration (only on parent nodes). */
206
+ container?: ContainerConfig;
67
207
  }
68
208
  export interface EdgeLabel {
69
209
  text: string;
@@ -72,20 +212,86 @@ export interface EdgeLabel {
72
212
  dx?: number;
73
213
  dy?: number;
74
214
  }
215
+ /** Edge routing algorithm. */
216
+ export type EdgeRouting = 'straight' | 'curved' | 'orthogonal';
217
+ /**
218
+ * Edge marker/arrowhead types.
219
+ *
220
+ * - `'none'`: No marker
221
+ * - `'arrow'`: Filled triangle (default arrowhead)
222
+ * - `'arrowOpen'`: Open/unfilled triangle (V shape)
223
+ * - `'diamond'`: Filled diamond (UML composition)
224
+ * - `'diamondOpen'`: Open diamond (UML aggregation)
225
+ * - `'circle'`: Filled circle
226
+ * - `'circleOpen'`: Open circle
227
+ * - `'square'`: Filled square
228
+ * - `'bar'`: Perpendicular line (T shape, for cardinality)
229
+ * - `'halfArrow'`: Single-sided arrow (one wing)
230
+ */
231
+ export type EdgeMarkerType = 'none' | 'arrow' | 'arrowOpen' | 'diamond' | 'diamondOpen' | 'circle' | 'circleOpen' | 'square' | 'bar' | 'halfArrow';
75
232
  export interface VizEdge {
76
233
  id: string;
77
234
  from: string;
78
235
  to: string;
236
+ /** @deprecated Use `labels` for multi-position support. Kept for backwards compatibility. */
79
237
  label?: EdgeLabel;
238
+ /** Multiple labels at different positions along the edge. */
239
+ labels?: EdgeLabel[];
80
240
  runtime?: VizRuntimeEdgeProps;
81
- markerEnd?: 'arrow' | 'none';
241
+ /** Marker at the target (end) of the edge. */
242
+ markerEnd?: EdgeMarkerType;
243
+ /** Marker at the source (start) of the edge. */
244
+ markerStart?: EdgeMarkerType;
245
+ /** Port id on the source node. When set, the edge starts at the port's position instead of the boundary. */
246
+ fromPort?: string;
247
+ /** Port id on the target node. When set, the edge ends at the port's position instead of the boundary. */
248
+ toPort?: string;
82
249
  anchor?: 'center' | 'boundary';
250
+ /** Per-edge visual styling. Overrides the CSS defaults when set. */
251
+ style?: {
252
+ stroke?: string;
253
+ strokeWidth?: number;
254
+ fill?: string;
255
+ opacity?: number;
256
+ };
83
257
  className?: string;
84
258
  hitArea?: number;
85
259
  data?: unknown;
86
260
  onClick?: (id: string, edge: VizEdge) => void;
87
261
  animations?: VizAnimSpec[];
262
+ /** Routing algorithm for the edge path (default: 'straight'). */
263
+ routing?: EdgeRouting;
264
+ /** User-defined intermediate waypoints the edge must pass through. */
265
+ waypoints?: Vec2[];
266
+ }
267
+ /**
268
+ * Overlay kind -> params mapping.
269
+ *
270
+ * This interface is intentionally empty in core and is meant to be augmented by:
271
+ * - core overlays (in this repo)
272
+ * - downstream libraries/apps (via TS module augmentation)
273
+ */
274
+ export interface OverlayKindRegistry {
88
275
  }
276
+ /** Internal runtime flag used to mark overlays as needing a DOM update. */
277
+ export declare const OVERLAY_RUNTIME_DIRTY: unique symbol;
278
+ /** String overlay ids that are known/typed via `OverlayKindRegistry`. */
279
+ export type KnownOverlayId = Extract<keyof OverlayKindRegistry, string>;
280
+ /** Any overlay id (typed known ids + arbitrary custom ids). */
281
+ export type OverlayId = KnownOverlayId | (string & {});
282
+ /**
283
+ * Params type for a given overlay id.
284
+ * - Known ids resolve to their registered params type.
285
+ * - Unknown/custom ids fall back to `unknown` (escape hatch).
286
+ */
287
+ export type OverlayParams<K extends string> = K extends KnownOverlayId ? OverlayKindRegistry[K] : unknown;
288
+ /** A type-safe overlay spec keyed by overlay id. */
289
+ export type TypedVizOverlaySpec<K extends OverlayId = OverlayId> = {
290
+ id: K;
291
+ key?: string;
292
+ params: OverlayParams<K>;
293
+ className?: string;
294
+ };
89
295
  export type VizOverlaySpec<T = any> = {
90
296
  id: string;
91
297
  key?: string;
package/dist/types.js CHANGED
@@ -1 +1,2 @@
1
- export {};
1
+ /** Internal runtime flag used to mark overlays as needing a DOM update. */
2
+ export const OVERLAY_RUNTIME_DIRTY = Symbol('vizcraft.overlay.runtimeDirty');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vizcraft",
3
- "version": "0.2.2",
3
+ "version": "1.0.0",
4
4
  "description": "A fluent, type-safe SVG scene builder for composing nodes, edges, animations, and overlays with incremental DOM updates and no framework dependency.",
5
5
  "keywords": [
6
6
  "visualization",
@@ -1 +0,0 @@
1
- export {};
@@ -1,49 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createPlayer } from './player';
3
- describe('anim/player', () => {
4
- it('chains sequential tweens on the same property', () => {
5
- const state = new Map([['node:a|x', 120]]);
6
- const adapter = {
7
- get(target, prop) {
8
- return state.get(`${target}|${prop}`);
9
- },
10
- set(target, prop, value) {
11
- state.set(`${target}|${prop}`, value);
12
- },
13
- flush() {
14
- // no-op
15
- },
16
- };
17
- const spec = {
18
- version: 'viz-anim/1',
19
- tweens: [
20
- {
21
- kind: 'tween',
22
- target: 'node:a',
23
- property: 'x',
24
- to: 320,
25
- duration: 1200,
26
- delay: 0,
27
- easing: 'linear',
28
- },
29
- {
30
- kind: 'tween',
31
- target: 'node:a',
32
- property: 'x',
33
- to: 120,
34
- duration: 1200,
35
- delay: 1800,
36
- easing: 'linear',
37
- },
38
- ],
39
- };
40
- const player = createPlayer(adapter);
41
- player.load(spec);
42
- player.seek(600);
43
- // Halfway from 120 -> 320
44
- expect(state.get('node:a|x')).toBeCloseTo(220, 5);
45
- player.seek(2400);
46
- // Halfway from 320 -> 120 (1200ms into the second tween)
47
- expect(state.get('node:a|x')).toBeCloseTo(220, 5);
48
- });
49
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,66 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { viz } from './index';
3
- describe('vizcraft core', () => {
4
- it('exports viz builder', () => {
5
- expect(viz).toBeDefined();
6
- expect(typeof viz).toBe('function');
7
- });
8
- it('creates a builder instance', () => {
9
- const builder = viz();
10
- expect(builder).toBeDefined();
11
- // Verify default viewbox
12
- const view = builder._getViewBox();
13
- expect(view).toEqual({ w: 800, h: 600 });
14
- });
15
- it('supports data-only builder.animate(cb) and stores specs on the scene', () => {
16
- const builder = viz();
17
- builder.node('a').at(0, 0);
18
- builder.node('b').at(10, 10);
19
- builder.edge('a', 'b');
20
- const spec = builder.animate((anim) => anim.node('a').to({ x: 200, opacity: 0.5 }, { duration: 600 }));
21
- expect(spec.version).toBe('viz-anim/1');
22
- expect(spec.tweens.length).toBe(2);
23
- expect(spec.tweens[0]).toBeDefined();
24
- expect(spec.tweens[0].target).toBe('node:a');
25
- const scene = builder.build();
26
- expect(scene.animationSpecs?.length).toBe(1);
27
- expect(scene.animationSpecs?.[0]).toEqual(spec);
28
- });
29
- it('keeps legacy .animate("flow") behavior separate from data-only specs', () => {
30
- const builder = viz();
31
- builder.node('a').at(0, 0);
32
- builder.node('b').at(10, 10);
33
- builder.edge('a', 'b').animate('flow', { duration: '1s' });
34
- const scene = builder.build();
35
- expect(scene.animationSpecs).toBeUndefined();
36
- expect(scene.edges[0]).toBeDefined();
37
- expect(scene.edges[0].animations?.[0]?.id).toBe('flow');
38
- });
39
- it('supports element-level .animate(cb) and animateTo(...) sugar', () => {
40
- const builder = viz();
41
- builder.node('a').at(0, 0).animateTo({ x: 123 }, { duration: 400 });
42
- builder.node('b').at(10, 10);
43
- builder
44
- .edge('a', 'b')
45
- .animate((anim) => anim.to({ strokeDashoffset: -100 }, { duration: 1000 }));
46
- const scene = builder.build();
47
- expect(scene.animationSpecs?.length).toBe(2);
48
- const nodeSpec = scene.animationSpecs?.[0];
49
- const edgeSpec = scene.animationSpecs?.[1];
50
- expect(nodeSpec).toBeDefined();
51
- expect(edgeSpec).toBeDefined();
52
- expect(nodeSpec.tweens[0]).toBeDefined();
53
- expect(edgeSpec.tweens[0]).toBeDefined();
54
- expect(nodeSpec.tweens[0].target).toBe('node:a');
55
- expect(edgeSpec.tweens[0].target).toBe('edge:a->b');
56
- });
57
- it('allows animating edges with custom ids via anim.edge(from, to, id)', () => {
58
- const builder = viz();
59
- builder.node('a').at(0, 0);
60
- builder.node('b').at(10, 10);
61
- builder.edge('a', 'b', 'e1');
62
- const spec = builder.animate((anim) => anim.edge('a', 'b', 'e1').to({ strokeDashoffset: -50 }, { duration: 250 }));
63
- expect(spec.tweens.length).toBe(1);
64
- expect(spec.tweens[0].target).toBe('edge:e1');
65
- });
66
- });