@visualli/core 0.1.7 → 0.2.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.
Files changed (127) hide show
  1. package/README.md +37 -105
  2. package/dist/chunk-25AJTPMS.js +68 -0
  3. package/dist/chunk-25AJTPMS.js.map +1 -0
  4. package/dist/{chunk-6NXADJK2.js → chunk-7NJWM7ZE.js} +84 -117
  5. package/dist/chunk-7NJWM7ZE.js.map +1 -0
  6. package/dist/chunk-KVSM3APM.js +96 -0
  7. package/dist/chunk-KVSM3APM.js.map +1 -0
  8. package/dist/{chunk-PDP5CVUL.js → chunk-LOQLNYB3.js} +2 -2
  9. package/dist/chunk-LV3EXUQY.js +753 -0
  10. package/dist/chunk-LV3EXUQY.js.map +1 -0
  11. package/dist/chunk-N7HCYULH.js +12 -0
  12. package/dist/chunk-N7HCYULH.js.map +1 -0
  13. package/dist/{chunk-I75TMOE4.js → chunk-P6PKWVYF.js} +2 -2
  14. package/dist/chunk-TPLJJKAU.js +151 -0
  15. package/dist/chunk-TPLJJKAU.js.map +1 -0
  16. package/dist/chunk-UAMEJG2C.js +267 -0
  17. package/dist/chunk-UAMEJG2C.js.map +1 -0
  18. package/dist/{chunk-LJYSKSCE.js → chunk-W2CAOOJJ.js} +6 -6
  19. package/dist/chunk-W2CAOOJJ.js.map +1 -0
  20. package/dist/config/index.cjs +250 -349
  21. package/dist/config/index.cjs.map +1 -1
  22. package/dist/config/index.d.cts +107 -66
  23. package/dist/config/index.d.ts +107 -66
  24. package/dist/config/index.js +52 -34
  25. package/dist/constants/index.cjs +14 -333
  26. package/dist/constants/index.cjs.map +1 -1
  27. package/dist/constants/index.d.cts +5 -243
  28. package/dist/constants/index.d.ts +5 -243
  29. package/dist/constants/index.js +5 -75
  30. package/dist/designSystem-BOrZHfEq.d.cts +150 -0
  31. package/dist/designSystem-BOrZHfEq.d.ts +150 -0
  32. package/dist/document-BFBwn93X.d.ts +26 -0
  33. package/dist/document-MEJEfxl3.d.cts +26 -0
  34. package/dist/ideaSize-CLDKeyqD.d.cts +11 -0
  35. package/dist/ideaSize-DxxGy2E8.d.ts +11 -0
  36. package/dist/index-BmILeKj6.d.cts +99 -0
  37. package/dist/index-CcPRwVUL.d.ts +99 -0
  38. package/dist/index.cjs +1636 -961
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +350 -27
  41. package/dist/index.d.ts +350 -27
  42. package/dist/index.js +434 -179
  43. package/dist/index.js.map +1 -1
  44. package/dist/{document-B3hnNr-F.d.cts → layer-B2A3Ki48.d.cts} +2 -16
  45. package/dist/{document-B3hnNr-F.d.ts → layer-B2A3Ki48.d.ts} +2 -16
  46. package/dist/layout/index.d.cts +2 -1
  47. package/dist/layout/index.d.ts +2 -1
  48. package/dist/mindmap-C3Qr0qKW.d.cts +182 -0
  49. package/dist/mindmap-DHeVa86G.d.ts +182 -0
  50. package/dist/parser/index.cjs +745 -179
  51. package/dist/parser/index.cjs.map +1 -1
  52. package/dist/parser/index.d.cts +23 -8
  53. package/dist/parser/index.d.ts +23 -8
  54. package/dist/parser/index.js +7 -2
  55. package/dist/performance/index.cjs +13 -0
  56. package/dist/performance/index.cjs.map +1 -1
  57. package/dist/performance/index.js +2 -2
  58. package/dist/theme/index.cjs +884 -0
  59. package/dist/theme/index.cjs.map +1 -0
  60. package/dist/theme/index.d.cts +2 -0
  61. package/dist/theme/index.d.ts +2 -0
  62. package/dist/theme/index.js +53 -0
  63. package/dist/types/index.d.cts +4 -2
  64. package/dist/types/index.d.ts +4 -2
  65. package/dist/viewport/index.cjs +17 -6
  66. package/dist/viewport/index.cjs.map +1 -1
  67. package/dist/viewport/index.d.cts +6 -4
  68. package/dist/viewport/index.d.ts +6 -4
  69. package/dist/viewport/index.js +2 -2
  70. package/licenses/visualli-design-system/LICENSE +9 -0
  71. package/licenses/visualli-design-system/NOTICE.md +6 -0
  72. package/package.json +12 -4
  73. package/src/config/README.md +2 -4
  74. package/src/config/USAGE_EXAMPLES.md +0 -4
  75. package/src/config/index.ts +3 -4
  76. package/src/constants/index.ts +1 -1
  77. package/src/constants/layout.ts +11 -0
  78. package/src/constants/performanceConstants.ts +8 -27
  79. package/src/generated/README.md +12 -0
  80. package/src/generated/designSystem.ts +758 -0
  81. package/src/generated/geometry/blob.ts +99 -0
  82. package/src/generated/geometry/blobShapes.ts +4 -0
  83. package/src/generated/geometry/color.ts +37 -0
  84. package/src/generated/geometry/container.ts +84 -0
  85. package/src/generated/geometry/detail.ts +22 -0
  86. package/src/generated/geometry/idea.ts +56 -0
  87. package/src/generated/geometry/interaction.ts +122 -0
  88. package/src/generated/geometry/motion.ts +140 -0
  89. package/src/index.ts +18 -1
  90. package/src/parser/configUtils.ts +12 -19
  91. package/src/parser/visualliConverter.ts +71 -112
  92. package/src/parser/visualliParser.ts +16 -1
  93. package/src/rendering/culling.ts +16 -12
  94. package/src/rendering/ideaSize.ts +21 -0
  95. package/src/rendering/nodeGeometry.ts +97 -0
  96. package/src/theme/index.ts +136 -0
  97. package/src/types/document.ts +9 -0
  98. package/src/types/layer.ts +1 -1
  99. package/src/types/mindmap.ts +13 -0
  100. package/src/utils/navigation.ts +33 -63
  101. package/src/viewport/viewportUtils.ts +6 -4
  102. package/dist/animations/index.cjs +0 -146
  103. package/dist/animations/index.cjs.map +0 -1
  104. package/dist/animations/index.d.cts +0 -81
  105. package/dist/animations/index.d.ts +0 -81
  106. package/dist/animations/index.js +0 -35
  107. package/dist/chunk-6NXADJK2.js.map +0 -1
  108. package/dist/chunk-F36SMC3K.js +0 -106
  109. package/dist/chunk-F36SMC3K.js.map +0 -1
  110. package/dist/chunk-G5H3P4YZ.js +0 -273
  111. package/dist/chunk-G5H3P4YZ.js.map +0 -1
  112. package/dist/chunk-LDJRGT2Q.js +0 -163
  113. package/dist/chunk-LDJRGT2Q.js.map +0 -1
  114. package/dist/chunk-LJYSKSCE.js.map +0 -1
  115. package/dist/chunk-LNTJTDB2.js +0 -349
  116. package/dist/chunk-LNTJTDB2.js.map +0 -1
  117. package/dist/mindmap-DWS0WZT_.d.cts +0 -118
  118. package/dist/mindmap-DWS0WZT_.d.ts +0 -118
  119. package/src/animations/constants.ts +0 -72
  120. package/src/animations/easing.ts +0 -70
  121. package/src/animations/index.ts +0 -2
  122. package/src/config/blobShapes.ts +0 -146
  123. package/src/config/textScaling.ts +0 -54
  124. package/src/constants/design.ts +0 -281
  125. /package/dist/{chunk-PDP5CVUL.js.map → chunk-LOQLNYB3.js.map} +0 -0
  126. /package/dist/{chunk-I75TMOE4.js.map → chunk-P6PKWVYF.js.map} +0 -0
  127. /package/dist/{animations → theme}/index.js.map +0 -0
