@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,146 @@
1
+ export interface BlobPoint { x: number; y: number }
2
+ export type BlobShape = BlobPoint[];
3
+
4
+ // ── Blob Layer Config ─────────────────────────────────────────────────────────
5
+
6
+ export const BLOB_LAYER_CONFIG = [
7
+ { opacity: 0.3, rotation: -15, scaleUp: 1.30, strokeWidth: 3, dash: [6, 4] }, // Layer #3 (outermost)
8
+ { opacity: 0.4, rotation: -10, scaleUp: 1.20, strokeWidth: 1, dash: undefined }, // Layer #2
9
+ { opacity: 0.5, rotation: -5, scaleUp: 1.10, strokeWidth: 2, dash: undefined }, // Layer #1
10
+ ] as const;
11
+
12
+ export const NODE_LAYER_CONFIG = { opacity: 1, rotation: 0, strokeWidth: 3 } as const;
13
+
14
+ // ── Blob Shapes ───────────────────────────────────────────────────────────────
15
+
16
+ export const ALL_BLOB_SHAPES: ReadonlyArray<BlobShape> = [
17
+ // Shape 0 · Blob 1
18
+ [
19
+ { x: 1.12, y: 0 }, { x: 0.86, y: 0.56 }, { x: 0.46, y: 0.94 }, { x: -0.08, y: 1.06 },
20
+ { x: -0.68, y: 0.82 }, { x: -0.96, y: 0.08 }, { x: -0.55, y: -0.82 }, { x: 0.04, y: -1.02 },
21
+ { x: 0.72, y: -0.8 }, { x: 1.08, y: -0.38 },
22
+ ],
23
+ // Shape 1 · Blob 2
24
+ [
25
+ { x: -0.489, y: -0.625 }, { x: -0.384, y: -0.716 }, { x: -0.285, y: -0.792 }, { x: -0.189, y: -0.855 },
26
+ { x: -0.090, y: -0.903 }, { x: 0.014, y: -0.932 }, { x: 0.129, y: -0.940 }, { x: 0.255, y: -0.924 },
27
+ { x: 0.412, y: -0.876 }, { x: 0.516, y: -0.832 }, { x: 0.612, y: -0.780 }, { x: 0.695, y: -0.725 },
28
+ { x: 0.767, y: -0.666 }, { x: 0.830, y: -0.606 }, { x: 0.885, y: -0.546 }, { x: 0.933, y: -0.486 },
29
+ { x: 0.979, y: -0.425 }, { x: 1.006, y: -0.385 }, { x: 1.034, y: -0.334 }, { x: 1.058, y: -0.276 },
30
+ { x: 1.078, y: -0.209 }, { x: 1.092, y: -0.133 }, { x: 1.101, y: -0.046 }, { x: 1.102, y: 0.052 },
31
+ { x: 1.094, y: 0.173 }, { x: 1.083, y: 0.244 }, { x: 1.063, y: 0.324 }, { x: 1.034, y: 0.401 },
32
+ { x: 0.996, y: 0.477 }, { x: 0.948, y: 0.549 }, { x: 0.889, y: 0.617 }, { x: 0.820, y: 0.681 },
33
+ { x: 0.735, y: 0.741 }, { x: 0.658, y: 0.783 }, { x: 0.569, y: 0.822 }, { x: 0.474, y: 0.855 },
34
+ { x: 0.372, y: 0.882 }, { x: 0.265, y: 0.904 }, { x: 0.156, y: 0.920 }, { x: 0.046, y: 0.931 },
35
+ { x: -0.069, y: 0.938 }, { x: -0.155, y: 0.935 }, { x: -0.250, y: 0.919 }, { x: -0.346, y: 0.890 },
36
+ { x: -0.443, y: 0.851 }, { x: -0.539, y: 0.800 }, { x: -0.633, y: 0.738 }, { x: -0.723, y: 0.667 },
37
+ { x: -0.808, y: 0.585 }, { x: -0.882, y: 0.501 }, { x: -0.944, y: 0.421 }, { x: -0.994, y: 0.343 },
38
+ { x: -1.034, y: 0.267 }, { x: -1.064, y: 0.192 }, { x: -1.085, y: 0.120 }, { x: -1.097, y: 0.042 },
39
+ { x: -1.102, y: 0.001 }, { x: -1.090, y: -0.243 }, { x: -1.050, y: -0.365 }, { x: -0.990, y: -0.441 },
40
+ { x: -0.911, y: -0.479 }, { x: -0.819, y: -0.499 }, { x: -0.715, y: -0.517 }, { x: -0.605, y: -0.551 },
41
+ ],
42
+ // Shape 2 · Blob 3
43
+ [
44
+ { x: 0.003, y: -0.970 }, { x: 0.163, y: -0.984 }, { x: 0.286, y: -1.028 }, { x: 0.389, y: -1.096 },
45
+ { x: 0.488, y: -1.174 }, { x: 0.597, y: -1.210 }, { x: 0.728, y: -1.165 }, { x: 0.867, y: -1.036 },
46
+ { x: 0.996, y: -0.839 }, { x: 1.098, y: -0.590 }, { x: 1.158, y: -0.304 }, { x: 1.153, y: 0.012 },
47
+ { x: 1.080, y: 0.336 }, { x: 0.944, y: 0.639 }, { x: 0.756, y: 0.889 }, { x: 0.484, y: 1.072 },
48
+ { x: 0.339, y: 1.079 }, { x: 0.219, y: 1.051 }, { x: 0.131, y: 0.994 }, { x: 0.059, y: 0.939 },
49
+ { x: 0.008, y: 0.915 }, { x: -0.105, y: 0.931 }, { x: -0.176, y: 0.965 }, { x: -0.249, y: 1.005 },
50
+ { x: -0.345, y: 1.039 }, { x: -0.497, y: 1.055 }, { x: -0.672, y: 1.032 }, { x: -0.817, y: 0.952 },
51
+ { x: -0.945, y: 0.810 }, { x: -1.054, y: 0.600 }, { x: -1.160, y: 0.302 }, { x: -1.187, y: -0.011 },
52
+ { x: -1.165, y: -0.326 }, { x: -1.097, y: -0.599 }, { x: -0.991, y: -0.838 }, { x: -0.852, y: -0.912 },
53
+ { x: -0.695, y: -0.958 }, { x: -0.530, y: -0.988 }, { x: -0.357, y: -0.992 }, { x: -0.180, y: -0.983 },
54
+ ],
55
+ // Shape 3 · Blob 4
56
+ [
57
+ { x: -0.734, y: -1.127 }, { x: -0.656, y: -1.146 }, { x: -0.567, y: -1.142 }, { x: -0.468, y: -1.115 },
58
+ { x: -0.358, y: -1.079 }, { x: -0.238, y: -1.034 }, { x: -0.107, y: -0.989 }, { x: 0.035, y: -0.953 },
59
+ { x: 0.188, y: -0.929 }, { x: 0.325, y: -0.916 }, { x: 0.484, y: -0.896 }, { x: 0.650, y: -0.854 },
60
+ { x: 0.811, y: -0.770 }, { x: 0.955, y: -0.631 }, { x: 1.068, y: -0.422 }, { x: 1.138, y: -0.126 },
61
+ { x: 1.152, y: 0.272 }, { x: 1.121, y: 0.451 }, { x: 1.046, y: 0.602 }, { x: 0.928, y: 0.727 },
62
+ { x: 0.767, y: 0.833 }, { x: 0.565, y: 0.925 }, { x: 0.323, y: 1.001 }, { x: 0.042, y: 1.075 },
63
+ { x: -0.277, y: 1.142 }, { x: -0.368, y: 1.147 }, { x: -0.475, y: 1.136 }, { x: -0.589, y: 1.104 },
64
+ { x: -0.704, y: 1.050 }, { x: -0.815, y: 0.973 }, { x: -0.913, y: 0.871 }, { x: -0.993, y: 0.746 },
65
+ { x: -1.048, y: 0.593 }, { x: -1.109, y: 0.298 }, { x: -1.145, y: 0.018 }, { x: -1.154, y: -0.243 },
66
+ { x: -1.135, y: -0.482 }, { x: -1.085, y: -0.695 }, { x: -1.003, y: -0.874 }, { x: -0.886, y: -1.020 },
67
+ ],
68
+ // Shape 4 · Blob 5
69
+ [
70
+ { x: -0.734, y: 1.127 }, { x: -0.656, y: 1.146 }, { x: -0.567, y: 1.142 }, { x: -0.468, y: 1.115 },
71
+ { x: -0.358, y: 1.079 }, { x: -0.238, y: 1.034 }, { x: -0.107, y: 0.989 }, { x: 0.035, y: 0.953 },
72
+ { x: 0.188, y: 0.929 }, { x: 0.325, y: 0.916 }, { x: 0.484, y: 0.896 }, { x: 0.650, y: 0.854 },
73
+ { x: 0.811, y: 0.770 }, { x: 0.955, y: 0.631 }, { x: 1.068, y: 0.422 }, { x: 1.138, y: 0.126 },
74
+ { x: 1.152, y: -0.272 }, { x: 1.121, y: -0.451 }, { x: 1.046, y: -0.602 }, { x: 0.928, y: -0.727 },
75
+ { x: 0.767, y: -0.833 }, { x: 0.565, y: -0.925 }, { x: 0.323, y: -1.001 }, { x: 0.042, y: -1.075 },
76
+ { x: -0.277, y: -1.142 }, { x: -0.368, y: -1.147 }, { x: -0.475, y: -1.136 }, { x: -0.589, y: -1.104 },
77
+ { x: -0.704, y: -1.050 }, { x: -0.815, y: -0.973 }, { x: -0.913, y: -0.871 }, { x: -0.993, y: -0.746 },
78
+ { x: -1.048, y: -0.593 }, { x: -1.109, y: -0.298 }, { x: -1.145, y: -0.018 }, { x: -1.154, y: 0.243 },
79
+ { x: -1.135, y: 0.482 }, { x: -1.085, y: 0.695 }, { x: -1.003, y: 0.874 }, { x: -0.886, y: 1.020 },
80
+ ],
81
+ // Shape 5 · Blob 6
82
+ [
83
+ { x: 0.001, y: -1.006 }, { x: 0.134, y: -1.001 }, { x: 0.262, y: -1.024 },
84
+ { x: 0.382, y: -1.073 }, { x: 0.498, y: -1.107 }, { x: 0.614, y: -1.105 },
85
+ { x: 0.733, y: -1.049 }, { x: 0.850, y: -0.932 }, { x: 0.953, y: -0.755 },
86
+ { x: 1.030, y: -0.529 }, { x: 1.067, y: -0.265 }, { x: 1.054, y: 0.018 },
87
+ { x: 0.986, y: 0.300 }, { x: 0.868, y: 0.557 }, { x: 0.715, y: 0.766 },
88
+ { x: 0.550, y: 0.909 }, { x: 0.396, y: 0.984 }, { x: 0.262, y: 1.005 },
89
+ { x: 0.148, y: 1.003 }, { x: 0.037, y: 1.008 }, { x: -0.083, y: 1.040 },
90
+ { x: -0.220, y: 1.118 }, { x: -0.360, y: 1.192 }, { x: -0.500, y: 1.234 },
91
+ { x: -0.650, y: 1.175 }, { x: -0.810, y: 1.045 }, { x: -0.990, y: 0.855 },
92
+ { x: -1.140, y: 0.610 }, { x: -1.240, y: 0.330 }, { x: -1.270, y: 0.040 },
93
+ { x: -1.275, y: -0.250 }, { x: -1.225, y: -0.500 }, { x: -1.080, y: -0.720 },
94
+ { x: -0.890, y: -0.920 }, { x: -0.734, y: -1.053 }, { x: -0.614, y: -1.112 },
95
+ { x: -0.498, y: -1.127 }, { x: -0.381, y: -1.110 }, { x: -0.261, y: -1.095 },
96
+ { x: -0.133, y: -1.058 }, { x: 0.001, y: -1.010 }, { x: 0.134, y: -1.001 },
97
+ ],
98
+ ];
99
+
100
+ export const ACTIVE_BLOB_TYPES: ReadonlyArray<number> = [0, 1, 2, 3, 4, 5];
101
+
102
+ /**
103
+ * Per-shape text x-offset as a fraction of baseRadiusX.
104
+ * Compensates for visually asymmetric blobs. Positive = right, negative = left.
105
+ */
106
+ export const BLOB_TEXT_OFFSETS: ReadonlyArray<number> = [0, 0, 0, 0, 0, -0.10];
107
+
108
+ /**
109
+ * Deterministic pseudo-random blob-type selector per tree level.
110
+ * Root level (0) always uses shape 0; consecutive levels use different shapes.
111
+ */
112
+ export function getBlobTypeForLayer(level: number): number {
113
+ const numTypes = ACTIVE_BLOB_TYPES.length;
114
+ if (numTypes <= 1) return ACTIVE_BLOB_TYPES[0] ?? 0;
115
+ let currentType = 0;
116
+ for (let i = 1; i <= level; i++) {
117
+ const pseudoRandom = Math.floor(Math.abs(Math.sin((i + 1) * 12.9898) * 43758.5453));
118
+ const stepOffset = (pseudoRandom % (numTypes - 1)) + 1;
119
+ currentType = (currentType + stepOffset) % numTypes;
120
+ }
121
+ return ACTIVE_BLOB_TYPES[currentType];
122
+ }
123
+
124
+ /**
125
+ * Draw the blob path onto a canvas context (centered at 0,0).
126
+ * Uses closed quadratic bezier curves through midpoints of control points.
127
+ */
128
+ export function drawBlobPath(
129
+ ctx: { beginPath(): void; moveTo(x: number, y: number): void; quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; closePath(): void },
130
+ radiusX: number,
131
+ radiusY: number,
132
+ blobType: number,
133
+ ): void {
134
+ const srcPts = ALL_BLOB_SHAPES[blobType] ?? ALL_BLOB_SHAPES[0];
135
+ const pts = srcPts.map(p => ({ x: p.x * radiusX, y: p.y * radiusY }));
136
+ ctx.beginPath();
137
+ const last = pts[pts.length - 1];
138
+ const first = pts[0];
139
+ ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
140
+ for (let i = 0; i < pts.length; i++) {
141
+ const curr = pts[i];
142
+ const next = pts[(i + 1) % pts.length];
143
+ ctx.quadraticCurveTo(curr.x, curr.y, (curr.x + next.x) / 2, (curr.y + next.y) / 2);
144
+ }
145
+ ctx.closePath();
146
+ }
@@ -0,0 +1,54 @@
1
+ import { NODE_HEIGHT } from '../constants/performanceConstants.js';
2
+
3
+ export const NODE_TEXT_BASE_FONT_PX = 28;
4
+ export const DESCRIPTION_TEXT_BASE_FONT_PX = 12;
5
+ export const CONTAINER_LABEL_BASE_FONT_PX = 24;
6
+ export const EDGE_LABEL_BASE_FONT_PX = 18;
7
+ export const OVERLAY_FONT_UPPER_BOOST_PX = 8;
8
+
9
+ // Keep text away from blob edges
10
+ const NODE_TEXT_SAFE_BLOB_FILL_RATIO = 0.88;
11
+
12
+ /**
13
+ * Computes world-space text scale for node title:
14
+ * - inverse zoom for screen-space consistency
15
+ * - clamped so text stays inside the node blob with safe padding
16
+ *
17
+ * Matches visualli.ai's computeNodeTextWorldScale(nodeWidth, zoomLevel).
18
+ */
19
+ export function computeNodeTextWorldScale(nodeWidth: number, zoomLevel: number): number {
20
+ const safeZoom = Math.max(zoomLevel, 0.0001);
21
+ const baseRadiusX = nodeWidth / 2;
22
+ const baseRadiusY = Math.max((NODE_HEIGHT * 1.6) / 2, baseRadiusX * 0.74);
23
+ const textNaturalWidth = nodeWidth - 40;
24
+ const textNaturalHeight = NODE_HEIGHT - 20;
25
+
26
+ const maxScaleX = ((baseRadiusX * 2) * NODE_TEXT_SAFE_BLOB_FILL_RATIO) / Math.max(textNaturalWidth, 1);
27
+ const maxScaleY = ((baseRadiusY * 2) * NODE_TEXT_SAFE_BLOB_FILL_RATIO) / Math.max(textNaturalHeight, 1);
28
+ const fitScale = Math.max(0.01, Math.min(maxScaleX, maxScaleY));
29
+
30
+ return Math.min(1 / safeZoom, fitScale);
31
+ }
32
+
33
+ /** Screen-space scale (zoom-adjusted) for text overlays. */
34
+ export function computeNodeTextScreenScale(nodeWidth: number, zoomLevel: number): number {
35
+ return computeNodeTextWorldScale(nodeWidth, zoomLevel) * zoomLevel;
36
+ }
37
+
38
+ /**
39
+ * Screen-space overlay scale for description/semantic tooltips.
40
+ * - lower bound: 1x
41
+ * - upper bound: node-title-root + OVERLAY_FONT_UPPER_BOOST_PX
42
+ */
43
+ export function computeOverlayScale(zoomLevel: number): number {
44
+ const safeZoom = Math.max(zoomLevel, 0.0001);
45
+ const inverseScale = 1 / safeZoom;
46
+ const maxScale = (NODE_TEXT_BASE_FONT_PX + OVERLAY_FONT_UPPER_BOOST_PX) / NODE_TEXT_BASE_FONT_PX;
47
+ return Math.min(Math.max(inverseScale, 1), maxScale);
48
+ }
49
+
50
+ /** Scale for edge path labels in screen pixels. */
51
+ export function computeEdgeLabelScale(zoomLevel: number): number {
52
+ const safeZoom = Math.max(zoomLevel, 0.0001);
53
+ return Math.min(Math.max(EDGE_LABEL_BASE_FONT_PX / safeZoom, 10), 32);
54
+ }
@@ -0,0 +1,281 @@
1
+ // ─── Design System Constants ──────────────────────────────────────────────────
2
+ //
3
+ // Single source of truth for all color tokens, typography, spacing, and
4
+ // visual design constants used across the SDK and consuming apps.
5
+ //
6
+ // ## Color Priority
7
+ // 1. Data file values (node.color, layer.backgroundColor)
8
+ // 2. Fall back to these constants
9
+
10
+ // ── Visualli Design System v2 ─────────────────────────────────────────────────
11
+
12
+ export const DS_COLORS = {
13
+ light: {
14
+ bgApp: '#F0EDE6',
15
+ bgSurface: '#FAF8F4',
16
+ bgInput: '#FFFFFF',
17
+ border: '#DDD9D0',
18
+ borderDivider: '#E8E4DC',
19
+ primary: '#1B2D4F',
20
+ primaryHover: '#243B68',
21
+ textPrimary: '#1A1A18',
22
+ textSecondary: '#6B6860',
23
+ textMuted: '#747270',
24
+ textOnPrimary: '#FAF8F4',
25
+ hoverItem: 'rgba(27,45,79,0.07)',
26
+ focusRing: 'rgba(27,45,79,0.40)',
27
+ btnMuted: '#8E8C88',
28
+ },
29
+ dark: {
30
+ bgApp: '#141412',
31
+ bgSurface: '#1C1C1A',
32
+ bgInput: '#262622',
33
+ border: '#333330',
34
+ borderDivider: '#2A2A28',
35
+ primary: '#BB532D',
36
+ primaryHover: '#CC6440',
37
+ textPrimary: '#F0EDE6',
38
+ textSecondary: '#B0ABA3',
39
+ textMuted: '#8E8C88',
40
+ textOnPrimary: '#FAF8F4',
41
+ hoverItem: 'rgba(198,94,56,0.12)',
42
+ focusRing: 'rgba(198,94,56,0.50)',
43
+ btnMuted: '#4A4845',
44
+ },
45
+ } as const;
46
+
47
+ export const DS_TYPOGRAPHY = {
48
+ fontDisplay: "'Nunito', sans-serif",
49
+ fontBody: "'Nunito Sans', sans-serif",
50
+ logo: { size: '20px', weight: 800, letterSpacing: '0.04em' },
51
+ heading: { size: '16px', weight: 700, letterSpacing: '-0.2px' },
52
+ cta: { size: '11px', weight: 800, letterSpacing: '0.12em', transform: 'uppercase' as const },
53
+ body: { size: '13px', weight: 400 },
54
+ input: { size: '13px', weight: 300, style: 'italic' as const },
55
+ search: { size: '12px', weight: 400 },
56
+ username: { size: '13px', weight: 600 },
57
+ avatar: { size: '11px', weight: 700 },
58
+ footer: { size: '12px', weight: 400 },
59
+ empty: { size: '11px', weight: 300, style: 'italic' as const },
60
+ } as const;
61
+
62
+ export const DS_RADII = {
63
+ sidebar: 16,
64
+ input: 8,
65
+ cta: 7,
66
+ item: 6,
67
+ swatch: 10,
68
+ } as const;
69
+
70
+ // ── Brand Colors ──────────────────────────────────────────────────────────────
71
+
72
+ export const BRAND_COLORS = {
73
+ primary: {
74
+ 50: '#EFF6FF',
75
+ 100: '#DBEAFE',
76
+ 200: '#BFDBFE',
77
+ 300: '#93C5FD',
78
+ 400: '#60A5FA',
79
+ 500: '#3B82F6',
80
+ 600: '#2563EB',
81
+ 700: '#1D4ED8',
82
+ 800: '#1E40AF',
83
+ 900: '#1E3A8A',
84
+ },
85
+ secondary: {
86
+ 50: '#F0FDF4',
87
+ 100: '#DCFCE7',
88
+ 200: '#BBF7D0',
89
+ 300: '#86EFAC',
90
+ 400: '#4ADE80',
91
+ 500: '#10B981',
92
+ 600: '#059669',
93
+ 700: '#047857',
94
+ 800: '#065F46',
95
+ 900: '#064E3B',
96
+ },
97
+ accent: {
98
+ purple: '#8B5CF6',
99
+ cyan: '#06B6D4',
100
+ sky: '#0EA5E9',
101
+ lime: '#84CC16',
102
+ amber: '#F59E0B',
103
+ red: '#EF4444',
104
+ },
105
+ } as const;
106
+
107
+ // ── Level Colors ──────────────────────────────────────────────────────────────
108
+
109
+ export const LEVEL_COLORS = {
110
+ level0: BRAND_COLORS.primary[500],
111
+ level1: BRAND_COLORS.primary[400],
112
+ level2: BRAND_COLORS.primary[300],
113
+ level3: BRAND_COLORS.primary[200],
114
+ level4: BRAND_COLORS.primary[300],
115
+ level5: BRAND_COLORS.primary[400],
116
+ level6: BRAND_COLORS.primary[500],
117
+ level7: BRAND_COLORS.primary[600],
118
+ level8: BRAND_COLORS.primary[700],
119
+ } as const;
120
+
121
+ export const LEVEL_COLOR_ARRAY = [
122
+ LEVEL_COLORS.level0,
123
+ LEVEL_COLORS.level1,
124
+ LEVEL_COLORS.level2,
125
+ LEVEL_COLORS.level3,
126
+ LEVEL_COLORS.level4,
127
+ LEVEL_COLORS.level5,
128
+ LEVEL_COLORS.level6,
129
+ LEVEL_COLORS.level7,
130
+ LEVEL_COLORS.level8,
131
+ ] as const;
132
+
133
+ export const PARENT_FALLBACK_COLORS = [
134
+ '#12C7D3', '#325E8C', '#8A70A6', '#F54A57', '#FF6C4D',
135
+ '#F28C16', '#FFD347', '#7F7F7F', '#8D8D8D',
136
+ ] as const;
137
+
138
+ export const SEMANTIC_COLORS = {
139
+ success: BRAND_COLORS.secondary[500],
140
+ error: BRAND_COLORS.accent.red,
141
+ warning: BRAND_COLORS.accent.amber,
142
+ info: BRAND_COLORS.primary[500],
143
+ } as const;
144
+
145
+ // ── Theme Colors ──────────────────────────────────────────────────────────────
146
+
147
+ export const THEME_COLORS = {
148
+ light: {
149
+ background: { primary: '#FAF8F4', secondary: '#F0EDE6', tertiary: '#FFFFFF' },
150
+ text: { primary: '#1A1A18', secondary: '#6B6860', tertiary: '#747270' },
151
+ border: { primary: '#DDD9D0', secondary: '#E8E4DC' },
152
+ },
153
+ dark: {
154
+ background: { primary: '#1C1C1A', secondary: '#141412', tertiary: '#262622' },
155
+ text: { primary: '#F0EDE6', secondary: '#B0ABA3', tertiary: '#8E8C88' },
156
+ border: { primary: '#333330', secondary: '#2A2A28' },
157
+ },
158
+ } as const;
159
+
160
+ // ── Canvas Node Colors ────────────────────────────────────────────────────────
161
+
162
+ export const CANVAS_COLORS = {
163
+ node: {
164
+ border: '#FFFFFF',
165
+ titleColor: '#1e1b4b',
166
+ badgeBackground: '#FFC107',
167
+ badgeStroke: '#fcd34d',
168
+ },
169
+ } as const;
170
+
171
+ // ── Spacing ───────────────────────────────────────────────────────────────────
172
+
173
+ export const SPACING = {
174
+ xs: 4,
175
+ sm: 8,
176
+ md: 16,
177
+ lg: 24,
178
+ xl: 32,
179
+ '2xl': 48,
180
+ '3xl': 64,
181
+ '4xl': 96,
182
+ } as const;
183
+
184
+ export const LAYOUT_SPACING = {
185
+ nodeSpacing: 200,
186
+ levelSpacing: 150,
187
+ containerPadding: 20,
188
+ gridSize: 50,
189
+ } as const;
190
+
191
+ // ── Typography ────────────────────────────────────────────────────────────────
192
+
193
+ export const FONTS = {
194
+ primary: "'Inter', sans-serif",
195
+ secondary: "'Poppins', sans-serif",
196
+ mono: "'Roboto Mono', monospace",
197
+ } as const;
198
+
199
+ export const FONT_SIZES = {
200
+ xs: '0.75rem',
201
+ sm: '0.875rem',
202
+ base: '1rem',
203
+ lg: '1.125rem',
204
+ xl: '1.25rem',
205
+ '2xl': '1.5rem',
206
+ '3xl': '1.875rem',
207
+ '4xl': '2.25rem',
208
+ } as const;
209
+
210
+ // ── Helpers ───────────────────────────────────────────────────────────────────
211
+
212
+ /** Get a level color, cycling back to level 0 for levels > 8. */
213
+ export function getColorForLevel(level: number): string {
214
+ return LEVEL_COLOR_ARRAY[level % LEVEL_COLOR_ARRAY.length];
215
+ }
216
+
217
+ /**
218
+ * Darken a hex color by reducing HSL lightness.
219
+ * Returns a monochromatic darker version — used for border/stroke colors.
220
+ * Falls back to the original string if parsing fails.
221
+ */
222
+ export function darkenHexColor(hex: string, amount: number = 0.22): string {
223
+ if (!hex || !hex.startsWith('#')) return hex;
224
+ let r: number, g: number, b: number;
225
+ if (hex.length === 4) {
226
+ r = parseInt(hex[1] + hex[1], 16);
227
+ g = parseInt(hex[2] + hex[2], 16);
228
+ b = parseInt(hex[3] + hex[3], 16);
229
+ } else if (hex.length === 7) {
230
+ r = parseInt(hex.slice(1, 3), 16);
231
+ g = parseInt(hex.slice(3, 5), 16);
232
+ b = parseInt(hex.slice(5, 7), 16);
233
+ } else {
234
+ return hex;
235
+ }
236
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return hex;
237
+ const rn = r / 255, gn = g / 255, bn = b / 255;
238
+ const cmax = Math.max(rn, gn, bn), cmin = Math.min(rn, gn, bn);
239
+ const d = cmax - cmin;
240
+ const l = (cmax + cmin) / 2;
241
+ const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
242
+ let h = 0;
243
+ if (d !== 0) {
244
+ const seg = cmax === rn ? (gn - bn) / d : cmax === gn ? (bn - rn) / d + 2 : (rn - gn) / d + 4;
245
+ h = ((seg * 60) + 360) % 360;
246
+ }
247
+ const newL = Math.max(0, l - amount);
248
+ const c2 = (1 - Math.abs(2 * newL - 1)) * s;
249
+ const x2 = c2 * (1 - Math.abs(((h / 60) % 2) - 1));
250
+ const m2 = newL - c2 / 2;
251
+ let r1 = 0, g1 = 0, b1 = 0;
252
+ if (h < 60) { r1 = c2; g1 = x2; b1 = 0; }
253
+ else if (h < 120) { r1 = x2; g1 = c2; b1 = 0; }
254
+ else if (h < 180) { r1 = 0; g1 = c2; b1 = x2; }
255
+ else if (h < 240) { r1 = 0; g1 = x2; b1 = c2; }
256
+ else if (h < 300) { r1 = x2; g1 = 0; b1 = c2; }
257
+ else { r1 = c2; g1 = 0; b1 = x2; }
258
+ const toHex = (v: number) => Math.round((v + m2) * 255).toString(16).padStart(2, '0');
259
+ return `#${toHex(r1)}${toHex(g1)}${toHex(b1)}`;
260
+ }
261
+
262
+ export function getThemeBackground(
263
+ isDark: boolean,
264
+ variant: 'primary' | 'secondary' | 'tertiary' = 'primary',
265
+ ): string {
266
+ return isDark ? THEME_COLORS.dark.background[variant] : THEME_COLORS.light.background[variant];
267
+ }
268
+
269
+ export function getThemeText(
270
+ isDark: boolean,
271
+ variant: 'primary' | 'secondary' | 'tertiary' = 'primary',
272
+ ): string {
273
+ return isDark ? THEME_COLORS.dark.text[variant] : THEME_COLORS.light.text[variant];
274
+ }
275
+
276
+ export function getThemeBorder(
277
+ isDark: boolean,
278
+ variant: 'primary' | 'secondary' = 'primary',
279
+ ): string {
280
+ return isDark ? THEME_COLORS.dark.border[variant] : THEME_COLORS.light.border[variant];
281
+ }
@@ -0,0 +1,3 @@
1
+ export * from './performanceConstants.js';
2
+ export * from './design.js';
3
+ export * from './renderConfig.js';
@@ -0,0 +1,157 @@
1
+ // ─── Performance & Rendering Constants ───────────────────────────────────────
2
+
3
+ // ── FPS ───────────────────────────────────────────────────────────────────────
4
+
5
+ export const FPS_TARGET = 60;
6
+ export const FPS_WARNING_THRESHOLD = 45;
7
+ export const FPS_CRITICAL_THRESHOLD = 30;
8
+ export const TARGET_FRAME_TIME = 1000 / FPS_TARGET; // ~16.67 ms
9
+ export const MAX_FRAME_TIME = 18; // p95 threshold
10
+
11
+ /** Legacy frame-rate targets kept for backward compatibility. */
12
+ export const FPS_TARGETS = {
13
+ PAN_MIN: 30,
14
+ ZOOM_TARGET: 60,
15
+ WARNING_THRESHOLD: 20,
16
+ } as const;
17
+
18
+ // ── Timing Thresholds (ms) ────────────────────────────────────────────────────
19
+
20
+ export const TIMING_THRESHOLDS = {
21
+ VIEWPORT_QUERY_MAX: 5,
22
+ SPATIAL_UPDATE_MAX: 10,
23
+ DRAG_RESPONSE_MAX: 16,
24
+ LOAD_5K_TARGET: 2000,
25
+ LOAD_10K_TARGET: 3000,
26
+ VIEWPORT_DEBOUNCE: 100,
27
+ SPATIAL_INDEX_BUILD: 50,
28
+ } as const;
29
+
30
+ // ── Memory Limits (MB) ────────────────────────────────────────────────────────
31
+
32
+ export const MEMORY_TARGET_MB = 500;
33
+ export const MEMORY_WARNING_MB = 600;
34
+ export const MEMORY_CRITICAL_MB = 800;
35
+
36
+ export const MEMORY_LIMITS = {
37
+ MAX_10K_NODES: 500,
38
+ MAX_GROWTH_PERCENT: 20,
39
+ WARNING_THRESHOLD: 400,
40
+ } as const;
41
+
42
+ // ── Viewport Culling ──────────────────────────────────────────────────────────
43
+
44
+ export const VIEWPORT_MARGIN = 0.1;
45
+ export const MAX_VISIBLE_NODES = 800;
46
+ export const CULLING_QUERY_TIME_BUDGET = 5;
47
+
48
+ // ── Spatial Index ─────────────────────────────────────────────────────────────
49
+
50
+ export const RBUSH_MAX_ENTRIES = 16;
51
+ export const SPATIAL_INDEX_BUILD_BUDGET = 50;
52
+
53
+ // ── Node Dimensions ───────────────────────────────────────────────────────────
54
+
55
+ export const NODE_MIN_WIDTH = 200;
56
+ export const NODE_HEIGHT = 80;
57
+ export const NODE_PADDING = 40;
58
+ export const CHAR_WIDTH = 8;
59
+ export const TITLE_MAX_LENGTH = 150;
60
+
61
+ // ── Visual Styling ────────────────────────────────────────────────────────────
62
+
63
+ export const CORNER_RADIUS = 8;
64
+ export const BORDER_WIDTH_NORMAL = 2;
65
+ export const BORDER_WIDTH_SELECTED = 3;
66
+
67
+ export const COLOR_SELECTED = '#1B2D4F';
68
+ export const COLOR_BORDER_LIGHT = '#4A3728';
69
+ export const COLOR_BORDER_DARK = '#FAF6F1';
70
+ export const COLOR_DEFAULT_NODE = '#FAF6F1';
71
+
72
+ // ── Zoom Constraints ──────────────────────────────────────────────────────────
73
+
74
+ export const ZOOM_MIN = 0.05;
75
+ export const ZOOM_MAX = 5.0;
76
+ export const ZOOM_DEFAULT = 1.0;
77
+ export const ZOOM_STEP = 0.1;
78
+ export const ZOOM_WHEEL_SENSITIVITY = 0.001;
79
+
80
+ export const ZOOM_NAV_IN_THRESHOLD = 2.7;
81
+ export const ZOOM_NAV_OUT_THRESHOLD = 0.4;
82
+ export const TEXT_LABEL_HIDE_BELOW_ZOOM = 0.3;
83
+
84
+ // ── Quality Level Presets ─────────────────────────────────────────────────────
85
+
86
+ export const QUALITY_HIGH = {
87
+ shadows: true,
88
+ bezierCurves: true,
89
+ animations: true,
90
+ textQuality: 'high' as const,
91
+ antialiasing: true,
92
+ };
93
+
94
+ export const QUALITY_MEDIUM = {
95
+ shadows: false,
96
+ bezierCurves: true,
97
+ animations: true,
98
+ textQuality: 'medium' as const,
99
+ antialiasing: true,
100
+ };
101
+
102
+ export const QUALITY_LOW = {
103
+ shadows: false,
104
+ bezierCurves: false,
105
+ animations: false,
106
+ textQuality: 'low' as const,
107
+ antialiasing: false,
108
+ };
109
+
110
+ // ── Performance Monitoring ────────────────────────────────────────────────────
111
+
112
+ export const FPS_SAMPLE_WINDOW = 60;
113
+ export const FPS_UPDATE_INTERVAL = 1000;
114
+ export const MEMORY_CHECK_INTERVAL = 5000;
115
+ export const PERFORMANCE_TOAST_COOLDOWN = 10000;
116
+
117
+ // ── Auto-Adjustment ───────────────────────────────────────────────────────────
118
+
119
+ export const FPS_DOWNGRADE_THRESHOLD = 45;
120
+ export const FPS_UPGRADE_THRESHOLD = 58;
121
+ export const FPS_STABILIZATION_FRAMES = 180;
122
+
123
+ // ── Canvas Dimensions ─────────────────────────────────────────────────────────
124
+
125
+ export const DEFAULT_CANVAS_WIDTH = 1920;
126
+ export const DEFAULT_CANVAS_HEIGHT = 1080;
127
+
128
+ // ── Interaction ───────────────────────────────────────────────────────────────
129
+
130
+ export const CLICK_THRESHOLD = 5;
131
+ export const DOUBLE_CLICK_DELAY = 300;
132
+ export const DRAG_START_DISTANCE = 10;
133
+
134
+ // ── Data Validation ───────────────────────────────────────────────────────────
135
+
136
+ export const MAX_NODES = 20000;
137
+ export const CIRCULAR_REFERENCE_MAX_DEPTH = 1000;
138
+
139
+ // ── Debounce / Throttle ───────────────────────────────────────────────────────
140
+
141
+ export const RESIZE_DEBOUNCE = 300;
142
+ export const SCROLL_THROTTLE = 16;
143
+ export const SEARCH_DEBOUNCE = 500;
144
+
145
+ /** Node count thresholds that gate optimisation strategies. */
146
+ export const NODE_COUNT_THRESHOLDS = {
147
+ SPATIAL_INDEX_MIN: 100,
148
+ VIEWPORT_CULLING_MIN: 500,
149
+ CANVAS_RENDERING_MIN: 2000,
150
+ DOM_RENDERING_MAX: 5000,
151
+ } as const;
152
+
153
+ /** Viewport buffer configuration. */
154
+ export const VIEWPORT_CONFIG = {
155
+ BUFFER_PERCENT: 0.2,
156
+ MIN_VISIBLE_NODES: 10,
157
+ } as const;