@visualli/core 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.
Files changed (43) hide show
  1. package/README.md +263 -0
  2. package/dist/index.cjs +2325 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +1181 -0
  5. package/dist/index.d.ts +1181 -0
  6. package/dist/index.js +2119 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +80 -0
  9. package/src/animations/constants.ts +72 -0
  10. package/src/animations/easing.ts +70 -0
  11. package/src/animations/index.ts +2 -0
  12. package/src/config/blobShapes.ts +146 -0
  13. package/src/config/textScaling.ts +54 -0
  14. package/src/constants/design.ts +281 -0
  15. package/src/constants/index.ts +3 -0
  16. package/src/constants/performanceConstants.ts +157 -0
  17. package/src/constants/renderConfig.ts +134 -0
  18. package/src/index.ts +18 -0
  19. package/src/layout/circularLayout.ts +95 -0
  20. package/src/layout/index.ts +3 -0
  21. package/src/layout/layoutUtils.ts +132 -0
  22. package/src/layout/linearLayout.ts +84 -0
  23. package/src/parser/configUtils.ts +129 -0
  24. package/src/parser/index.ts +4 -0
  25. package/src/parser/middleware.ts +24 -0
  26. package/src/parser/mindmapUtils.ts +76 -0
  27. package/src/parser/visualliConverter.ts +328 -0
  28. package/src/parser/visualliParser.ts +191 -0
  29. package/src/performance/index.ts +1 -0
  30. package/src/performance/performanceMonitor.ts +145 -0
  31. package/src/rendering/culling.ts +71 -0
  32. package/src/spatial/index.ts +1 -0
  33. package/src/spatial/spatialIndex.ts +83 -0
  34. package/src/stores/index.ts +61 -0
  35. package/src/types/document.ts +15 -0
  36. package/src/types/index.ts +17 -0
  37. package/src/types/layer.ts +67 -0
  38. package/src/types/meta.ts +9 -0
  39. package/src/types/mindmap.ts +136 -0
  40. package/src/types/schema.ts +36 -0
  41. package/src/utils/navigation.ts +161 -0
  42. package/src/viewport/index.ts +1 -0
  43. package/src/viewport/viewportUtils.ts +195 -0