package/README.md CHANGED
@@ -7,13 +7,13 @@ Framework-agnostic core logic, types, and algorithms for Visualli. Zero React /
7
7
  | Module | What it provides |
8
8
  |--------|------------------|
9
9
  | **Types** | Full TypeScript interfaces for `VisualliDocument`, `VisualliLayer`, `FlatNode`, `ViewportState`, `RenderConfig`, `Connection`, and more |
10
- | **Parser** | Parse `.visualli` JSONL files, convert layers to flat nodes, resolve spatial overlaps |
11
- | **Layout** | Circular and linear layout algorithms with automatic radius/spacing calculation |
12
- | **Viewport** | Pure pan/zoom/bounds math — world↔screen coordinate transforms, fit-to-screen |
10
+ | **Parser** | Parse `.visualli` JSONL files and convert layers to flat nodes: each idea sized for its label and coloured by the design system's rules, placed where the file puts it |
11
+ | **Layout** | Circular and linear layouts for files whose ideas carry no positions |
12
+ | **Design system** | The Visualli design system's rules, copied verbatim from `design-system/` (tokens, blob geometry, motion, idea size, colours, containers, fit, peek, trail, level of detail) |
13
+ | **Viewport** | Pure pan/zoom/bounds math — world↔screen coordinate transforms, fit to view |
13
14
  | **Spatial index** | RBush-backed O(log n) spatial index for viewport culling of large node graphs |
14
- | **Animations** | Easing functions (cubic, quartic, sine, bezier LUT) and timing phase constants |
15
15
  | **Performance** | rAF-based FPS monitor, memory monitor, profiler |
16
- | **Constants** | Design tokens (colors, spacing, typography), zoom limits, FPS targets, render config |
16
+ | **Constants** | Zoom limits, FPS targets, render config |
17
17
 
18
18
  ## Installation
19
19
 
@@ -36,6 +36,10 @@ const doc = parseVisualliFile(rawJsonlString);
36
36
  // Get the root layer
37
37
  const rootLayerId = [...doc.layers.keys()][0];
38
38
  const nodes = getNodesForLayer(doc, rootLayerId); // FlatNode[]
39
+
40
+ // Ideas are sized for their labels (the design system's idea.ts). Renderers pass their own text measurement in the
41
+ // label face; without one, an approximation is used.
42
+ const sized = getNodesForLayer(doc, rootLayerId, { measure: (text, px) => ctx.measureText(text).width, labelScale: 1 });
39
43
  ```
40
44
 
41
45
  ### Apply layouts
@@ -75,25 +79,15 @@ index.bulkLoad(nodes.map(n => ({
75
79
  const visible = index.query(viewportBounds); // string[] — node IDs
76
80
  ```
77
81
 
78
- ### Easing
79
-
80
- ```ts
81
- import { easeInOutCubic, easeOutCubic, createCubicBezier } from '@visualli/core';
82
-
83
- const t = easeInOutCubic(progress); // 0..1 → 0..1
84
- const custom = createCubicBezier(0.4, 0, 0.2, 1); // CSS timing function
85
- ```
86
-
87
- ### Colors & design tokens
82
+ ### Colours
88
83
 
89
84
  ```ts
90
- import {
91
- getColorForLevel, getThemeBackground,
92
- LEVEL_COLOR_ARRAY, DS_COLORS, BRAND_COLORS,
93
- } from '@visualli/core';
85
+ import { ideaColor, ideaStyle } from '@visualli/core';
94
86
 
95
- const nodeColor = getColorForLevel(2); // '#...' for level 2
96
- const bg = getThemeBackground(isDark); // canvas background
87
+ ideaColor('teal', 0); // { topic: 'teal', custom: null } — a topic name
88
+ ideaColor(undefined, 2); // { topic: 'iris', custom: null } — no colour: topics in sibling order
89
+ ideaColor('#b7e7f3', 0); // { topic: null, custom: { fill: '#b7e7f3', ring: '#8fb4be' } } — drawn as given
90
+ ideaStyle('dark', node); // { fill, ring } in a theme
97
91
  ```
98
92
 
99
93
  ## Module Reference
@@ -104,7 +98,7 @@ const bg = getThemeBackground(isDark); // canvas background
104
98
  |--------|-------------|
105
99
  | `VisualliDocument` | Top-level document (layers Map, extensions Map) |
