@typstream/mindmap 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +45 -2
- package/THIRD-PARTY-NOTICES.txt +31 -0
- package/dist/flow-D3CKRG3R.js +461 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +124 -0
- package/dist/react.d.ts +61 -0
- package/dist/react.js +12 -0
- package/package.json +73 -3
- package/typst/island.typ +27 -0
- package/typst/lib.typ +7 -0
- package/typst/mindmap.typ +406 -0
- package/typst/typst.toml +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ikram Hasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @typstream/mindmap
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Mind maps for typstream: written as a list, drawn in plain Typst for print, PDF and every renderer,
|
|
4
|
+
and interactive in `<Typstream>` (pan, zoom, open and close branches). The first render island.
|
|
5
|
+
|
|
6
|
+
```typst
|
|
7
|
+
#mindmap[
|
|
8
|
+
Photosynthesis
|
|
9
|
+
- Inputs
|
|
10
|
+
- Sunlight
|
|
11
|
+
- Water ($H_2 O$)
|
|
12
|
+
- #topic(color: "#16a34a", collapsed: true)[Outputs]
|
|
13
|
+
- Glucose
|
|
14
|
+
- Oxygen
|
|
15
|
+
]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { mindmap } from "@typstream/mindmap";
|
|
20
|
+
import { mindmapIslands } from "@typstream/mindmap/react";
|
|
21
|
+
|
|
22
|
+
<Typstream source={text} plugins={[mindmap]} islands={mindmapIslands} />;
|
|
23
|
+
buildSystemPrompt([edu, mindmap]); // the model learns `#mindmap`
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Typst** (`typst/`): `mindmap(style:, layout:, text-size:)[centre, then a list]` and `topic(color:, size:, weight:,
|
|
27
|
+
image:, collapsed:)[label]`, which cover the fields of Monsha's mind map format. Drawn with `measure`, `place` and
|
|
28
|
+
`curve`, not CeTZ: 9–11 ms per compile while streaming in engine-wasm. Every parameter takes what a stream cut leaves.
|
|
29
|
+
The drawing sits in an island marker (`island.typ`) carrying the resolved tree as data.
|
|
30
|
+
- **Plugin** (`.`): the files as a local package `@typstream/mindmap:0.1.0`, the preamble, the prompt kit (catalogue
|
|
31
|
+
from the `///` doc comments, rules, `examples/`), and the `mindmap` island with `parseMindMap`, which checks the
|
|
32
|
+
marker's untrusted data.
|
|
33
|
+
- **Interactive map** (`./react`): React Flow, loaded with the first map (about 65 KB gzipped with its CSS), laid out like
|
|
34
|
+
the drawing (`src/react/layout.ts`) on labels measured in the page's font, read-only. CSS is adopted as constructable
|
|
35
|
+
stylesheets (strict CSP). On pages, the map is mounted over the page's canvas,
|
|
36
|
+
which keeps the drawing for print and exports.
|
|
37
|
+
- `pnpm embed` after editing `typst/` or `examples/` (a test checks `src/assets.generated.ts` and React Flow's CSS).
|
|
38
|
+
`pnpm test` compiles through engine-wasm and native `typst` (every healed stream prefix of the examples);
|
|
39
|
+
`pnpm test:browser` mounts the island on the real engine worker in Chromium, Firefox and WebKit; `pnpm size`.
|
|
40
|
+
|
|
41
|
+
Docs: [reference](https://typstream.vercel.app/docs/reference/mindmap),
|
|
42
|
+
[Writing an island](https://typstream.vercel.app/docs/guides/islands).
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT. The React Flow stylesheet the interactive map embeds is MIT too; its license is in `THIRD-PARTY-NOTICES.txt`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Third-party notices for @typstream/mindmap
|
|
2
|
+
|
|
3
|
+
The chunk the interactive map (./react) loads, dist/flow-*.js, embeds the stylesheet (dist/style.css) of
|
|
4
|
+
@xyflow/react 12.12.0 (React Flow, https://github.com/xyflow/xyflow). React Flow's code is a dependency, not bundled.
|
|
5
|
+
Its license follows.
|
|
6
|
+
|
|
7
|
+
================================================================================
|
|
8
|
+
@xyflow/react 12.12.0
|
|
9
|
+
================================================================================
|
|
10
|
+
|
|
11
|
+
MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2019-2025 webkid GmbH
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/react/flow.tsx
|
|
4
|
+
import {
|
|
5
|
+
BaseEdge,
|
|
6
|
+
Controls,
|
|
7
|
+
Handle,
|
|
8
|
+
Position,
|
|
9
|
+
ReactFlow,
|
|
10
|
+
ReactFlowProvider,
|
|
11
|
+
useReactFlow,
|
|
12
|
+
useStore
|
|
13
|
+
} from "@xyflow/react";
|
|
14
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
|
|
16
|
+
// src/react/css.ts
|
|
17
|
+
var adopted = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
var adoptCss = (node, css) => {
|
|
19
|
+
const root = node.getRootNode();
|
|
20
|
+
const done = adopted.get(root) ?? /* @__PURE__ */ new Set();
|
|
21
|
+
adopted.set(root, done);
|
|
22
|
+
if (done.has(css)) return;
|
|
23
|
+
done.add(css);
|
|
24
|
+
const Sheet = node.ownerDocument?.defaultView?.CSSStyleSheet ?? CSSStyleSheet;
|
|
25
|
+
const sheet = new Sheet();
|
|
26
|
+
sheet.replaceSync(css);
|
|
27
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/react/layout.ts
|
|
31
|
+
var PX = 4 / 3;
|
|
32
|
+
var GAP_X = 22 * PX;
|
|
33
|
+
var GAP_Y = 6 * PX;
|
|
34
|
+
var GAP_BRANCH = 12 * PX;
|
|
35
|
+
var descendants = (n) => (n.children ?? []).reduce((sum, c) => sum + 1 + descendants(c), 0);
|
|
36
|
+
var layoutMindMap = (data, closed, size) => {
|
|
37
|
+
const open = (id, n) => n.children?.length && !closed(id, n) ? n.children.map((c, i) => [`${id}.${i}`, c]) : [];
|
|
38
|
+
const box = (id, n, depth) => {
|
|
39
|
+
const hidden = n.children?.length && closed(id, n) ? descendants(n) : 0;
|
|
40
|
+
return { hidden, ...size(n, depth, hidden) };
|
|
41
|
+
};
|
|
42
|
+
const place = (id, n, depth, side) => {
|
|
43
|
+
const { w, h, hidden } = box(id, n, depth);
|
|
44
|
+
const me = { id, node: n, depth, side, hidden, x: 0, y: 0, w, h };
|
|
45
|
+
const kids = open(id, n);
|
|
46
|
+
if (kids.length === 0) return { nodes: [me], edges: [], height: h, cy: h / 2 };
|
|
47
|
+
const nodes2 = [];
|
|
48
|
+
const edges2 = [];
|
|
49
|
+
const centres = [];
|
|
50
|
+
let y = 0;
|
|
51
|
+
for (const [cid, c] of kids) {
|
|
52
|
+
const sub = place(cid, c, depth + 1, side);
|
|
53
|
+
for (const m of sub.nodes) nodes2.push({ ...m, x: m.x + w + GAP_X, y: m.y + y });
|
|
54
|
+
edges2.push(...sub.edges, {
|
|
55
|
+
id: `${id}-${cid}`,
|
|
56
|
+
source: id,
|
|
57
|
+
target: cid,
|
|
58
|
+
side,
|
|
59
|
+
color: n.color,
|
|
60
|
+
points: [0, 0, 0, 0]
|
|
61
|
+
});
|
|
62
|
+
centres.push(y + sub.cy);
|
|
63
|
+
y += sub.height + GAP_Y;
|
|
64
|
+
}
|
|
65
|
+
const height2 = y - GAP_Y;
|
|
66
|
+
let cy = (centres[0] + centres[centres.length - 1]) / 2;
|
|
67
|
+
const shift = Math.max(0, h / 2 - cy);
|
|
68
|
+
for (const m of nodes2) m.y += shift;
|
|
69
|
+
cy += shift;
|
|
70
|
+
nodes2.push({ ...me, y: cy - h / 2 });
|
|
71
|
+
return { nodes: nodes2, edges: edges2, height: Math.max(height2 + shift, cy + h / 2), cy };
|
|
72
|
+
};
|
|
73
|
+
const stack = (branches2, side) => {
|
|
74
|
+
const nodes2 = [];
|
|
75
|
+
const edges2 = [];
|
|
76
|
+
let y = 0;
|
|
77
|
+
for (const [id, b] of branches2) {
|
|
78
|
+
const sub = place(id, b, 1, side);
|
|
79
|
+
for (const m of sub.nodes) nodes2.push({ ...m, y: m.y + y });
|
|
80
|
+
edges2.push(...sub.edges, { id: `0-${id}`, source: "0", target: id, side, color: b.color, points: [0, 0, 0, 0] });
|
|
81
|
+
y += sub.height + GAP_BRANCH;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
nodes: nodes2,
|
|
85
|
+
edges: edges2,
|
|
86
|
+
width: Math.max(0, ...nodes2.map((m) => m.x + m.w)),
|
|
87
|
+
height: Math.max(0, y - GAP_BRANCH)
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const root = data.root;
|
|
91
|
+
const centre = box("0", root, 0);
|
|
92
|
+
const branches = open("0", root);
|
|
93
|
+
const right = stack(
|
|
94
|
+
branches.filter(([, b]) => b.side !== "left"),
|
|
95
|
+
"right"
|
|
96
|
+
);
|
|
97
|
+
const left = stack(
|
|
98
|
+
branches.filter(([, b]) => b.side === "left"),
|
|
99
|
+
"left"
|
|
100
|
+
);
|
|
101
|
+
const gap = 2 * GAP_X;
|
|
102
|
+
const height = Math.max(right.height, left.height, centre.h);
|
|
103
|
+
const cx = left.nodes.length ? left.width + gap : 0;
|
|
104
|
+
const ry = (height - right.height) / 2;
|
|
105
|
+
const ly = (height - left.height) / 2;
|
|
106
|
+
const nodes = [
|
|
107
|
+
{ id: "0", node: root, depth: 0, side: null, x: cx, y: (height - centre.h) / 2, ...centre }
|
|
108
|
+
];
|
|
109
|
+
const ox = cx + centre.w + gap;
|
|
110
|
+
for (const m of right.nodes) nodes.push({ ...m, x: ox + m.x, y: ry + m.y });
|
|
111
|
+
for (const m of left.nodes) nodes.push({ ...m, x: cx - gap - m.x - m.w, y: ly + m.y });
|
|
112
|
+
const byId = new Map(nodes.map((m) => [m.id, m]));
|
|
113
|
+
const edges = [...right.edges, ...left.edges];
|
|
114
|
+
for (const e of edges) {
|
|
115
|
+
const [from, to] = [byId.get(e.source), byId.get(e.target)];
|
|
116
|
+
const right2 = e.side === "right";
|
|
117
|
+
e.points = [right2 ? from.x + from.w : from.x, from.y + from.h / 2, right2 ? to.x : to.x + to.w, to.y + to.h / 2];
|
|
118
|
+
}
|
|
119
|
+
return { nodes, edges };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/react/measure.ts
|
|
123
|
+
var context = null;
|
|
124
|
+
var canvas = () => {
|
|
125
|
+
context ??= typeof OffscreenCanvas === "function" ? new OffscreenCanvas(1, 1).getContext("2d") : document.createElement("canvas").getContext("2d");
|
|
126
|
+
return context;
|
|
127
|
+
};
|
|
128
|
+
var weightOf = (n) => n.weight === "bold" ? 700 : n.weight === "regular" ? 400 : n.weight;
|
|
129
|
+
var wrap = (label, max, width) => {
|
|
130
|
+
const lines = [];
|
|
131
|
+
let line = "";
|
|
132
|
+
for (const word of label.split(/\s+/).filter(Boolean)) {
|
|
133
|
+
const next = line ? `${line} ${word}` : word;
|
|
134
|
+
if (line && width(next) > max) {
|
|
135
|
+
lines.push(line);
|
|
136
|
+
line = word;
|
|
137
|
+
} else line = next;
|
|
138
|
+
}
|
|
139
|
+
lines.push(line);
|
|
140
|
+
return lines;
|
|
141
|
+
};
|
|
142
|
+
var measureNode = (n, depth, hidden, data, family) => {
|
|
143
|
+
const ctx = canvas();
|
|
144
|
+
const size = n.size * PX;
|
|
145
|
+
const base = data.size * PX;
|
|
146
|
+
const width = (s) => ctx ? ctx.measureText(s).width : s.length * size * 0.55;
|
|
147
|
+
if (ctx) ctx.font = `${weightOf(n)} ${size}px ${family}`;
|
|
148
|
+
const lines = wrap(n.label, base * (data.style === "text-focused" ? 14 : 10), width);
|
|
149
|
+
let w = Math.max(...lines.map(width));
|
|
150
|
+
if (hidden > 0) {
|
|
151
|
+
if (ctx) ctx.font = `700 ${0.7 * size}px ${family}`;
|
|
152
|
+
w += 4 * PX + width(`+${hidden}`) + 8;
|
|
153
|
+
}
|
|
154
|
+
let h = lines.length * size * 1.25;
|
|
155
|
+
let image = null;
|
|
156
|
+
if (n.image && data.style === "visual") {
|
|
157
|
+
image = { h: base * 3.2 };
|
|
158
|
+
w = Math.max(w, base * 4.5);
|
|
159
|
+
h += image.h + 4;
|
|
160
|
+
}
|
|
161
|
+
const border = depth === 0 ? 0 : depth === 1 ? 1.6 : 1.0667;
|
|
162
|
+
return { w: Math.ceil(w + size + 2 * border) + 1, h: Math.ceil(h + 0.6 * size + 2 * border), lines, image };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/react/styles.ts
|
|
166
|
+
var MINDMAP_CSS = `.ts-mindmap {
|
|
167
|
+
margin: 0;
|
|
168
|
+
width: 100%;
|
|
169
|
+
height: 100%;
|
|
170
|
+
background: var(--typstream-island-background, Canvas);
|
|
171
|
+
}
|
|
172
|
+
.ts-mindmap .react-flow {
|
|
173
|
+
--xy-background-color: transparent;
|
|
174
|
+
font: inherit;
|
|
175
|
+
}
|
|
176
|
+
.ts-mindmap :is(svg, img) {
|
|
177
|
+
max-width: none;
|
|
178
|
+
}
|
|
179
|
+
.ts-mindmap-node {
|
|
180
|
+
box-sizing: border-box;
|
|
181
|
+
border: 0 solid;
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
line-height: 1.25;
|
|
187
|
+
text-align: center;
|
|
188
|
+
white-space: nowrap;
|
|
189
|
+
}
|
|
190
|
+
.ts-mindmap-node[data-depth="1"] {
|
|
191
|
+
border-width: 1.6px;
|
|
192
|
+
}
|
|
193
|
+
.ts-mindmap-node[data-depth="2"] {
|
|
194
|
+
border-width: 1.0667px;
|
|
195
|
+
}
|
|
196
|
+
.ts-mindmap-label {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 5.333px;
|
|
200
|
+
}
|
|
201
|
+
.ts-mindmap-label > span[dir] {
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
}
|
|
205
|
+
.ts-mindmap-badge {
|
|
206
|
+
border-radius: 1em;
|
|
207
|
+
padding: 2px 4px;
|
|
208
|
+
font-weight: 700;
|
|
209
|
+
line-height: 1.2;
|
|
210
|
+
background: var(--ts-mindmap-color);
|
|
211
|
+
color: #fff;
|
|
212
|
+
}
|
|
213
|
+
.ts-mindmap-node[data-depth="0"] .ts-mindmap-badge {
|
|
214
|
+
background: #fff;
|
|
215
|
+
color: var(--ts-mindmap-color);
|
|
216
|
+
}
|
|
217
|
+
.ts-mindmap-image {
|
|
218
|
+
align-self: stretch;
|
|
219
|
+
margin-bottom: 4px;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
border-radius: 2.667px;
|
|
222
|
+
background: #e5e7eb;
|
|
223
|
+
color: #4b5563;
|
|
224
|
+
font-size: 0.7em;
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
}
|
|
229
|
+
.ts-mindmap-image img {
|
|
230
|
+
width: 100%;
|
|
231
|
+
height: 100%;
|
|
232
|
+
object-fit: cover;
|
|
233
|
+
}
|
|
234
|
+
.ts-mindmap .react-flow__handle {
|
|
235
|
+
opacity: 0;
|
|
236
|
+
pointer-events: none;
|
|
237
|
+
width: 1px;
|
|
238
|
+
height: 1px;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
min-height: 0;
|
|
241
|
+
border: 0;
|
|
242
|
+
}
|
|
243
|
+
.ts-mindmap .react-flow__node[aria-label*="("] {
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
}
|
|
246
|
+
.ts-mindmap .react-flow__node:focus-visible {
|
|
247
|
+
outline: 2px solid #2563eb;
|
|
248
|
+
outline-offset: 2px;
|
|
249
|
+
border-radius: 4px;
|
|
250
|
+
}`;
|
|
251
|
+
|
|
252
|
+
// src/react/xyflow-css.generated.ts
|
|
253
|
+
var XYFLOW_CSS = "/* this gets exported as style.css and can be used for the default theming */\n/* these are the necessary styles for React/Svelte Flow, they get used by base.css and style.css */\n.react-flow {\n direction: ltr;\n\n --xy-edge-stroke-default: #b1b1b7;\n --xy-edge-stroke-width-default: 1;\n --xy-edge-stroke-selected-default: #555;\n\n --xy-connectionline-stroke-default: #b1b1b7;\n --xy-connectionline-stroke-width-default: 1;\n\n --xy-attribution-background-color-default: rgba(255, 255, 255, 0.5);\n\n --xy-minimap-background-color-default: #fff;\n --xy-minimap-mask-background-color-default: rgba(240, 240, 240, 0.6);\n --xy-minimap-mask-stroke-color-default: transparent;\n --xy-minimap-mask-stroke-width-default: 1;\n --xy-minimap-node-background-color-default: #e2e2e2;\n --xy-minimap-node-stroke-color-default: transparent;\n --xy-minimap-node-stroke-width-default: 2;\n\n --xy-background-color-default: transparent;\n --xy-background-pattern-dots-color-default: #91919a;\n --xy-background-pattern-lines-color-default: #eee;\n --xy-background-pattern-cross-color-default: #e2e2e2;\n background-color: var(--xy-background-color, var(--xy-background-color-default));\n --xy-node-color-default: inherit;\n --xy-node-border-default: 1px solid #1a192b;\n --xy-node-background-color-default: #fff;\n --xy-node-group-background-color-default: rgba(240, 240, 240, 0.25);\n --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08);\n --xy-node-boxshadow-selected-default: 0 0 0 0.5px #1a192b;\n --xy-node-border-radius-default: 3px;\n\n --xy-handle-background-color-default: #1a192b;\n --xy-handle-border-color-default: #fff;\n\n --xy-selection-background-color-default: rgba(0, 89, 220, 0.08);\n --xy-selection-border-default: 1px dotted rgba(0, 89, 220, 0.8);\n\n --xy-controls-button-background-color-default: #fefefe;\n --xy-controls-button-background-color-hover-default: #f4f4f4;\n --xy-controls-button-color-default: inherit;\n --xy-controls-button-color-hover-default: inherit;\n --xy-controls-button-border-color-default: #eee;\n --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);\n\n --xy-edge-label-background-color-default: #ffffff;\n --xy-edge-label-color-default: inherit;\n --xy-resize-background-color-default: #3367d9;\n}\n.react-flow.dark {\n --xy-edge-stroke-default: #3e3e3e;\n --xy-edge-stroke-width-default: 1;\n --xy-edge-stroke-selected-default: #727272;\n\n --xy-connectionline-stroke-default: #b1b1b7;\n --xy-connectionline-stroke-width-default: 1;\n\n --xy-attribution-background-color-default: rgba(150, 150, 150, 0.25);\n\n --xy-minimap-background-color-default: #141414;\n --xy-minimap-mask-background-color-default: rgba(60, 60, 60, 0.6);\n --xy-minimap-mask-stroke-color-default: transparent;\n --xy-minimap-mask-stroke-width-default: 1;\n --xy-minimap-node-background-color-default: #2b2b2b;\n --xy-minimap-node-stroke-color-default: transparent;\n --xy-minimap-node-stroke-width-default: 2;\n\n --xy-background-color-default: #141414;\n --xy-background-pattern-dots-color-default: #555;\n --xy-background-pattern-lines-color-default: #333;\n --xy-background-pattern-cross-color-default: #333;\n --xy-node-color-default: #f8f8f8;\n --xy-node-border-default: 1px solid #3c3c3c;\n --xy-node-background-color-default: #1e1e1e;\n --xy-node-group-background-color-default: rgba(240, 240, 240, 0.25);\n --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, 0.08);\n --xy-node-boxshadow-selected-default: 0 0 0 0.5px #999;\n\n --xy-handle-background-color-default: #bebebe;\n --xy-handle-border-color-default: #1e1e1e;\n\n --xy-selection-background-color-default: rgba(200, 200, 220, 0.08);\n --xy-selection-border-default: 1px dotted rgba(200, 200, 220, 0.8);\n\n --xy-controls-button-background-color-default: #2b2b2b;\n --xy-controls-button-background-color-hover-default: #3e3e3e;\n --xy-controls-button-color-default: #f8f8f8;\n --xy-controls-button-color-hover-default: #fff;\n --xy-controls-button-border-color-default: #5b5b5b;\n --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);\n\n --xy-edge-label-background-color-default: #141414;\n --xy-edge-label-color-default: #f8f8f8;\n}\n.react-flow__background {\n background-color: var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));\n pointer-events: none;\n z-index: -1;\n}\n.react-flow__container {\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n}\n.react-flow__pane {\n z-index: 1;\n touch-action: none;\n}\n.react-flow__pane.draggable {\n cursor: grab;\n }\n.react-flow__pane.dragging {\n cursor: grabbing;\n }\n.react-flow__pane.selection {\n cursor: pointer;\n }\n.react-flow__viewport {\n transform-origin: 0 0;\n z-index: 2;\n pointer-events: none;\n}\n.react-flow__renderer {\n z-index: 4;\n}\n.react-flow__selection {\n z-index: 6;\n}\n.react-flow__nodesselection-rect:focus,\n.react-flow__nodesselection-rect:focus-visible {\n outline: none;\n}\n.react-flow__edge-path {\n stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));\n stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));\n fill: none;\n}\n.react-flow__connection-path {\n stroke: var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));\n stroke-width: var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));\n fill: none;\n}\n.react-flow .react-flow__edges {\n position: absolute;\n}\n.react-flow .react-flow__edges svg {\n overflow: visible;\n position: absolute;\n pointer-events: none;\n }\n.react-flow__edge {\n pointer-events: visibleStroke;\n}\n.react-flow__edge.selectable {\n cursor: pointer;\n }\n.react-flow__edge.animated path {\n stroke-dasharray: 5;\n animation: dashdraw 0.5s linear infinite;\n }\n.react-flow__edge.animated path.react-flow__edge-interaction {\n stroke-dasharray: none;\n animation: none;\n }\n.react-flow__edge.inactive {\n pointer-events: none;\n }\n.react-flow__edge.selected,\n .react-flow__edge:focus,\n .react-flow__edge:focus-visible {\n outline: none;\n }\n.react-flow__edge.selected .react-flow__edge-path,\n .react-flow__edge.selectable:focus .react-flow__edge-path,\n .react-flow__edge.selectable:focus-visible .react-flow__edge-path {\n stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default));\n }\n.react-flow__edge-textwrapper {\n pointer-events: all;\n }\n.react-flow__edge .react-flow__edge-text {\n pointer-events: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n }\n/* Arrowhead marker styles - use CSS custom properties as default */\n.react-flow__arrowhead polyline {\n stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));\n}\n.react-flow__arrowhead polyline.arrowclosed {\n fill: var(--xy-edge-stroke, var(--xy-edge-stroke-default));\n}\n.react-flow__connection {\n pointer-events: none;\n}\n.react-flow__connection .animated {\n stroke-dasharray: 5;\n animation: dashdraw 0.5s linear infinite;\n }\nsvg.react-flow__connectionline {\n z-index: 1001;\n overflow: visible;\n position: absolute;\n}\n.react-flow__nodes {\n pointer-events: none;\n transform-origin: 0 0;\n}\n.react-flow__node {\n position: absolute;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n pointer-events: all;\n transform-origin: 0 0;\n box-sizing: border-box;\n cursor: default;\n}\n.react-flow__node.selectable {\n cursor: pointer;\n }\n.react-flow__node.draggable {\n cursor: grab;\n pointer-events: all;\n }\n.react-flow__node.draggable.dragging {\n cursor: grabbing;\n }\n.react-flow__nodesselection {\n z-index: 3;\n transform-origin: left top;\n pointer-events: none;\n}\n.react-flow__nodesselection-rect {\n position: absolute;\n pointer-events: all;\n cursor: grab;\n }\n.react-flow__handle {\n position: absolute;\n pointer-events: none;\n min-width: 5px;\n min-height: 5px;\n width: 6px;\n height: 6px;\n background-color: var(--xy-handle-background-color, var(--xy-handle-background-color-default));\n border: 1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));\n border-radius: 100%;\n}\n.react-flow__handle.connectingfrom {\n pointer-events: all;\n }\n.react-flow__handle.connectionindicator {\n pointer-events: all;\n cursor: crosshair;\n }\n.react-flow__handle-bottom {\n top: auto;\n left: 50%;\n bottom: 0;\n transform: translate(-50%, 50%);\n }\n.react-flow__handle-top {\n top: 0;\n left: 50%;\n transform: translate(-50%, -50%);\n }\n.react-flow__handle-left {\n top: 50%;\n left: 0;\n transform: translate(-50%, -50%);\n }\n.react-flow__handle-right {\n top: 50%;\n right: 0;\n transform: translate(50%, -50%);\n }\n.react-flow__edgeupdater {\n cursor: move;\n pointer-events: all;\n}\n.react-flow__pane.selection .react-flow__panel {\n pointer-events: none;\n}\n.react-flow__panel {\n position: absolute;\n z-index: 5;\n margin: 15px;\n}\n.react-flow__panel.top {\n top: 0;\n }\n.react-flow__panel.bottom {\n bottom: 0;\n }\n.react-flow__panel.top.center, .react-flow__panel.bottom.center {\n left: 50%;\n transform: translateX(-15px) translateX(-50%);\n }\n.react-flow__panel.left {\n left: 0;\n }\n.react-flow__panel.right {\n right: 0;\n }\n.react-flow__panel.left.center, .react-flow__panel.right.center {\n top: 50%;\n transform: translateY(-15px) translateY(-50%);\n }\n.react-flow__attribution {\n font-size: 10px;\n background: var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));\n padding: 2px 3px;\n margin: 0;\n}\n.react-flow__attribution a {\n text-decoration: none;\n color: #999;\n }\n@keyframes dashdraw {\n from {\n stroke-dashoffset: 10;\n }\n}\n.react-flow__edgelabel-renderer {\n position: absolute;\n width: 100%;\n height: 100%;\n pointer-events: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n left: 0;\n top: 0;\n}\n.react-flow__viewport-portal {\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n top: 0;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n}\n.react-flow__minimap {\n background: var(\n --xy-minimap-background-color-props,\n var(--xy-minimap-background-color, var(--xy-minimap-background-color-default))\n );\n}\n.react-flow__minimap-svg {\n display: block;\n }\n.react-flow__minimap-mask {\n fill: var(\n --xy-minimap-mask-background-color-props,\n var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default))\n );\n stroke: var(\n --xy-minimap-mask-stroke-color-props,\n var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default))\n );\n stroke-width: var(\n --xy-minimap-mask-stroke-width-props,\n var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default))\n );\n }\n.react-flow__minimap-node {\n fill: var(\n --xy-minimap-node-background-color-props,\n var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default))\n );\n stroke: var(\n --xy-minimap-node-stroke-color-props,\n var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default))\n );\n stroke-width: var(\n --xy-minimap-node-stroke-width-props,\n var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default))\n );\n }\n.react-flow__background-pattern.dots {\n fill: var(\n --xy-background-pattern-color-props,\n var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default))\n );\n }\n.react-flow__background-pattern.lines {\n stroke: var(\n --xy-background-pattern-color-props,\n var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default))\n );\n }\n.react-flow__background-pattern.cross {\n stroke: var(\n --xy-background-pattern-color-props,\n var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default))\n );\n }\n.react-flow__controls {\n display: flex;\n flex-direction: column;\n box-shadow: var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default));\n}\n.react-flow__controls.horizontal {\n flex-direction: row;\n }\n.react-flow__controls-button {\n display: flex;\n justify-content: center;\n align-items: center;\n height: 26px;\n width: 26px;\n padding: 4px;\n border: none;\n background: var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));\n border-bottom: 1px solid\n var(\n --xy-controls-button-border-color-props,\n var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default))\n );\n color: var(\n --xy-controls-button-color-props,\n var(--xy-controls-button-color, var(--xy-controls-button-color-default))\n );\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n }\n.react-flow__controls-button svg {\n width: 100%;\n max-width: 12px;\n max-height: 12px;\n fill: currentColor;\n }\n.react-flow__edge.updating .react-flow__edge-path {\n stroke: #777;\n }\n.react-flow__edge-text {\n font-size: 10px;\n }\n.react-flow__node.selectable:focus,\n .react-flow__node.selectable:focus-visible {\n outline: none;\n }\n.react-flow__node-input,\n.react-flow__node-default,\n.react-flow__node-output,\n.react-flow__node-group {\n padding: 10px;\n border-radius: var(--xy-node-border-radius, var(--xy-node-border-radius-default));\n width: 150px;\n font-size: 12px;\n color: var(--xy-node-color, var(--xy-node-color-default));\n text-align: center;\n border: var(--xy-node-border, var(--xy-node-border-default));\n background-color: var(--xy-node-background-color, var(--xy-node-background-color-default));\n}\n.react-flow__node-input.selectable:hover, .react-flow__node-default.selectable:hover, .react-flow__node-output.selectable:hover, .react-flow__node-group.selectable:hover {\n box-shadow: var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default));\n }\n.react-flow__node-input.selectable.selected,\n .react-flow__node-input.selectable:focus,\n .react-flow__node-input.selectable:focus-visible,\n .react-flow__node-default.selectable.selected,\n .react-flow__node-default.selectable:focus,\n .react-flow__node-default.selectable:focus-visible,\n .react-flow__node-output.selectable.selected,\n .react-flow__node-output.selectable:focus,\n .react-flow__node-output.selectable:focus-visible,\n .react-flow__node-group.selectable.selected,\n .react-flow__node-group.selectable:focus,\n .react-flow__node-group.selectable:focus-visible {\n box-shadow: var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default));\n }\n.react-flow__node-group {\n background-color: var(--xy-node-group-background-color, var(--xy-node-group-background-color-default));\n}\n.react-flow__nodesselection-rect,\n.react-flow__selection {\n background: var(--xy-selection-background-color, var(--xy-selection-background-color-default));\n border: var(--xy-selection-border, var(--xy-selection-border-default));\n}\n.react-flow__nodesselection-rect:focus,\n .react-flow__nodesselection-rect:focus-visible,\n .react-flow__selection:focus,\n .react-flow__selection:focus-visible {\n outline: none;\n }\n.react-flow__controls-button:hover {\n background: var(\n --xy-controls-button-background-color-hover-props,\n var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default))\n );\n color: var(\n --xy-controls-button-color-hover-props,\n var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default))\n );\n }\n.react-flow__controls-button:disabled {\n pointer-events: none;\n }\n.react-flow__controls-button:disabled svg {\n fill-opacity: 0.4;\n }\n.react-flow__controls-button:last-child {\n border-bottom: none;\n }\n.react-flow__controls.horizontal .react-flow__controls-button {\n border-bottom: none;\n border-right: 1px solid\n var(\n --xy-controls-button-border-color-props,\n var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default))\n );\n }\n.react-flow__controls.horizontal .react-flow__controls-button:last-child {\n border-right: none;\n }\n.react-flow__resize-control {\n position: absolute;\n}\n.react-flow__resize-control.left,\n.react-flow__resize-control.right {\n cursor: ew-resize;\n}\n.react-flow__resize-control.top,\n.react-flow__resize-control.bottom {\n cursor: ns-resize;\n}\n.react-flow__resize-control.top.left,\n.react-flow__resize-control.bottom.right {\n cursor: nwse-resize;\n}\n.react-flow__resize-control.bottom.left,\n.react-flow__resize-control.top.right {\n cursor: nesw-resize;\n}\n/* handle styles */\n.react-flow__resize-control.handle {\n width: 5px;\n height: 5px;\n border: 1px solid #fff;\n border-radius: 1px;\n background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));\n translate: -50% -50%;\n}\n.react-flow__resize-control.handle.left {\n left: 0;\n top: 50%;\n}\n.react-flow__resize-control.handle.right {\n left: 100%;\n top: 50%;\n}\n.react-flow__resize-control.handle.top {\n left: 50%;\n top: 0;\n}\n.react-flow__resize-control.handle.bottom {\n left: 50%;\n top: 100%;\n}\n.react-flow__resize-control.handle.top.left {\n left: 0;\n}\n.react-flow__resize-control.handle.bottom.left {\n left: 0;\n}\n.react-flow__resize-control.handle.top.right {\n left: 100%;\n}\n.react-flow__resize-control.handle.bottom.right {\n left: 100%;\n}\n/* line styles */\n.react-flow__resize-control.line {\n border-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));\n border-width: 0;\n border-style: solid;\n}\n.react-flow__resize-control.line.left,\n.react-flow__resize-control.line.right {\n width: 1px;\n transform: translate(-50%, 0);\n top: 0;\n height: 100%;\n}\n.react-flow__resize-control.line.left {\n left: 0;\n border-left-width: 1px;\n}\n.react-flow__resize-control.line.right {\n left: 100%;\n border-right-width: 1px;\n}\n.react-flow__resize-control.line.top,\n.react-flow__resize-control.line.bottom {\n height: 1px;\n transform: translate(0, -50%);\n left: 0;\n width: 100%;\n}\n.react-flow__resize-control.line.top {\n top: 0;\n border-top-width: 1px;\n}\n.react-flow__resize-control.line.bottom {\n border-bottom-width: 1px;\n top: 100%;\n}\n.react-flow__edge-textbg {\n fill: var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default));\n}\n.react-flow__edge-text {\n fill: var(--xy-edge-label-color, var(--xy-edge-label-color-default));\n}\n";
|
|
254
|
+
|
|
255
|
+
// src/react/flow.tsx
|
|
256
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
257
|
+
var lighten = (color, amount) => {
|
|
258
|
+
const c = [1, 3, 5].map((i) => Number.parseInt(color.slice(i, i + 2), 16));
|
|
259
|
+
return `rgb(${c.map((v) => Math.round(v + (255 - v) * amount)).join(" ")})`;
|
|
260
|
+
};
|
|
261
|
+
var light = (color) => {
|
|
262
|
+
const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(color.slice(i, i + 2), 16) / 255);
|
|
263
|
+
return 0.299 * r + 0.587 * g + 0.114 * b > 0.6;
|
|
264
|
+
};
|
|
265
|
+
var Topic = ({ data }) => {
|
|
266
|
+
const { laid, metrics, image } = data;
|
|
267
|
+
const { node, depth, side } = laid;
|
|
268
|
+
const size = node.size * PX;
|
|
269
|
+
const fill = depth === 0 ? node.color : depth === 1 ? lighten(node.color, 0.85) : "#fff";
|
|
270
|
+
const ink = depth === 0 && !light(node.color) ? "#fff" : "#000";
|
|
271
|
+
const outer = side === "left" ? Position.Left : Position.Right;
|
|
272
|
+
const inner = side === "left" ? Position.Right : Position.Left;
|
|
273
|
+
return /* @__PURE__ */ jsxs(
|
|
274
|
+
"div",
|
|
275
|
+
{
|
|
276
|
+
className: "ts-mindmap-node",
|
|
277
|
+
"data-depth": Math.min(depth, 2),
|
|
278
|
+
style: {
|
|
279
|
+
["--ts-mindmap-color"]: node.color,
|
|
280
|
+
width: laid.w,
|
|
281
|
+
height: laid.h,
|
|
282
|
+
padding: `${0.3 * size}px ${0.5 * size}px`,
|
|
283
|
+
background: fill,
|
|
284
|
+
color: ink,
|
|
285
|
+
borderColor: node.color,
|
|
286
|
+
borderRadius: depth === 0 ? 0.4 * size + 0.3 * size : 4 * PX,
|
|
287
|
+
fontSize: size,
|
|
288
|
+
fontWeight: node.weight === "regular" ? 400 : node.weight === "bold" ? 700 : node.weight
|
|
289
|
+
},
|
|
290
|
+
children: [
|
|
291
|
+
metrics.image && /* @__PURE__ */ jsx("div", { className: "ts-mindmap-image", style: { height: metrics.image.h }, children: image ? /* @__PURE__ */ jsx("img", { src: image, alt: node.image?.alt ?? "" }) : /* @__PURE__ */ jsx("span", { children: node.image?.alt }) }),
|
|
292
|
+
/* @__PURE__ */ jsxs("div", { className: "ts-mindmap-label", children: [
|
|
293
|
+
/* @__PURE__ */ jsx("span", { dir: "auto", children: metrics.lines.map((line, i) => (
|
|
294
|
+
// Lines are the measured wrap, so the box fits them exactly; the same line can repeat.
|
|
295
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: lines have no identity of their own.
|
|
296
|
+
/* @__PURE__ */ jsx("span", { children: line }, i)
|
|
297
|
+
)) }),
|
|
298
|
+
laid.hidden > 0 && /* @__PURE__ */ jsxs("span", { className: "ts-mindmap-badge", style: { fontSize: size * 0.7 }, children: [
|
|
299
|
+
"+",
|
|
300
|
+
laid.hidden
|
|
301
|
+
] })
|
|
302
|
+
] }),
|
|
303
|
+
depth > 0 && /* @__PURE__ */ jsx(Handle, { type: "target", position: inner, isConnectable: false }),
|
|
304
|
+
depth === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
305
|
+
/* @__PURE__ */ jsx(Handle, { id: "right", type: "source", position: Position.Right, isConnectable: false }),
|
|
306
|
+
/* @__PURE__ */ jsx(Handle, { id: "left", type: "source", position: Position.Left, isConnectable: false })
|
|
307
|
+
] }) : /* @__PURE__ */ jsx(Handle, { type: "source", position: outer, isConnectable: false })
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
var Branch = ({ data, style }) => {
|
|
313
|
+
const [x1, y1, x2, y2] = data?.points ?? [0, 0, 0, 0];
|
|
314
|
+
const mx = (x1 + x2) / 2;
|
|
315
|
+
return /* @__PURE__ */ jsx(BaseEdge, { path: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`, ...style ? { style } : {} });
|
|
316
|
+
};
|
|
317
|
+
var nodeTypes = { topic: Topic };
|
|
318
|
+
var edgeTypes = { branch: Branch };
|
|
319
|
+
var useImages = (file) => {
|
|
320
|
+
const urls = useRef(/* @__PURE__ */ new Map());
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const map = urls.current;
|
|
323
|
+
return () => {
|
|
324
|
+
for (const url of map.values()) if (url) URL.revokeObjectURL(url);
|
|
325
|
+
map.clear();
|
|
326
|
+
};
|
|
327
|
+
}, []);
|
|
328
|
+
return useCallback(
|
|
329
|
+
(src) => {
|
|
330
|
+
const map = urls.current;
|
|
331
|
+
if (!map.has(src)) {
|
|
332
|
+
const content = file(src);
|
|
333
|
+
map.set(src, content instanceof Uint8Array ? URL.createObjectURL(new Blob([content])) : null);
|
|
334
|
+
}
|
|
335
|
+
return map.get(src) ?? null;
|
|
336
|
+
},
|
|
337
|
+
[file]
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
var MindMapView = ({ data, isIncomplete, file }) => {
|
|
341
|
+
const wrapper = useRef(null);
|
|
342
|
+
const [font, setFont] = useState(null);
|
|
343
|
+
const [toggled, setToggled] = useState(/* @__PURE__ */ new Map());
|
|
344
|
+
const image = useImages(file);
|
|
345
|
+
const flow = useReactFlow();
|
|
346
|
+
const width = useStore((s) => s.width);
|
|
347
|
+
const height = useStore((s) => s.height);
|
|
348
|
+
const moved = useRef(false);
|
|
349
|
+
useLayoutEffect(() => {
|
|
350
|
+
const el = wrapper.current;
|
|
351
|
+
if (!el) return;
|
|
352
|
+
adoptCss(el, XYFLOW_CSS);
|
|
353
|
+
adoptCss(el, MINDMAP_CSS);
|
|
354
|
+
setFont(getComputedStyle(el).fontFamily);
|
|
355
|
+
}, []);
|
|
356
|
+
const closed = useCallback((id, n) => toggled.get(id) ?? n.collapsed === true, [toggled]);
|
|
357
|
+
const { nodes, edges } = useMemo(() => {
|
|
358
|
+
if (font === null) return { nodes: [], edges: [] };
|
|
359
|
+
const metrics = /* @__PURE__ */ new WeakMap();
|
|
360
|
+
const layout = layoutMindMap(data, closed, (n, depth, hidden) => {
|
|
361
|
+
const m = measureNode(n, depth, hidden, data, font);
|
|
362
|
+
metrics.set(n, m);
|
|
363
|
+
return m;
|
|
364
|
+
});
|
|
365
|
+
const nodes2 = layout.nodes.map((laid) => {
|
|
366
|
+
const kids = laid.node.children?.length ?? 0;
|
|
367
|
+
const open = kids > 0 && !closed(laid.id, laid.node);
|
|
368
|
+
return {
|
|
369
|
+
id: laid.id,
|
|
370
|
+
type: "topic",
|
|
371
|
+
position: { x: laid.x, y: laid.y },
|
|
372
|
+
width: laid.w,
|
|
373
|
+
height: laid.h,
|
|
374
|
+
data: {
|
|
375
|
+
laid,
|
|
376
|
+
metrics: metrics.get(laid.node),
|
|
377
|
+
image: laid.node.image && data.style === "visual" ? image(laid.node.image.src) : null,
|
|
378
|
+
open
|
|
379
|
+
},
|
|
380
|
+
draggable: false,
|
|
381
|
+
selectable: false,
|
|
382
|
+
connectable: false,
|
|
383
|
+
ariaLabel: kids > 0 ? `${laid.node.label} (${open ? "expanded" : `collapsed, ${laid.hidden} hidden`})` : laid.node.label || " "
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
const edges2 = layout.edges.map((e) => ({
|
|
387
|
+
id: e.id,
|
|
388
|
+
type: "branch",
|
|
389
|
+
data: { points: e.points },
|
|
390
|
+
source: e.source,
|
|
391
|
+
target: e.target,
|
|
392
|
+
...e.source === "0" ? { sourceHandle: e.side } : {},
|
|
393
|
+
focusable: false,
|
|
394
|
+
selectable: false,
|
|
395
|
+
style: { stroke: e.color, strokeWidth: 1.2 * PX }
|
|
396
|
+
}));
|
|
397
|
+
return { nodes: nodes2, edges: edges2 };
|
|
398
|
+
}, [data, font, closed, image]);
|
|
399
|
+
const ids = nodes.map((n) => n.id).join(" ");
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
if (!moved.current && ids && width && height) void flow.fitView({ padding: 0.05 });
|
|
402
|
+
}, [flow, ids, width, height]);
|
|
403
|
+
const toggle = (id) => {
|
|
404
|
+
const n = nodes.find((m) => m.id === id);
|
|
405
|
+
if (!n?.data.laid.node.children?.length) return;
|
|
406
|
+
setToggled((t) => new Map(t).set(id, n.data.open));
|
|
407
|
+
};
|
|
408
|
+
const onKeyDown = (event) => {
|
|
409
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
410
|
+
const id = event.target.closest?.(".react-flow__node")?.getAttribute("data-id");
|
|
411
|
+
if (!id) return;
|
|
412
|
+
event.preventDefault();
|
|
413
|
+
toggle(id);
|
|
414
|
+
};
|
|
415
|
+
return (
|
|
416
|
+
// A figure, as the drawing it covers; keys from its nodes bubble up to open and close them.
|
|
417
|
+
/* @__PURE__ */ jsx(
|
|
418
|
+
"figure",
|
|
419
|
+
{
|
|
420
|
+
ref: wrapper,
|
|
421
|
+
className: "ts-mindmap",
|
|
422
|
+
"aria-label": data.root.label,
|
|
423
|
+
"aria-busy": isIncomplete,
|
|
424
|
+
onKeyDown,
|
|
425
|
+
children: font !== null && /* @__PURE__ */ jsx(
|
|
426
|
+
ReactFlow,
|
|
427
|
+
{
|
|
428
|
+
nodes,
|
|
429
|
+
edges,
|
|
430
|
+
nodeTypes,
|
|
431
|
+
edgeTypes,
|
|
432
|
+
colorMode: "light",
|
|
433
|
+
proOptions: { hideAttribution: true },
|
|
434
|
+
fitView: true,
|
|
435
|
+
fitViewOptions: { padding: 0.05 },
|
|
436
|
+
minZoom: 0.1,
|
|
437
|
+
maxZoom: 4,
|
|
438
|
+
nodesDraggable: false,
|
|
439
|
+
nodesConnectable: false,
|
|
440
|
+
elementsSelectable: false,
|
|
441
|
+
edgesFocusable: false,
|
|
442
|
+
zoomOnScroll: false,
|
|
443
|
+
panOnScroll: false,
|
|
444
|
+
preventScrolling: false,
|
|
445
|
+
zoomOnDoubleClick: false,
|
|
446
|
+
onNodeClick: (_, n) => toggle(n.id),
|
|
447
|
+
onMoveStart: (event) => {
|
|
448
|
+
if (event) moved.current = true;
|
|
449
|
+
},
|
|
450
|
+
children: height >= 140 && /* @__PURE__ */ jsx(Controls, { showInteractive: false, position: "bottom-left" })
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
var MindMapFlow = (props) => /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(MindMapView, { ...props }) });
|
|
458
|
+
var flow_default = MindMapFlow;
|
|
459
|
+
export {
|
|
460
|
+
flow_default as default
|
|
461
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TypstreamPlugin, FunctionDoc, PromptExample, IslandSpec } from '@typstream/core';
|
|
2
|
+
|
|
3
|
+
/** The Typst package's version, as in its `typst.toml`. */
|
|
4
|
+
declare const MINDMAP_VERSION = "0.1.0";
|
|
5
|
+
/**
|
|
6
|
+
* The package spec the mind map's files are mounted under: `${MINDMAP_ROOT}/lib.typ` and so on,
|
|
7
|
+
* a local package the engine reads from the files the plugin mounts (as `@typstream/edu`'s).
|
|
8
|
+
*/
|
|
9
|
+
declare const MINDMAP_ROOT = "@typstream/mindmap:0.1.0";
|
|
10
|
+
/** The Typst sources and `typst.toml`, keyed by their path in the engine (under {@link MINDMAP_ROOT}). */
|
|
11
|
+
declare const mindmapFiles: Readonly<Record<string, string>>;
|
|
12
|
+
/** Typst to put before a document: imports `mindmap` and `topic`. */
|
|
13
|
+
declare const mindmapPreamble: string;
|
|
14
|
+
/** One entry per public function, from the doc comments in `typst/`. */
|
|
15
|
+
declare const mindmapCatalogue: FunctionDoc[];
|
|
16
|
+
/** Whole documents with mind maps, for few-shot prompts. */
|
|
17
|
+
declare const mindmapExamples: PromptExample[];
|
|
18
|
+
/**
|
|
19
|
+
* How to write mind maps, for the system prompt. They follow what models get wrong about lists
|
|
20
|
+
* in Typst, and the content rules of teachers' mind maps: short labels, a logical order,
|
|
21
|
+
* colours and images only where they mean something.
|
|
22
|
+
*/
|
|
23
|
+
declare const mindmapPromptRules: string[];
|
|
24
|
+
/** A node of a mind map, as the marker carries it: every value resolved by the Typst side. */
|
|
25
|
+
interface MindMapNode {
|
|
26
|
+
/** Its label as plain text (math's sub- and superscripts as Unicode). */
|
|
27
|
+
label: string;
|
|
28
|
+
/** Its colour, `#rrggbb` or `#rrggbbaa`: border, tint, and the lines to its children. */
|
|
29
|
+
color: string;
|
|
30
|
+
/** Text size, in points. */
|
|
31
|
+
size: number;
|
|
32
|
+
/** Font weight: `bold`, `regular`, or a number from 100 to 900. */
|
|
33
|
+
weight: "bold" | "regular" | number;
|
|
34
|
+
/** On a main branch: the side of the centre it is drawn on. */
|
|
35
|
+
side?: "left" | "right";
|
|
36
|
+
/** An image above the label: the path the document named (a URL, a file) and its caption. */
|
|
37
|
+
image?: {
|
|
38
|
+
src: string;
|
|
39
|
+
alt: string;
|
|
40
|
+
};
|
|
41
|
+
/** Its branches start hidden. */
|
|
42
|
+
collapsed?: boolean;
|
|
43
|
+
/** Its branches, in order; a collapsed node's too, which the interactive map can open. */
|
|
44
|
+
children?: MindMapNode[];
|
|
45
|
+
}
|
|
46
|
+
/** A mind map island's data. */
|
|
47
|
+
interface MindMapData {
|
|
48
|
+
/** The format's version. */
|
|
49
|
+
v: 1;
|
|
50
|
+
/** `visual` (colours, images) or `text-focused` (neutral, no images, wider labels). */
|
|
51
|
+
style: "visual" | "text-focused";
|
|
52
|
+
/** Branches on both sides of the centre, or all on one. */
|
|
53
|
+
layout: "both" | "right";
|
|
54
|
+
/** The text direction; a one-sided map grows in it. */
|
|
55
|
+
dir: "ltr" | "rtl";
|
|
56
|
+
/** The text size of the main branches, in points; other sizes are relative to it. */
|
|
57
|
+
size: number;
|
|
58
|
+
/** The centre topic. */
|
|
59
|
+
root: MindMapNode;
|
|
60
|
+
}
|
|
61
|
+
/** The most nodes a map may have; a larger one keeps its fallback drawing. */
|
|
62
|
+
declare const MAX_NODES = 2000;
|
|
63
|
+
/**
|
|
64
|
+
* Checks a mind map marker's data, which the document wrote and is untrusted: the shape the Typst
|
|
65
|
+
* side writes, with sizes, colours and lengths in range. `undefined` for anything else, which
|
|
66
|
+
* keeps the fallback drawing.
|
|
67
|
+
*/
|
|
68
|
+
declare const parseMindMap: (data: unknown) => MindMapData | undefined;
|
|
69
|
+
/** The island's spec: what the plugin declares under `islands.mindmap`. @beta */
|
|
70
|
+
declare const mindmapIsland: IslandSpec<MindMapData>;
|
|
71
|
+
/**
|
|
72
|
+
* Mind maps as a typstream plugin: the Typst package and its preamble for the engine, the prompt
|
|
73
|
+
* kit, and the `mindmap` island. Pair it with `@typstream/mindmap/react`'s `MindMapIsland` for
|
|
74
|
+
* the interactive map; without it, documents show the drawing.
|
|
75
|
+
*/
|
|
76
|
+
declare const mindmap: TypstreamPlugin;
|
|
77
|
+
|
|
78
|
+
export { MAX_NODES, MINDMAP_ROOT, MINDMAP_VERSION, type MindMapData, type MindMapNode, mindmap, mindmapCatalogue, mindmapExamples, mindmapFiles, mindmapIsland, mindmapPreamble, mindmapPromptRules, parseMindMap };
|