@@ -0,0 +1,134 @@
1
+ // ─── Render Configuration ─────────────────────────────────────────────────────
2
+
3
+ import type { RenderConfig } from '../types/mindmap.js';
4
+ import {
5
+ FPS_TARGET,
6
+ MEMORY_TARGET_MB,
7
+ QUALITY_HIGH,
8
+ QUALITY_MEDIUM,
9
+ QUALITY_LOW,
10
+ NODE_COUNT_THRESHOLDS,
11
+ } from './performanceConstants.js';
12
+
13
+ // ── Defaults ──────────────────────────────────────────────────────────────────
14
+
15
+ export const DEFAULT_RENDER_CONFIG: RenderConfig = {
16
+ renderMode: 'canvas',
17
+ qualityLevel: 'high',
18
+ cullingEnabled: false,
19
+ fpsTarget: FPS_TARGET,
20
+ memoryLimit: MEMORY_TARGET_MB,
21
+ };
22
+
23
+ export const SIMPLIFIED_RENDER_CONFIG: RenderConfig = {
24
+ renderMode: 'simplified',
25
+ qualityLevel: 'low',
26
+ cullingEnabled: true,
27
+ fpsTarget: 30,
28
+ memoryLimit: 400,
29
+ };
30
+
31
+ // ── Quality Map ───────────────────────────────────────────────────────────────
32
+
33
+ export const QUALITY_SETTINGS = {
34
+ high: QUALITY_HIGH,
35
+ medium: QUALITY_MEDIUM,
36
+ low: QUALITY_LOW,
37
+ } as const;
38
+
39
+ // ── Feature Flags ─────────────────────────────────────────────────────────────
40
+
41
+ export const FEATURE_FLAGS = {
42
+ viewportCulling: true,
43
+ spatialIndexing: true,
44
+ shadows: true,
45
+ bezierCurves: true,
46
+ animations: true,
47
+ branchBadges: true,
48
+ autoQualityAdjustment: true,
49
+ performanceMonitoring: true,
50
+ performanceToasts: true,
51
+ debugOverlay: false,
52
+ showFPS: false,
53
+ showMemory: false,
54
+ showVisibleCount: false,
55
+ } as const;
56
+
57
+ // ── Render Modes ──────────────────────────────────────────────────────────────
58
+
59
+ export type RenderMode = RenderConfig['renderMode'];
60
+ export type QualityLevel = RenderConfig['qualityLevel'];
61
+
62
+ export const RENDER_MODES = {
63
+ canvas: {
64
+ name: 'Canvas',
65
+ description: 'Full Konva canvas rendering with all features',
66
+ features: { culling: true, shadows: true, bezierCurves: true, animations: true },
67
+ },
68
+ simplified: {
69
+ name: 'Simplified',
70
+ description: 'Reduced feature set for low-end devices',
71
+ features: { culling: true, shadows: false, bezierCurves: false, animations: false },
72
+ },
73
+ } as const;
74
+
75
+ export const QUALITY_LEVELS = {
76
+ high: { name: 'High', description: 'All visual features enabled', targetFPS: 60, features: QUALITY_HIGH },
77
+ medium: { name: 'Medium', description: 'Shadows disabled, other features enabled', targetFPS: 60, features: QUALITY_MEDIUM },
78
+ low: { name: 'Low', description: 'Minimal visual features, max performance', targetFPS: 30, features: QUALITY_LOW },
79
+ } as const;
80
+
81
+ // ── Auto-Adjustment Helpers ───────────────────────────────────────────────────
82
+
83
+ export function getOptimalQualityLevel(
84
+ currentFPS: number,
85
+ currentQuality: QualityLevel,
86
+ ): QualityLevel {
87
+ if (currentFPS < 45 && currentQuality === 'high') return 'medium';
88
+ if (currentFPS < 30 && currentQuality === 'medium') return 'low';
89
+ if (currentFPS > 58 && currentQuality === 'low') return 'medium';
90
+ if (currentFPS > 58 && currentQuality === 'medium') return 'high';
91
+ return currentQuality;
92
+ }
93
+
94
+ export function getRenderConfigForQuality(quality: QualityLevel): Partial<RenderConfig> {
95
+ return { qualityLevel: quality, fpsTarget: QUALITY_LEVELS[quality].targetFPS };
96
+ }
97
+
98
+ export function isFeatureEnabled(
99
+ featureName: keyof typeof FEATURE_FLAGS,
100
+ config: RenderConfig,
101
+ ): boolean {
102
+ if (!FEATURE_FLAGS[featureName]) return false;
103
+
104
+ const qs = QUALITY_SETTINGS[config.qualityLevel];
105
+ switch (featureName) {
106
+ case 'shadows': return qs.shadows;
107
+ case 'bezierCurves': return qs.bezierCurves;
108
+ case 'animations': return qs.animations;
109
+ default: return FEATURE_FLAGS[featureName] as boolean;
110
+ }
111
+ }
112
+
113
+ // ── Device Detection ──────────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Recommend a render config based on detected device memory (browser only).
117
+ * Falls back to SIMPLIFIED_RENDER_CONFIG when the Performance Memory API is
118
+ * unavailable or memory is constrained.
119
+ */
120
+ export function getRecommendedRenderConfig(): RenderConfig {
121
+ if (typeof performance === 'undefined' || !(performance as unknown as Record<string, unknown>)['memory']) {
122
+ return SIMPLIFIED_RENDER_CONFIG;
123
+ }
124
+
125
+ const memory = (performance as unknown as Record<string, unknown>)['memory'] as Record<string, number>;
126
+ if (memory?.['jsHeapSizeLimit'] && memory['jsHeapSizeLimit'] < 512 * 1024 * 1024) {
127
+ return SIMPLIFIED_RENDER_CONFIG;
128
+ }
129
+
130
+ return DEFAULT_RENDER_CONFIG;
131
+ }
132
+
133
+ /** Re-export so consumers need only one import. */
134
+ export { NODE_COUNT_THRESHOLDS };
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ // ─── @visualli/core — Main Barrel Export ────────────────────────────────────
2
+ //
3
+ // Import from sub-paths for tree-shaking (e.g. `@visualli/core/layout`),
4
+ // or use the main barrel for convenience.
5
+
6
+ export * from './types/index.js';
7
+ export * from './layout/index.js';
8
+ export * from './parser/index.js';
9
+ export * from './constants/index.js';
10
+ export * from './viewport/index.js';
11
+ export * from './performance/index.js';
12
+ export * from './animations/index.js';
13
+ export * from './spatial/index.js';
14
+ export * from './stores/index.js';
15
+ export * from './utils/navigation.js';
16
+ export * from './rendering/culling.js';
17
+ export * from './config/textScaling.js';
18
+ export * from './config/blobShapes.js';
@@ -0,0 +1,95 @@
1
+ // ─── Circular Layout Algorithm ────────────────────────────────────────────────
2
+ //
3
+ // Positions nodes in a uniform circle with equal angular spacing.
4
+ // PATTERN : Uniform circular distribution (polar coordinates)
5
+ // SPACING : Equal angular intervals (2π/n)
6
+ // ORIGIN : Configurable center (default 0, 0)
7
+ // RADIUS : Configurable percentage of container (default 13.5%)
8
+
9
+ import type { FlatNode } from '../types/mindmap.js';
10
+
11
+ export interface CircularLayoutOptions {
12
+ /** Radius as a fraction of the container's minimum dimension (0–1). Default: 0.135 */
13
+ radiusPercentage?: number;
14
+ /** Container width used to calculate absolute radius. Default: 2000 */
15
+ containerWidth?: number;
16
+ /** Container height used to calculate absolute radius. Default: 2000 */
17
+ containerHeight?: number;
18
+ /** World-space X of the circle centre. Default: 0 */
19
+ centerX?: number;
20
+ /** World-space Y of the circle centre. Default: 0 */
21
+ centerY?: number;
22
+ }
23
+
24
+ /**
25
+ * Apply circular layout to a set of nodes.
26
+ * Mutates the `x` / `y` properties of each node and returns the same array.
27
+ */
28
+ export function applyCircularLayout(
29
+ nodes: FlatNode[],
30
+ options: CircularLayoutOptions = {},
31
+ ): FlatNode[] {
32
+ const {
33
+ radiusPercentage = 0.135,
34
+ containerWidth = 2000,
35
+ containerHeight = 2000,
36
+ centerX = 0,
37
+ centerY = 0,
38
+ } = options;
39
+
40
+ if (nodes.length === 0) return nodes;
41
+
42
+ if (nodes.length === 1) {
43
+ nodes[0].x = centerX;
44
+ nodes[0].y = centerY;
45
+ return nodes;
46
+ }
47
+
48
+ const radius = Math.min(containerWidth, containerHeight) * radiusPercentage;
49
+
50
+ // n=2 → horizontal; n≥3 → start at 12 o'clock
51
+ const startAngle = nodes.length === 2 ? 0 : -Math.PI / 2;
52
+ const angleStep = (2 * Math.PI) / nodes.length;
53
+
54
+ for (let i = 0; i < nodes.length; i++) {
55
+ const angle = startAngle + i * angleStep;
56
+ nodes[i].x = centerX + radius * Math.cos(angle);
57
+ nodes[i].y = centerY + radius * Math.sin(angle);
58
+ }
59
+
60
+ return nodes;
61
+ }
62
+
63
+ /**
64
+ * Calculate the minimum radius needed to prevent node boxes from overlapping.
65
+ *
66
+ * Uses the chord-length formula: `chord = 2r · sin(θ/2)`, solved for `r`.
67
+ */
68
+ export function calculateMinimumRadius(
69
+ nodeCount: number,
70
+ nodeWidth = 200,
71
+ nodePadding = 150,
72
+ ): number {
73
+ if (nodeCount <= 1) return 0;
74
+ const angleStep = (2 * Math.PI) / nodeCount;
75
+ const minChordLength = nodeWidth + nodePadding;
76
+ return minChordLength / (2 * Math.sin(angleStep / 2));
77
+ }
78
+
79
+ /**
80
+ * Calculate the optimal radius percentage for a given node count,
81
+ * ensuring sufficient spacing for edge labels.
82
+ *
83
+ * @returns A value in [0.135, 0.45]
84
+ */
85
+ export function calculateOptimalRadiusPercentage(
86
+ nodeCount: number,
87
+ containerWidth = 2000,
88
+ containerHeight = 2000,
89
+ nodeWidth = 200,
90
+ nodePadding = 120,
91
+ ): number {
92
+ const minRadius = calculateMinimumRadius(nodeCount, nodeWidth, nodePadding);
93
+ const minDimension = Math.min(containerWidth, containerHeight);
94
+ return Math.min(0.45, Math.max(0.135, minRadius / minDimension));
95
+ }
@@ -0,0 +1,3 @@
1
+ export * from './circularLayout.js';
2
+ export * from './linearLayout.js';
3
+ export * from './layoutUtils.js';
@@ -0,0 +1,132 @@
1
+ // ─── Layout Utilities ─────────────────────────────────────────────────────────
2
+ //
3
+ // Collision detection and force-based positioning for MindMapNode trees.
4
+
5
+ import type { MindMapNode } from '../types/mindmap.js';
6
+
7
+ export interface CollisionBox {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+
14
+ /** Axis-aligned bounding box for a node, expanded by a fixed padding. */
15
+ export function getNodeBounds(node: MindMapNode): CollisionBox {
16
+ const baseWidth = Math.max(120, node.title.length * 8 + 40);
17
+ const baseHeight = Math.max(40, 32 + 16);
18
+ const padding = 20;
19
+ return {
20
+ x: node.x - baseWidth / 2 - padding,
21
+ y: node.y - baseHeight / 2 - padding,
22
+ width: baseWidth + padding * 2,
23
+ height: baseHeight + padding * 2,
24
+ };
25
+ }
26
+
27
+ export function checkCollision(box1: CollisionBox, box2: CollisionBox): boolean {
28
+ return !(
29
+ box1.x + box1.width < box2.x ||
30
+ box2.x + box2.width < box1.x ||
31
+ box1.y + box1.height < box2.y ||
32
+ box2.y + box2.height < box1.y
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Iteratively resolve bounding-box collisions within each level.
38
+ * Nodes are pushed apart symmetrically until no overlaps remain
39
+ * or `maxIterations` is reached.
40
+ */
41
+ export function resolveCollisions(nodes: MindMapNode[], maxIterations = 10): MindMapNode[] {
42
+ const result = [...nodes];
43
+
44
+ for (let iter = 0; iter < maxIterations; iter++) {
45
+ let hasCollisions = false;
46
+
47
+ const byLevel = result.reduce<Record<number, MindMapNode[]>>((acc, n) => {
48
+ (acc[n.level] ??= []).push(n);
49
+ return acc;
50
+ }, {});
51
+
52
+ for (const levelNodes of Object.values(byLevel)) {
53
+ for (let i = 0; i < levelNodes.length; i++) {
54
+ for (let j = i + 1; j < levelNodes.length; j++) {
55
+ const n1 = levelNodes[i];
56
+ const n2 = levelNodes[j];
57
+ if (!checkCollision(getNodeBounds(n1), getNodeBounds(n2))) continue;
58
+
59
+ hasCollisions = true;
60
+ const dx = n2.x - n1.x;
61
+ const dy = n2.y - n1.y;
62
+ const dist = Math.sqrt(dx * dx + dy * dy);
63
+
64
+ if (dist < 1) {
65
+ n2.x += 100;
66
+ } else {
67
+ const b1 = getNodeBounds(n1);
68
+ const b2 = getNodeBounds(n2);
69
+ const minSep = (b1.width + b2.width) / 2;
70
+ const overlap = minSep - dist + 20;
71
+ if (overlap > 0) {
72
+ const move = overlap / 2;
73
+ n1.x -= (dx / dist) * move;
74
+ n1.y -= (dy / dist) * move;
75
+ n2.x += (dx / dist) * move;
76
+ n2.y += (dy / dist) * move;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ if (!hasCollisions) break;
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Apply a lightweight repulsive force between same-level nodes that are
91
+ * closer than 300 px, then resolve remaining collisions.
92
+ */
93
+ export function optimizeLayout(nodes: MindMapNode[]): MindMapNode[] {
94
+ const result = [...nodes];
95
+ const iterations = 5;
96
+ const repulsion = 1000;
97
+ const damp = 0.8;
98
+
99
+ for (let iter = 0; iter < iterations; iter++) {
100
+ const forces = new Map<string, { x: number; y: number }>();
101
+ result.forEach(n => forces.set(n.id, { x: 0, y: 0 }));
102
+
103
+ for (let i = 0; i < result.length; i++) {
104
+ for (let j = i + 1; j < result.length; j++) {
105
+ const n1 = result[i];
106
+ const n2 = result[j];
107
+ if (n1.level !== n2.level) continue;
108
+
109
+ const dx = n2.x - n1.x;
110
+ const dy = n2.y - n1.y;
111
+ const dist = Math.sqrt(dx * dx + dy * dy);
112
+ if (dist <= 0 || dist >= 300) continue;
113
+
114
+ const force = repulsion / (dist * dist);
115
+ const f1 = forces.get(n1.id)!;
116
+ const f2 = forces.get(n2.id)!;
117
+ f1.x -= (dx / dist) * force;
118
+ f1.y -= (dy / dist) * force;
119
+ f2.x += (dx / dist) * force;
120
+ f2.y += (dy / dist) * force;
121
+ }
122
+ }
123
+
124
+ result.forEach(n => {
125
+ const f = forces.get(n.id)!;
126
+ n.x += f.x * damp;
127
+ n.y += f.y * damp;
128
+ });
129
+ }
130
+
131
+ return resolveCollisions(result);
132
+ }
@@ -0,0 +1,84 @@
1
+ // ─── Linear Layout Algorithms ─────────────────────────────────────────────────
2
+ //
3
+ // Positions nodes in a straight line without overlap, with enough gap between
4
+ // node edges for edge-label text to render cleanly.
5
+ //
6
+ // linear-horizontal → left-to-right along X-axis
7
+ // linear-vertical → top-to-bottom along Y-axis
8
+
9
+ import type { FlatNode } from '../types/mindmap.js';
10
+
11
+ export interface LinearLayoutOptions {
12
+ /** Logical node width (px). Default: 200 */
13
+ nodeWidth?: number;
14
+ /** Logical node height (px). Default: 80 */
15
+ nodeHeight?: number;
16
+ /** Gap between node edges for horizontal layout (px). Default: 180 */
17
+ hGap?: number;
18
+ /** Gap between node edges for vertical layout (px). Default: 220 */
19
+ vGap?: number;
20
+ /** World X of the layout midpoint. Default: 0 */
21
+ centerX?: number;
22
+ /** World Y of the layout midpoint. Default: 0 */
23
+ centerY?: number;
24
+ }
25
+
26
+ /**
27
+ * Arrange nodes in a horizontal row (shared Y, varying X).
28
+ * Row is centred on (`centerX`, `centerY`).
29
+ */
30
+ export function applyLinearHorizontalLayout(
31
+ nodes: FlatNode[],
32
+ options: LinearLayoutOptions = {},
33
+ ): FlatNode[] {
34
+ const { nodeWidth = 200, hGap = 180, centerX = 0, centerY = 0 } = options;
35
+
36
+ if (nodes.length === 0) return nodes;
37
+
38
+ if (nodes.length === 1) {
39
+ nodes[0].x = centerX;
40
+ nodes[0].y = centerY;
41
+ return nodes;
42
+ }
43
+
44
+ const spacing = nodeWidth + hGap;
45
+ const totalWidth = (nodes.length - 1) * spacing;
46
+ const startX = centerX - totalWidth / 2;
47
+
48
+ for (let i = 0; i < nodes.length; i++) {
49
+ nodes[i].x = startX + i * spacing;
50
+ nodes[i].y = centerY;
51
+ }
52
+
53
+ return nodes;
54
+ }
55
+
56
+ /**
57
+ * Arrange nodes in a vertical column (shared X, varying Y).
58
+ * Column is centred on (`centerX`, `centerY`).
59
+ */
60
+ export function applyLinearVerticalLayout(
61
+ nodes: FlatNode[],
62
+ options: LinearLayoutOptions = {},
63
+ ): FlatNode[] {
64
+ const { nodeHeight = 80, vGap = 220, centerX = 0, centerY = 0 } = options;
65
+
66
+ if (nodes.length === 0) return nodes;
67
+
68
+ if (nodes.length === 1) {
69
+ nodes[0].x = centerX;
70
+ nodes[0].y = centerY;
71
+ return nodes;
72
+ }
73
+
74
+ const spacing = nodeHeight + vGap;
75
+ const totalHeight = (nodes.length - 1) * spacing;
76
+ const startY = centerY - totalHeight / 2;
77
+
78
+ for (let i = 0; i < nodes.length; i++) {
79
+ nodes[i].x = centerX;
80
+ nodes[i].y = startY + i * spacing;
81
+ }
82
+
83
+ return nodes;
84
+ }
@@ -0,0 +1,129 @@
1
+ // ─── MindMapConfig Utilities ──────────────────────────────────────────────────
2
+ //
3
+ // Helpers for creating and generating MindMapConfig objects (pure data, no UI).
4
+
5
+ import type { MindMapNode, MindMapConfig, TopLevelConnection } from '../types/mindmap.js';
6
+ import { LEVEL_COLORS, LAYOUT_SPACING, getColorForLevel } from '../constants/design.js';
7
+
8
+ export function createMindMapConfig(
9
+ nodes: MindMapNode[],
10
+ topLevelConnections: TopLevelConnection[],
11
+ title = 'Untitled Mindmap',
12
+ ): MindMapConfig {
13
+ const now = new Date().toISOString();
14
+ return {
15
+ metadata: {
16
+ title,
17
+ description: `Interactive mindmap with ${nodes.length} nodes`,
18
+ version: '1.0.0',
19
+ created: now,
20
+ lastModified: now,
21
+ },
22
+ nodes,
23
+ topLevelConnections,
24
+ settings: {
25
+ defaultColors: {
26
+ level0: LEVEL_COLORS.level0,
27
+ level1: LEVEL_COLORS.level1,
28
+ level2: LEVEL_COLORS.level2,
29
+ level3: LEVEL_COLORS.level3,
30
+ },
31
+ layout: {
32
+ nodeSpacing: LAYOUT_SPACING.nodeSpacing,
33
+ levelSpacing: LAYOUT_SPACING.levelSpacing,
34
+ },
35
+ },
36
+ };
37
+ }
38
+
39
+ // ── Sample Data Generator ─────────────────────────────────────────────────────
40
+
41
+ function generateId(prefix: string, level: number, index: number): string {
42
+ return `${prefix}-l${level}-${index}`;
43
+ }
44
+
45
+ function generateLevelNodes(
46
+ parentId: string | null,
47
+ level: number,
48
+ maxLevels: number,
49
+ xOffset = 0,
50
+ yOffset = 0,
51
+ ): MindMapNode[] {
52
+ if (level >= maxLevels) return [];
53
+
54
+ const count = 5;
55
+ const levelSpacing = 200;
56
+ const nodeSpacing = 300;
57
+ const nodes: MindMapNode[] = [];
58
+
59
+ for (let i = 0; i < count; i++) {
60
+ const nodeId = parentId
61
+ ? generateId(`${parentId}-${i}`, level, i)
62
+ : generateId('root', level, i);
63
+
64
+ const x = xOffset + (i - (count - 1) / 2) * nodeSpacing;
65
+ const y = yOffset + level * levelSpacing;
66
+
67
+ const node: MindMapNode = {
68
+ id: nodeId,
69
+ title: `Level ${level} Node ${i + 1}`,
70
+ level,
71
+ x,
72
+ y,
73
+ color: getColorForLevel(level),
74
+ relationshipLabel: level > 0 ? 'child' : undefined,
75
+ parent: parentId ?? undefined,
76
+ };
77
+
78
+ const children = generateLevelNodes(nodeId, level + 1, maxLevels, x, y + levelSpacing);
79
+ if (children.length > 0) node.children = children;
80
+
81
+ nodes.push(node);
82
+ }
83
+
84
+ return nodes;
85
+ }
86
+
87
+ /**
88
+ * Generate a sample MindMapConfig with `maxLevels` of hierarchy.
89
+ * Useful for testing and demos.
90
+ */
91
+ export function generateSampleConfig(maxLevels = 9): MindMapConfig {
92
+ const nodes = generateLevelNodes(null, 0, maxLevels, 0, 0);
93
+
94
+ const topLevelConnections: TopLevelConnection[] = nodes.slice(0, -1).map((n, i) => ({
95
+ from: n.id,
96
+ to: nodes[i + 1].id,
97
+ label: 'related to',
98
+ }));
99
+
100
+ return {
101
+ metadata: {
102
+ id: `mindmap-${Date.now()}`,
103
+ title: 'Sample Mind Map',
104
+ description: 'A sample mind map',
105
+ version: '1.0.0',
106
+ created: new Date().toISOString(),
107
+ lastModified: new Date().toISOString(),
108
+ },
109
+ nodes,
110
+ topLevelConnections,
111
+ settings: {
112
+ defaultColors: {
113
+ level0: LEVEL_COLORS.level0,
114
+ level1: LEVEL_COLORS.level1,
115
+ level2: LEVEL_COLORS.level2,
116
+ level3: LEVEL_COLORS.level3,
117
+ level4: LEVEL_COLORS.level4,
118
+ level5: LEVEL_COLORS.level5,
119
+ level6: LEVEL_COLORS.level6,
120
+ level7: LEVEL_COLORS.level7,
121
+ level8: LEVEL_COLORS.level8,
122
+ },
123
+ layout: {
124
+ nodeSpacing: LAYOUT_SPACING.nodeSpacing,
125
+ levelSpacing: LAYOUT_SPACING.levelSpacing,
126
+ },
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,4 @@
1
+ export * from './visualliParser.js';
2
+ export * from './visualliConverter.js';
3
+ export * from './mindmapUtils.js';
4
+ export * from './configUtils.js';
@@ -0,0 +1,24 @@
1
+ // ─── Middleware Types ──────────────────────────────────────────────────────────
2
+ //
3
+ // Defines the middleware system for transforming parsed JSONL data
4
+
5
+ /**
6
+ * Raw node data from a single JSONL line before any transformation
7
+ */
8
+ export type RawNodeData = Record<string, unknown>;
9
+
10
+ /**
11
+ * Middleware function that can transform raw JSONL data
12
+ * - Return the transformed node to continue the pipeline
13
+ * - Return null to skip/filter this node
14
+ * - Can mutate the input or return a new object
15
+ */
16
+ export type ParserMiddleware = (rawData: RawNodeData) => RawNodeData | null;
17
+
18
+ /**
19
+ * Middleware context with document state (for advanced use cases)
20
+ */
21
+ export interface ParserContext {
22
+ layers: Map<string, unknown>;
23
+ meta: unknown | null;
24
+ }