106
100
  | `VisualliLayer` | A single layer: level, nodes, connections, containers |
107
- | `FlatNode` | Renderable node (position, size, title, color, level, parentId) |
101
+ | `FlatNode` | Renderable idea: position, size for its label (`width` / `height`, `kind`), title, colour (`topic` or `custom`), level, parentId |
108
102
  | `NodeMap` | `Map<string, FlatNode>` |
109
103
  | `ViewportState` | `centerX/Y`, `zoomLevel`, `rotation`, `visibleBounds` |
110
104
  | `RenderConfig` | Quality level, FPS target, culling flag, render mode |
@@ -116,15 +110,15 @@ const bg = getThemeBackground(isDark); // canvas background
116
110
  |--------|-------------|
117
111
  | `parseVisualliFile(str)` | Parse JSONL → `VisualliDocument` |
118
112
  | `loadVisualliFileFromFile(file)` | Parse a browser `File` object |
119
- | `getNodesForLayer(doc, layerId)` | `FlatNode[]` for one layer |
120
- | `convertVisualliToFlatNodes(doc)` | All layers → `NodeMap` |
113
+ | `getNodesForLayer(doc, layerId, opts?)` | `FlatNode[]` for one layer (`opts`: `measure`, `labelScale` for sizing ideas) |
114
+ | `convertVisualliToFlatNodes(doc, opts?)` | All layers → `NodeMap` |
121
115
  | `getChildLayers(doc, layerId)` | Direct child layers |
122
116
 
123
117
  ### `layout/`
124
118
 
125
119
  | Export | Description |
126
120
  |--------|-------------|
127
- | `applyCircularLayout(nodes)` | Radial arrangement |
121
+ | `applyCircularLayout(nodes)` | Radial arrangement (used when the file gives no positions) |
128
122
  | `applyLinearHorizontalLayout(nodes)` | Left-to-right tree |
129
123
  | `applyLinearVerticalLayout(nodes)` | Top-to-bottom tree |
130
124
  | `resolveCollisions(nodes)` | Push apart overlapping nodes |
@@ -137,7 +131,7 @@ const bg = getThemeBackground(isDark); // canvas background
137
131
  | `zoomViewport(delta, vp, px, py, w, h)` | Zoom to cursor |
138
132
  | `panViewport(dx, dy, vp)` | Translate camera |
139
133
  | `setViewportCenter(x, y, vp)` | Teleport center |
140
- | `clampZoom(level)` | Clamp to `[ZOOM_MIN, ZOOM_MAX]` |
134
+ | `clampZoom(level, fit?)` | Clamp to `[ZOOM_MIN, ZOOM_MAX]` × the layer's fit scale (the design system's limits are relative to the fit) |
141
135
  | `worldToScreen(x, y, vp, w, h)` | World → screen px |
142
136
  | `screenToWorld(x, y, vp, w, h)` | Screen px → world |
143
137
 
@@ -145,12 +139,11 @@ const bg = getThemeBackground(isDark); // canvas background
145
139
 
146
140
  | Export | Description |
147
141
  |--------|-------------|
148
- | `ZOOM_MIN` / `ZOOM_MAX` | `0.3` / `5.0` |
149
- | `ZOOM_NAV_IN_THRESHOLD` | `2.7` — zoom level that triggers drill-in |
150
- | `ZOOM_NAV_OUT_THRESHOLD` | `0.4` — zoom level that triggers back navigation |
151
- | `LEVEL_COLOR_ARRAY` | 10-color palette indexed by node level |
142
+ | `ZOOM_MIN` / `ZOOM_MAX` | `0.3` / `5.0` — the design system's `VIEW`, relative to the layer's fit |
152
143
  | `DEFAULT_RENDER_CONFIG` | Baseline quality settings |
153
- | `ANIMATION_PHASES` | Duration constants for layer transitions |
144
+
145
+ Motion, idea size, colours, fit, peek placement and the depth trail are the design system's rules, exported verbatim
146
+ (`MOTION`, `layerReveal`, `IDEA`, `ideaSize`, `labelGrowth`, `ideaColor`, `layerBounds`, `fitView`, `peekPosition`, `TRAIL` …).
154
147
 
155
148
  ## TypeScript
156
149
 
@@ -178,86 +171,25 @@ npm run typecheck # tsc --noEmit (0 errors expected)
178
171
 
179
172
  ```
180
173
  src/
181
- ├── types/ TypeScript interfaces, enums, Zod schemas
182
- │ ├── mindmap.ts MindMapNode, FlatNode, NodeMap, ViewportState, RenderConfig …
183
- │ ├── meta.ts VisualliMeta
184
- │ ├── layer.ts VisualliLayer, LayerNode, LayerConnection, LayerContainer …
185
- │ ├── extension.ts VisualliExtension
186
- │ ├── document.ts VisualliDocument
187
- │ ├── schema.ts JSON-schema-aligned types + re-exports
188
- │ ├── processing.ts ConnectionState, ProcessingState, type guards
189
- │ └── sse.ts SSEEvent discriminated union + Zod validation + helpers
190
- │
191
- ├── layout/ Pure layout algorithms (no side-effects)
192
- │ ├── circularLayout.ts applyCircularLayout, calculateOptimalRadiusPercentage
193
- │ ├── linearLayout.ts applyLinearHorizontalLayout, applyLinearVerticalLayout
194
- │ └── layoutUtils.ts getNodeBounds, resolveCollisions, optimizeLayout
195
- │
196
- ├── parser/ File parsing and data conversion
197
- │ ├── visualliParser.ts parseVisualliFile, loadVisualliFile, getChildLayers …
198
- │ ├── visualliConverter.ts convertVisualliToFlatNodes, resolveNodeOverlaps …
199
- │ ├── mindmapUtils.ts flattenNodes, getVisibleNodes, calculateDistance …
200
- │ └── configUtils.ts createMindMapConfig, generateSampleConfig
201
- │
202
- ├── services/ Network utilities (fetch, SSE, logging)
203
- │ ├── api.ts initApi, apiRequest, ApiError, mindmapApi, correlationId helpers
204
- │ ├── logger.ts Logger class (buffered remote + console)
205
- │ └── sseClient.ts createSSEClient (fetch ReadableStream, reconnection)
206
- │
207
- ├── constants/ Design tokens, performance thresholds, render config
208
- │ ├── performanceConstants.ts FPS targets, zoom limits, node dimensions …
209
- │ ├── design.ts DS_COLORS, LEVEL_COLORS, BRAND_COLORS, helpers …
210
- │ └── renderConfig.ts DEFAULT_RENDER_CONFIG, FEATURE_FLAGS, helpers …
211
- │
212
- ├── viewport/ Coordinate math and pan/zoom operations
213
- │ └── viewportUtils.ts calculateViewportBounds, worldToScreen, zoomViewport …
174
+ ├── generated/ The Visualli design system, generated from design-system/ (never edited by hand)
175
+ │ ├── designSystem.ts TOKENS, METRICS, TYPE_STYLES, CANVAS_STYLE (from css/spec.css)
176
+ │ └── geometry/ blob, motion, idea, color, container, interaction, detail — copied verbatim
214
177
  │
178
+ ├── types/ TypeScript interfaces (FlatNode, VisualliDocument, VisualliLayer, ViewportState …)
179
+ ├── parser/ parseVisualliFile; convertLayerToFlatNodes / getNodesForLayer (sizes, colours, positions)
180
+ ├── layout/ Circular and linear layouts, for layers whose ideas carry no positions
181
+ ├── rendering/ nodeGeometry (radii, rings, connector endpoints), ideaSize (idea.ts + measurement), culling
182
+ ├── theme/ Theme and comfort resolution; ideaColor / ideaStyle (the design system's colour rule)
183
+ ├── utils/ Layer navigation and fit to view (layerBounds / fitView)
184
+ ├── viewport/ Coordinate math and pan / zoom (limits relative to the layer's fit)
185
+ ├── constants/ Zoom limits, performance thresholds, render config
215
186
  ├── performance/ FPS / memory monitoring (browser APIs, no React)
216
- │ └── performanceMonitor.ts FPSMonitor, MemoryMonitor, PerformanceProfiler
217
- │
218
- ├── animations/ Easing functions and timing constants
219
- │ ├── easing.ts easeInOutCubic, createCubicBezier …
220
- │ └── constants.ts ANIMATION_DURATION, ANIMATION_PHASES, COOLDOWN …
221
- │
222
187
  └── spatial/ RBush spatial index for O(log n) viewport culling
223
- └── spatialIndex.ts RBushSpatialIndex, ISpatialIndex, BoundingBox
224
- ```
225
-
226
- ## Installation
227
-
228
- ```bash
229
- npm install @visualli/core
230
- ```
231
-
232
- > **Dependencies:** `zod` (schema validation) · `rbush` (spatial index)
233
-
234
- ## Quick Start
235
-
236
- ```ts
237
- // Parse a .visualli document
238
- import { parseVisualliFile, convertVisualliToFlatNodes } from '@visualli/core/parser';
239
-
240
- const doc = parseVisualliFile(rawJsonlString);
241
- const nodeMap = convertVisualliToFlatNodes(doc);
242
-
243
- // Apply a circular layout
244
- import { applyCircularLayout } from '@visualli/core/layout';
245
- const nodes = [...nodeMap.values()];
246
- applyCircularLayout(nodes);
247
-
248
- // Viewport math
249
- import { zoomViewport } from '@visualli/core/viewport';
250
- const next = zoomViewport(0.1, currentViewport, pivotX, pivotY);
251
-
252
- // API client
253
- import { initApi, mindmapApi } from '@visualli/core/services';
254
- initApi({ baseUrl: 'https://api.example.com/api' });
255
- const list = await mindmapApi.getAllMindmaps();
256
188
  ```
257
189
 
258
190
  ## Design Principles
259
191
 
260
192
  - **No React** — no hooks, no JSX, no Context
261
- - **No DOM manipulation** — pure data transforms; browser APIs (`fetch`, `FileReader`) are used only at IO boundaries
262
- - **No Vite env vars** — configure at runtime via `initApi()`
193
+ - **No DOM manipulation** — pure data transforms; text measurement is passed in by the renderer
194
+ - **No design decisions of its own** — every visual rule comes from the Visualli design system (`design-system/`)
263
195
  - **Tree-shakeable** — import from sub-paths to avoid bundling unused modules
@@ -0,0 +1,68 @@
1
+ // src/generated/geometry/idea.ts
2
+ var LABEL_GROWTH = { idea: 1.3, connector: 1.6 };
3
+ var IDEA = {
4
+ /** Width an idea starts at… */
5
+ baseWidth: { root: 240, node: 200, mini: 120 },
6
+ /** …and the widest it gets before it grows taller instead. */
7
+ maxWidth: { root: 400, node: 360, mini: 200 },
8
+ /** Width grows in steps of this. */
9
+ step: 20,
10
+ /** Height = width × aspect (at least `minRy` × 2), more if the label needs it. */
11
+ aspect: 0.74,
12
+ minRy: 64,
13
+ /** The label's box is the idea's width less this (22 each side). */
14
+ labelInset: 44,
15
+ /** The idea widens until its label fits in this many lines. */
16
+ labelLines: 3,
17
+ /** Label size (before the label scale and zoom growth), weight from the canvas type styles. */
18
+ labelSize: { root: 30, node: 22, mini: 15 },
19
+ labelLineHeight: 1.15,
20
+ /** The label may fill at most this share of the idea's height. */
21
+ labelFillHeight: 0.7
22
+ };
23
+ function wrapLines(text, maxWidth, measure) {
24
+ const words = text.split(/\s+/).filter(Boolean);
25
+ const lines = [];
26
+ let cur = "";
27
+ for (const w of words) {
28
+ const next = cur ? `${cur} ${w}` : w;
29
+ if (cur && measure(next) > maxWidth) {
30
+ lines.push(cur);
31
+ cur = w;
32
+ } else cur = next;
33
+ }
34
+ if (cur) lines.push(cur);
35
+ return lines.length ? lines : [""];
36
+ }
37
+ function ideaSize(kind, label, measure, labelScale = 1) {
38
+ const px = IDEA.labelSize[kind] * labelScale * LABEL_GROWTH.idea;
39
+ const fit = (w) => wrapLines(label, w - IDEA.labelInset, (t) => measure(t, px));
40
+ const base = IDEA.baseWidth[kind], max = IDEA.maxWidth[kind];
41
+ let width = base, lines = fit(width);
42
+ while (lines.length > IDEA.labelLines && width < max) {
43
+ width = Math.min(max, width + IDEA.step);
44
+ lines = fit(width);
45
+ }
46
+ const rx = width / 2;
47
+ const ry = Math.max(IDEA.minRy, rx * IDEA.aspect, lines.length * px * IDEA.labelLineHeight / 2 / IDEA.labelFillHeight);
48
+ return { width, height: ry * 2, rx, ry, lines };
49
+ }
50
+ function labelGrowth(scale) {
51
+ const inv = 1 / Math.max(scale, 1e-4);
52
+ return { idea: Math.max(1, Math.min(inv, LABEL_GROWTH.idea)), connector: Math.max(1, Math.min(inv, LABEL_GROWTH.connector)) };
53
+ }
54
+
55
+ // src/rendering/ideaSize.ts
56
+ var approximateMeasure = (text, px) => text.length * px * 0.5;
57
+ var ideaKindOf = (layer, index) => layer.level === 0 && (layer.layout ?? "radial") === "radial" && index === 0 ? "root" : "node";
58
+
59
+ export {
60
+ LABEL_GROWTH,
61
+ IDEA,
62
+ wrapLines,
63
+ ideaSize,
64
+ labelGrowth,
65
+ approximateMeasure,
66
+ ideaKindOf
67
+ };
68
+ //# sourceMappingURL=chunk-25AJTPMS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generated/geometry/idea.ts","../src/rendering/ideaSize.ts"],"sourcesContent":["// Generated from visualli.ai design-system/components/src/idea.ts — do not edit here.\n/* An idea's size and its label. An idea grows to show its whole label: it starts at its base width and widens in\n steps until the label fits in `labelLines` lines; at its widest it grows taller instead, so a label is never cut.\n Labels (and connector labels) grow a little when the map is zoomed out, so they stay readable. Every renderer sizes\n ideas with these rules, measuring text with its own font engine (`measure`). No dependencies, so it can travel alone. */\n\nexport type IdeaKind = 'root' | 'node' | 'mini';\n\n/** How much labels grow when zoomed out (`scale` = the map's zoom): never smaller than their size, up to these caps.\n * Idea labels and container names use `idea`; connector labels `connector`. Below 100% they grow; above, they don't. */\nexport const LABEL_GROWTH = { idea: 1.3, connector: 1.6 } as const;\n\nexport const IDEA = {\n /** Width an idea starts at… */ baseWidth: { root: 240, node: 200, mini: 120 },\n /** …and the widest it gets before it grows taller instead. */ maxWidth: { root: 400, node: 360, mini: 200 },\n /** Width grows in steps of this. */ step: 20,\n /** Height = width × aspect (at least `minRy` × 2), more if the label needs it. */ aspect: 0.74, minRy: 64,\n /** The label's box is the idea's width less this (22 each side). */ labelInset: 44,\n /** The idea widens until its label fits in this many lines. */ labelLines: 3,\n /** Label size (before the label scale and zoom growth), weight from the canvas type styles. */ labelSize: { root: 30, node: 22, mini: 15 },\n labelLineHeight: 1.15,\n /** The label may fill at most this share of the idea's height. */ labelFillHeight: 0.7,\n} as const;\n\n/** Greedy word wrap of `text` into lines no wider than `maxWidth` (a single word longer than that gets its own line). */\nexport function wrapLines(text: string, maxWidth: number, measure: (s: string) => number): string[] {\n const words = text.split(/\\s+/).filter(Boolean);\n const lines: string[] = [];\n let cur = '';\n for (const w of words) {\n const next = cur ? `${cur} ${w}` : w;\n if (cur && measure(next) > maxWidth) { lines.push(cur); cur = w; } else cur = next;\n }\n if (cur) lines.push(cur);\n return lines.length ? lines : [''];\n}\n\n/** An idea's size for its label: width, half-width / half-height for the blob, and the label's lines. The label is\n * measured at its largest drawn size — its type size × `labelScale` (Comfort: larger text) × LABEL_GROWTH.idea (zoomed\n * out) — so it fits at every zoom and the idea never changes size while zooming. `measure(text, px)` returns the\n * text's width in px in the label's face at `px`. */\nexport function ideaSize(kind: IdeaKind, label: string, measure: (text: string, px: number) => number, labelScale = 1) {\n const px = IDEA.labelSize[kind] * labelScale * LABEL_GROWTH.idea;\n const fit = (w: number) => wrapLines(label, w - IDEA.labelInset, (t) => measure(t, px));\n const base: number = IDEA.baseWidth[kind], max: number = IDEA.maxWidth[kind];\n let width = base, lines = fit(width);\n while (lines.length > IDEA.labelLines && width < max) { width = Math.min(max, width + IDEA.step); lines = fit(width); }\n const rx = width / 2;\n const ry = Math.max(IDEA.minRy, rx * IDEA.aspect, (lines.length * px * IDEA.labelLineHeight) / 2 / IDEA.labelFillHeight);\n return { width, height: ry * 2, rx, ry, lines };\n}\n\nexport function labelGrowth(scale: number): { idea: number; connector: number } {\n const inv = 1 / Math.max(scale, 0.0001);\n return { idea: Math.max(1, Math.min(inv, LABEL_GROWTH.idea)), connector: Math.max(1, Math.min(inv, LABEL_GROWTH.connector)) };\n}\n","// ─── Idea size and label growth ──────────────────────────────────────────────\n//\n// The design system's idea.ts (copied verbatim into generated/geometry): an idea\n// grows to show its whole label, and labels grow a little when zoomed out. The\n// SDK only supplies the text measurement (its font engine) and picks the kind.\n\nimport { IDEA, LABEL_GROWTH, ideaSize, labelGrowth, wrapLines, type IdeaKind } from '../generated/geometry/idea.js';\nimport type { VisualliLayer } from '../types/layer.js';\n\nexport { IDEA, LABEL_GROWTH, ideaSize, labelGrowth, wrapLines };\nexport type { IdeaKind };\n\n/** Width in px of `text` in the idea label's face at `px` (the label's weight; readable type adds its tracking). */\nexport type IdeaMeasure = (text: string, px: number) => number;\n\n/** Measurement without a font engine (server, tests): half an em per character. Renderers pass the real face. */\nexport const approximateMeasure: IdeaMeasure = (text, px) => text.length * px * 0.5;\n\n/** The size kind of the `index`-th idea of a layer: the centre of the root radial layer is the root, the rest ideas. */\nexport const ideaKindOf = (layer: Pick<VisualliLayer, 'level' | 'layout'>, index: number): IdeaKind =>\n layer.level === 0 && (layer.layout ?? 'radial') === 'radial' && index === 0 ? 'root' : 'node';\n"],"mappings":";AAUO,IAAM,eAAe,EAAE,MAAM,KAAK,WAAW,IAAI;AAEjD,IAAM,OAAO;AAAA;AAAA,EACc,WAAW,EAAE,MAAM,KAAK,MAAM,KAAK,MAAM,IAAI;AAAA;AAAA,EACd,UAAU,EAAE,MAAM,KAAK,MAAM,KAAK,MAAM,IAAI;AAAA;AAAA,EACtE,MAAM;AAAA;AAAA,EACwC,QAAQ;AAAA,EAAM,OAAO;AAAA;AAAA,EACnC,YAAY;AAAA;AAAA,EACjB,YAAY;AAAA;AAAA,EACoB,WAAW,EAAE,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG;AAAA,EAC1I,iBAAiB;AAAA;AAAA,EACkD,iBAAiB;AACtF;AAGO,SAAS,UAAU,MAAc,UAAkB,SAA0C;AAClG,QAAM,QAAQ,KAAK,MAAM,KAAK,EAAE,OAAO,OAAO;AAC9C,QAAM,QAAkB,CAAC;AACzB,MAAI,MAAM;AACV,aAAW,KAAK,OAAO;AACrB,UAAM,OAAO,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK;AACnC,QAAI,OAAO,QAAQ,IAAI,IAAI,UAAU;AAAE,YAAM,KAAK,GAAG;AAAG,YAAM;AAAA,IAAG,MAAO,OAAM;AAAA,EAChF;AACA,MAAI,IAAK,OAAM,KAAK,GAAG;AACvB,SAAO,MAAM,SAAS,QAAQ,CAAC,EAAE;AACnC;AAMO,SAAS,SAAS,MAAgB,OAAe,SAA+C,aAAa,GAAG;AACrH,QAAM,KAAK,KAAK,UAAU,IAAI,IAAI,aAAa,aAAa;AAC5D,QAAM,MAAM,CAAC,MAAc,UAAU,OAAO,IAAI,KAAK,YAAY,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC;AACtF,QAAM,OAAe,KAAK,UAAU,IAAI,GAAG,MAAc,KAAK,SAAS,IAAI;AAC3E,MAAI,QAAQ,MAAM,QAAQ,IAAI,KAAK;AACnC,SAAO,MAAM,SAAS,KAAK,cAAc,QAAQ,KAAK;AAAE,YAAQ,KAAK,IAAI,KAAK,QAAQ,KAAK,IAAI;AAAG,YAAQ,IAAI,KAAK;AAAA,EAAG;AACtH,QAAM,KAAK,QAAQ;AACnB,QAAM,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,KAAK,QAAS,MAAM,SAAS,KAAK,KAAK,kBAAmB,IAAI,KAAK,eAAe;AACvH,SAAO,EAAE,OAAO,QAAQ,KAAK,GAAG,IAAI,IAAI,MAAM;AAChD;AAEO,SAAS,YAAY,OAAoD;AAC9E,QAAM,MAAM,IAAI,KAAK,IAAI,OAAO,IAAM;AACtC,SAAO,EAAE,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,aAAa,IAAI,CAAC,GAAG,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,aAAa,SAAS,CAAC,EAAE;AAC9H;;;ACvCO,IAAM,qBAAkC,CAAC,MAAM,OAAO,KAAK,SAAS,KAAK;AAGzE,IAAM,aAAa,CAAC,OAAgD,UACzE,MAAM,UAAU,MAAM,MAAM,UAAU,cAAc,YAAY,UAAU,IAAI,SAAS;","names":[]}
@@ -1,14 +1,24 @@
1
1
  import {
2
- LAYOUT_SPACING,
3
- LEVEL_COLORS,
4
- getColorForLevel
5
- } from "./chunk-G5H3P4YZ.js";
2
+ approximateMeasure,
3
+ ideaKindOf,
4
+ ideaSize
5
+ } from "./chunk-25AJTPMS.js";
6
+ import {
7
+ LAYOUT_SPACING
8
+ } from "./chunk-N7HCYULH.js";
6
9
  import {
7
10
  applyCircularLayout,
8
11
  applyLinearHorizontalLayout,
9
12
  applyLinearVerticalLayout,
10
13
  calculateOptimalRadiusPercentage
11
14
  } from "./chunk-GQMFBYLK.js";
15
+ import {
16
+ ideaColor
17
+ } from "./chunk-KVSM3APM.js";
18
+ import {
19
+ TOKENS,
20
+ TOPICS
21
+ } from "./chunk-LV3EXUQY.js";
12
22
 
13
23
  // src/parser/visualliParser.ts
14
24
  function toVisualliLayer(schemaLayer) {
@@ -75,6 +85,13 @@ function parseVisualliFile(content) {
75
85
  }
76
86
  break;
77
87
  }
88
+ case "extension": {
89
+ const id = obj["id"];
90
+ if (typeof id === "string") {
91
+ (doc.extensions ?? (doc.extensions = {}))[id] = Array.isArray(obj["data"]) ? obj["data"] : [];
92
+ }
93
+ break;
94
+ }
78
95
  default:
79
96
  break;
80
97
  }
@@ -112,6 +129,11 @@ function loadVisualliFileFromFile(file) {
112
129
  reader.readAsText(file);
113
130
  });
114
131
  }
132
+ function getSemanticAnchors(doc) {
133
+ const raw = doc.extensions?.["semantic-anchors"];
134
+ if (!Array.isArray(raw)) return [];
135
+ return raw.filter((a) => !!a && typeof a.word === "string" && typeof a.description === "string");
136
+ }
115
137
  function getChildLayers(doc, nodeId) {
116
138
  const result = [];
117
139
  for (const layer of doc.layers.values()) {
@@ -169,26 +191,10 @@ function resolveNodeOverlaps(nodes, maxIterations = 10) {
169
191
  if (!any) break;
170
192
  }
171
193
  }
172
- var COLOR_PALETTE = [
173
- "#12C7D3",
174
- "#325E8C",
175
- "#8A70A6",
176
- "#F54A57",
177
- "#FF6C4D",
178
- "#F28C16",
179
- "#FFD347",
180
- "#12C7D3",
181
- "#7F7F7F",
182
- "#8D8D8D",
183
- "#12C7D3"
184
- ];
185
- function getRandomColorForNode(nodeId) {
186
- let hash = 0;
187
- for (let i = 0; i < nodeId.length; i++) {
188
- hash = nodeId.charCodeAt(i) + ((hash << 5) - hash);
189
- }
190
- const index = Math.abs(hash) % COLOR_PALETTE.length;
191
- return COLOR_PALETTE[index];
194
+ function hasFilePositions(layer) {
195
+ const ps = layer.nodes.map((n) => n.position);
196
+ if (!ps.length || !ps.every((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y))) return false;
197
+ return ps.length === 1 || ps.some((p) => p.x !== ps[0].x || p.y !== ps[0].y);
192
198
  }
193
199
  function applyFormation(nodes, formation) {
194
200
  if (nodes.length === 0) return;
@@ -252,44 +258,34 @@ function proxyRadial(proxies, radius = 500) {
252
258
  proxies[i].y = radius * Math.sin(angle);
253
259
  }
254
260
  }
255
- function calculateNodeWidth(title) {
256
- return Math.max(200, title.length * 8 + 40);
257
- }
258
- function makeFlatNode(nodeId, label, summary, layer, color, branchCount, x = 0, y = 0) {
261
+ function makeFlatNode(node, layer, doc, siblingIndex, opts) {
262
+ const label = Array.isArray(node.data.label) ? node.data.label.join(" ") : node.data.label || "Untitled";
263
+ const branchCount = countLayersBeneath(doc, node.id);
264
+ const kind = ideaKindOf(layer, siblingIndex);
265
+ const size = ideaSize(kind, label, opts.measure ?? approximateMeasure, opts.labelScale ?? 1);
266
+ const { topic, custom } = ideaColor(node.data.color, siblingIndex);
259
267
  return {
260
- id: nodeId,
268
+ id: node.id,
261
269
  parentId: layer.parentNodeId ?? null,
262
- x,
263
- y,
270
+ x: node.position?.x ?? 0,
271
+ y: node.position?.y ?? 0,
264
272
  level: layer.level,
265
273
  title: label,
266
- description: summary,
267
- color,
268
- width: calculateNodeWidth(label),
269
- height: 80,
274
+ description: node.data.summary || "",
275
+ color: node.data.color ?? "",
276
+ ...topic ? { topic } : {},
277
+ ...custom ? { custom } : {},
278
+ kind,
279
+ width: size.width,
280
+ height: size.height,
270
281
  isExpanded: branchCount > 0,
271
282
  branchCount,
272
283
  createdAt: /* @__PURE__ */ new Date(),
273
284
  updatedAt: /* @__PURE__ */ new Date()
274
285
  };
275
286
  }
276
- function convertLayerWithContainers(layer, doc) {
287
+ function layoutLayerWithContainers(layer, flatNodes) {
277
288
  const PADDING = 80;
278
- const flatNodes = [];
279
- for (const node of layer.nodes) {
280
- const label = Array.isArray(node.data.label) ? node.data.label.join(" ") : node.data.label || "Untitled";
281
- const branchCount = countLayersBeneath(doc, node.id);
282
- flatNodes.push(
283
- makeFlatNode(
284
- node.id,
285
- label,
286
- node.data.summary || "",
287
- layer,
288
- node.data.color || getRandomColorForNode(node.id),
289
- branchCount
290
- )
291
- );
292
- }
293
289
  const byId = new Map(flatNodes.map((n) => [n.id, n]));
294
290
  const containerNodeIds = /* @__PURE__ */ new Set();
295
291
  const containerGroups = /* @__PURE__ */ new Map();
@@ -302,11 +298,8 @@ function convertLayerWithContainers(layer, doc) {
302
298
  containerGroups.set(container.id, group);
303
299
  }
304
300
  const proxies = [];
305
- for (const node of layer.nodes) {
306
- if (!containerNodeIds.has(node.id)) {
307
- const nodeWidth = calculateNodeWidth(node.data.label);
308
- proxies.push({ id: node.id, x: 0, y: 0, width: nodeWidth, height: 80 });
309
- }
301
+ for (const n of flatNodes) {
302
+ if (!containerNodeIds.has(n.id)) proxies.push({ id: n.id, x: 0, y: 0, width: n.width, height: n.height });
310
303
  }
311
304
  for (const container of layer.containers) {
312
305
  const group = containerGroups.get(container.id);
@@ -333,54 +326,40 @@ function convertLayerWithContainers(layer, doc) {
333
326
  }
334
327
  }
335
328
  }
336
- resolveNodeOverlaps(flatNodes);
337
- return flatNodes;
338
329
  }
339
- function convertLayerToFlatNodes(layer, doc) {
340
- if ((layer.containers ?? []).length > 0) {
341
- return convertLayerWithContainers(layer, doc);
342
- }
343
- const flatNodes = [];
344
- for (const node of layer.nodes) {
345
- const label = Array.isArray(node.data.label) ? node.data.label.join(" ") : node.data.label || "Untitled";
346
- const branchCount = countLayersBeneath(doc, node.id);
347
- flatNodes.push(
348
- makeFlatNode(
349
- node.id,
350
- label,
351
- node.data.summary || "",
352
- layer,
353
- node.data.color || getRandomColorForNode(node.id),
354
- branchCount,
355
- 0,
356
- 0
357
- )
358
- );
330
+ function layoutLayer(layer, flatNodes) {
331
+ const layout = layer.layout || "radial";
332
+ if (layout === "linear-horizontal") {
333
+ applyLinearHorizontalLayout(flatNodes, { centerX: 0, centerY: 0 });
334
+ } else if (layout === "linear-vertical") {
335
+ applyLinearVerticalLayout(flatNodes, { centerX: 0, centerY: 0 });
336
+ } else {
337
+ const pct = calculateOptimalRadiusPercentage(flatNodes.length, 2e3, 2e3, 200, 30);
338
+ applyCircularLayout(flatNodes, {
339
+ radiusPercentage: pct,
340
+ containerWidth: 2e3,
341
+ containerHeight: 2e3,
342
+ centerX: 0,
343
+ centerY: 0
344
+ });
359
345
  }
360
- if (flatNodes.length > 0) {
361
- const layout = layer.layout || "radial";
362
- if (layout === "linear-horizontal") {
363
- applyLinearHorizontalLayout(flatNodes, { centerX: 0, centerY: 0 });
364
- } else if (layout === "linear-vertical") {
365
- applyLinearVerticalLayout(flatNodes, { centerX: 0, centerY: 0 });
366
- } else {
367
- const pct = calculateOptimalRadiusPercentage(flatNodes.length, 2e3, 2e3, 200, 30);
368
- applyCircularLayout(flatNodes, {
369
- radiusPercentage: pct,
370
- containerWidth: 2e3,
371
- containerHeight: 2e3,
372
- centerX: 0,
373
- centerY: 0
374
- });
375
- }
346
+ }
347
+ function convertLayerToFlatNodes(layer, doc, opts = {}) {
348
+ const flatNodes = layer.nodes.map((node, i) => makeFlatNode(node, layer, doc, i, opts));
349
+ if (flatNodes.length === 0 || hasFilePositions(layer)) return flatNodes;
350
+ for (const n of flatNodes) {
351
+ n.x = 0;
352
+ n.y = 0;
376
353
  }
354
+ if ((layer.containers ?? []).length > 0) layoutLayerWithContainers(layer, flatNodes);
355
+ else layoutLayer(layer, flatNodes);
377
356
  resolveNodeOverlaps(flatNodes);
378
357
  return flatNodes;
379
358
  }
380
- function convertVisualliToFlatNodes(doc) {
359
+ function convertVisualliToFlatNodes(doc, opts = {}) {
381
360
  const map = /* @__PURE__ */ new Map();
382
361
  for (const layer of doc.layers.values()) {
383
- for (const node of convertLayerToFlatNodes(layer, doc)) {
362
+ for (const node of convertLayerToFlatNodes(layer, doc, opts)) {
384
363
  map.set(node.id, node);
385
364
  }
386
365
  }
@@ -392,9 +371,9 @@ function convertVisualliToFlatNodes(doc) {
392
371
  }
393
372
  return map;
394
373
  }
395
- function getNodesForLayer(doc, layerId) {
374
+ function getNodesForLayer(doc, layerId, opts = {}) {
396
375
  const layer = doc.layers.get(layerId);
397
- return layer ? convertLayerToFlatNodes(layer, doc) : [];
376
+ return layer ? convertLayerToFlatNodes(layer, doc, opts) : [];
398
377
  }
399
378
 
400
379
  // src/parser/mindmapUtils.ts
@@ -446,6 +425,8 @@ function calculateDistance(x1, y1, x2, y2) {
446
425
  }
447
426
 
448
427
  // src/parser/configUtils.ts
428
+ var levelColor = (level) => TOKENS.light[`topic-${TOPICS[level % TOPICS.length]}`];
429
+ var levelColors = (count) => Object.fromEntries(Array.from({ length: count }, (_, i) => [`level${i}`, levelColor(i)]));
449
430
  function createMindMapConfig(nodes, topLevelConnections, title = "Untitled Mindmap") {
450
431
  const now = (/* @__PURE__ */ new Date()).toISOString();
451
432
  return {
@@ -459,12 +440,7 @@ function createMindMapConfig(nodes, topLevelConnections, title = "Untitled Mindm
459
440
  nodes,
460
441
  topLevelConnections,
461
442
  settings: {
462
- defaultColors: {
463
- level0: LEVEL_COLORS.level0,
464
- level1: LEVEL_COLORS.level1,
465
- level2: LEVEL_COLORS.level2,
466
- level3: LEVEL_COLORS.level3
467
- },
443
+ defaultColors: levelColors(4),
468
444
  layout: {
469
445
  nodeSpacing: LAYOUT_SPACING.nodeSpacing,
470
446
  levelSpacing: LAYOUT_SPACING.levelSpacing
@@ -491,7 +467,7 @@ function generateLevelNodes(parentId, level, maxLevels, xOffset = 0, yOffset = 0
491
467
  level,
492
468
  x,
493
469
  y,
494
- color: getColorForLevel(level),
470
+ color: levelColor(level),
495
471
  relationshipLabel: level > 0 ? "child" : void 0,
496
472
  parent: parentId ?? void 0
497
473
  };
@@ -520,17 +496,7 @@ function generateSampleConfig(maxLevels = 9) {
520
496
  nodes,
521
497
  topLevelConnections,
522
498
  settings: {
523
- defaultColors: {
524
- level0: LEVEL_COLORS.level0,
525
- level1: LEVEL_COLORS.level1,
526
- level2: LEVEL_COLORS.level2,
527
- level3: LEVEL_COLORS.level3,
528
- level4: LEVEL_COLORS.level4,
529
- level5: LEVEL_COLORS.level5,
530
- level6: LEVEL_COLORS.level6,
531
- level7: LEVEL_COLORS.level7,
532
- level8: LEVEL_COLORS.level8
533
- },
499
+ defaultColors: levelColors(9),
534
500
  layout: {
535
501
  nodeSpacing: LAYOUT_SPACING.nodeSpacing,
536
502
  levelSpacing: LAYOUT_SPACING.levelSpacing
@@ -545,6 +511,7 @@ export {
545
511
  fetchAndParseVisualli,
546
512
  parseVisualliFromMongoDB,
547
513
  loadVisualliFileFromFile,
514
+ getSemanticAnchors,
548
515
  getChildLayers,
549
516
  countLayersBeneath,
550
517
  resolveNodeOverlaps,
@@ -559,4 +526,4 @@ export {
559
526
  createMindMapConfig,
560
527
  generateSampleConfig
561
528
  };
562
- //# sourceMappingURL=chunk-6NXADJK2.js.map
529
+ //# sourceMappingURL=chunk-7NJWM7ZE.js.map