mellos-mapping 0.20.2 → 0.22.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 (68) hide show
  1. package/README.md +125 -88
  2. package/README.zh-CN.md +122 -84
  3. package/dist/hook-session-start.mjs +25 -19
  4. package/dist/mmap.mjs +302 -170
  5. package/dist/preview.mjs +1418 -0
  6. package/dist/server.mjs +1198 -400
  7. package/dist/store-paths.mjs +40 -21
  8. package/dist/terminal-worker.mjs +3188 -0
  9. package/dist/watch.mjs +523 -280
  10. package/dist/web/TERMINAL-LICENSES.txt +70 -0
  11. package/dist/web/app.css +967 -0
  12. package/dist/web/app.js +1356 -0
  13. package/dist/web/index.html +9 -0
  14. package/dist/web/terminal.css +9 -0
  15. package/dist/web/terminal.html +7 -0
  16. package/dist/web/terminal.js +9293 -0
  17. package/dist/web/xterm.css +285 -0
  18. package/dist/web.mjs +5535 -0
  19. package/docs/codex.md +183 -0
  20. package/lib/domain/text.d.ts +9 -0
  21. package/lib/domain/text.js +43 -0
  22. package/lib/domain/types.js +10 -1
  23. package/lib/preview/index.d.ts +3 -0
  24. package/lib/preview/index.js +3 -0
  25. package/lib/preview/markdown.d.ts +8 -0
  26. package/lib/preview/markdown.js +54 -0
  27. package/lib/preview/presentation.d.ts +6 -0
  28. package/lib/preview/presentation.js +14 -0
  29. package/lib/preview/publisher.d.ts +23 -0
  30. package/lib/preview/publisher.js +143 -0
  31. package/lib/preview/svg.d.ts +3 -0
  32. package/lib/preview/svg.js +74 -0
  33. package/lib/preview/text.d.ts +4 -0
  34. package/lib/preview/text.js +13 -0
  35. package/lib/render/canvas.d.ts +1 -1
  36. package/lib/render/canvas.js +4 -2
  37. package/lib/render/draw.js +9 -6
  38. package/lib/render/render.d.ts +6 -0
  39. package/lib/render/render.js +50 -18
  40. package/lib/render/width.js +3 -1
  41. package/lib/store/atomic.d.ts +18 -0
  42. package/lib/store/atomic.js +86 -0
  43. package/lib/store/channels.d.ts +40 -0
  44. package/lib/store/channels.js +135 -0
  45. package/lib/store/format.js +3 -1
  46. package/lib/store/json-text.d.ts +9 -0
  47. package/lib/store/json-text.js +16 -0
  48. package/lib/store/maps.d.ts +12 -0
  49. package/lib/store/maps.js +42 -0
  50. package/lib/store/migration.d.ts +12 -0
  51. package/lib/store/migration.js +46 -0
  52. package/lib/store/pages.d.ts +46 -0
  53. package/lib/store/pages.js +89 -0
  54. package/lib/store/policy.d.ts +74 -0
  55. package/lib/store/policy.js +144 -0
  56. package/lib/store/store.d.ts +9 -256
  57. package/lib/store/store.js +10 -694
  58. package/lib/store/viewers.d.ts +81 -0
  59. package/lib/store/viewers.js +186 -0
  60. package/package.json +25 -6
  61. package/scripts/codex-cli.mjs +42 -0
  62. package/scripts/codex-register.mjs +27 -94
  63. package/scripts/mmap.mjs +26 -16
  64. package/scripts/open-pane.mjs +37 -18
  65. package/scripts/pane-core.mjs +57 -220
  66. package/scripts/terminal-session.mjs +137 -0
  67. package/scripts/tmux-session.mjs +90 -0
  68. package/scripts/watcher-command.mjs +16 -0
@@ -0,0 +1,1356 @@
1
+ // src/domain/types.ts
2
+ var RANK_MIN = 0;
3
+ var RANK_MAX = 99;
4
+ var RANK_RULE_TEXT = `an integer in ${RANK_MIN}..${RANK_MAX}, 0 = bottom / most primitive`;
5
+
6
+ // src/domain/ops.ts
7
+ function aggregateStatus(nodes) {
8
+ if (nodes.some((n) => n.status === "regressed")) return "regressed";
9
+ if (nodes.some((n) => n.status === "in-progress")) return "in-progress";
10
+ if (nodes.length > 0 && nodes.every((n) => n.status === "done")) return "done";
11
+ return "planned";
12
+ }
13
+ function groupStatus(map, id) {
14
+ return aggregateStatus(map.nodes.filter((n) => n.group === id));
15
+ }
16
+
17
+ // src/semantics/vocabulary.ts
18
+ var STATUS_GLYPHS = {
19
+ planned: ["\xB7", "."],
20
+ "in-progress": ["\u283F", "*"],
21
+ done: ["\u25A0", "#"],
22
+ regressed: ["\u2717", "X"]
23
+ };
24
+ function statusGlyph(status, unicode) {
25
+ const [uni, ascii] = STATUS_GLYPHS[status];
26
+ return unicode ? uni : ascii;
27
+ }
28
+ var UNVERIFIED_DONE_GLYPHS = ["\u25A1", "o"];
29
+ function unverifiedDoneGlyph(unicode) {
30
+ const [uni, ascii] = UNVERIFIED_DONE_GLYPHS;
31
+ return unicode ? uni : ascii;
32
+ }
33
+ var NODE_KIND_GLYPHS = {
34
+ selector: ["?", "?"],
35
+ sequence: ["\xBB", ">"],
36
+ parallel: ["\u2016", "="],
37
+ decorator: ["\u25CC", "o"],
38
+ condition: ["\u25C7", "c"],
39
+ action: ["\xB7", "."],
40
+ source: ["\u25CB", "o"],
41
+ transform: ["\u25D0", "%"],
42
+ sink: ["\u25CF", "*"],
43
+ service: ["\u25C6", "S"],
44
+ db: ["\u25A4", "D"],
45
+ queue: ["\u2263", "Q"],
46
+ ui: ["\u25A3", "U"]
47
+ };
48
+ function kindGlyph(kind, unicode) {
49
+ const pair = NODE_KIND_GLYPHS[kind];
50
+ return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
51
+ }
52
+
53
+ // src/semantics/semantics.ts
54
+ function zoomMode(zoom2) {
55
+ if (zoom2 >= 1) return "detail";
56
+ if (zoom2 <= -4) return "overview";
57
+ return "boxes";
58
+ }
59
+ function isNeutralKind(map) {
60
+ return map.kind !== void 0 && map.kind !== "dev";
61
+ }
62
+ function aggregateMap(map) {
63
+ if (map.groups.length === 0) return void 0;
64
+ const representative = /* @__PURE__ */ new Map();
65
+ for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
66
+ const nodes = map.groups.map((g) => {
67
+ const members = map.nodes.filter((n) => n.group === g.id);
68
+ const done = members.filter((n) => n.status === "done").length;
69
+ return {
70
+ id: g.id,
71
+ // neutral kinds document structure, not progress — no member counts
72
+ label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
73
+ layer: g.layer,
74
+ status: groupStatus(map, g.id)
75
+ };
76
+ });
77
+ for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
78
+ const seen = /* @__PURE__ */ new Set();
79
+ const edges = [];
80
+ for (const e of map.edges) {
81
+ const from = representative.get(e.from);
82
+ const to = representative.get(e.to);
83
+ if (from === to || seen.has(`${from}->${to}`)) continue;
84
+ seen.add(`${from}->${to}`);
85
+ edges.push({ from, to });
86
+ }
87
+ return {
88
+ ...map.title !== void 0 ? { title: map.title } : {},
89
+ ...map.kind !== void 0 ? { kind: map.kind } : {},
90
+ layers: map.layers,
91
+ groups: [],
92
+ lanes: map.lanes,
93
+ nodes,
94
+ edges
95
+ };
96
+ }
97
+ function focusInfo(map, focusId) {
98
+ const layerNameOf = (layerId) => map.layers.find((l) => l.id === layerId)?.name ?? layerId;
99
+ const group = map.groups.find((g) => g.id === focusId);
100
+ if (group) {
101
+ const members = map.nodes.filter((n) => n.group === group.id);
102
+ const memberIds = new Set(members.map((n) => n.id));
103
+ const rep = (id) => {
104
+ const n = map.nodes.find((x) => x.id === id);
105
+ const owner = n.group !== void 0 ? map.groups.find((g) => g.id === n.group) : void 0;
106
+ return owner !== void 0 ? { id: owner.id, label: owner.label, status: groupStatus(map, owner.id) } : { id: n.id, label: n.label, status: n.status };
107
+ };
108
+ const dedupe = (refs) => {
109
+ const seen = /* @__PURE__ */ new Set();
110
+ const out = [];
111
+ for (const r of refs) {
112
+ if (seen.has(r.id)) continue;
113
+ seen.add(r.id);
114
+ out.push(r);
115
+ }
116
+ return out;
117
+ };
118
+ return {
119
+ kind: "group",
120
+ group,
121
+ status: groupStatus(map, group.id),
122
+ layerName: layerNameOf(group.layer),
123
+ members,
124
+ uses: dedupe(
125
+ map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => rep(e.to))
126
+ ),
127
+ usedBy: dedupe(
128
+ map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => rep(e.from))
129
+ )
130
+ };
131
+ }
132
+ const node = map.nodes.find((n) => n.id === focusId);
133
+ if (!node) return void 0;
134
+ const ref = (id, edgeLabel) => {
135
+ const n = map.nodes.find((x) => x.id === id);
136
+ return {
137
+ id,
138
+ label: n?.label ?? id,
139
+ status: n?.status ?? "planned",
140
+ ...edgeLabel !== void 0 ? { edgeLabel } : {}
141
+ };
142
+ };
143
+ const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
144
+ return {
145
+ kind: "node",
146
+ node,
147
+ layerName: layerNameOf(node.layer),
148
+ ...laneLabel !== void 0 ? { laneLabel } : {},
149
+ uses: map.edges.filter((e) => e.from === node.id).map((e) => ref(e.to, e.label)),
150
+ usedBy: map.edges.filter((e) => e.to === node.id).map((e) => ref(e.from, e.label))
151
+ };
152
+ }
153
+ function mostRecentKey(keys, mtimeOf) {
154
+ let best;
155
+ let bestMtime = -Infinity;
156
+ for (const key of keys) {
157
+ const mtime = mtimeOf(key);
158
+ if (mtime !== void 0 && mtime > bestMtime) {
159
+ best = key;
160
+ bestMtime = mtime;
161
+ }
162
+ }
163
+ return best ?? keys[0];
164
+ }
165
+ function flipForSequence(map) {
166
+ if (map.kind !== "sequence") return map;
167
+ return {
168
+ ...map,
169
+ // VIOLATION: state-explicit-in-types - `-l.rank as Rank` produces a value
170
+ // the Rank brand promises cannot exist: mirroring 0..99 gives -99..0, and
171
+ // makeRank would refuse every one of them. The alternative is a second
172
+ // ordered-position type (an unbranded `order` field) threaded through the
173
+ // renderer's whole layout stage purely so this one derived map can be
174
+ // typed — a large change to express "these ranks are an order, not a
175
+ // stored value". What makes it safe is the same thing that makes it
176
+ // wrong: this map only ever reaches a renderer, which compares ranks and
177
+ // never writes them (same contract as aggregateMap).
178
+ layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
179
+ edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
180
+ };
181
+ }
182
+
183
+ // src/preview/presentation.ts
184
+ var LABELS = { planned: "\u5F85\u5F00\u53D1", "in-progress": "\u5F00\u53D1\u4E2D", done: "\u5DF2\u9A8C\u8BC1", regressed: "\u51FA\u73B0\u56DE\u5F52" };
185
+ function isVerified(node) {
186
+ return node.status === "done" && node.evidence !== void 0;
187
+ }
188
+ function statusText(node) {
189
+ return node.status === "done" && !isVerified(node) ? `${unverifiedDoneGlyph(true)} \u5B8C\u6210\u4F46\u7F3A\u5C11\u8BC1\u636E` : `${statusGlyph(node.status, true)} ${LABELS[node.status]}`;
190
+ }
191
+
192
+ // src/render/zoom-geometry.ts
193
+ var BOX_H = 3;
194
+ var BOX_GAP = 2;
195
+ var LEFT_MARGIN = 2;
196
+ var BAR_MIN_RUN = 7;
197
+ var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
198
+ var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
199
+ function zoomGeometry(zoom2) {
200
+ const m = zoomMode(zoom2);
201
+ const mode = m === "overview" ? "constellation" : m;
202
+ switch (zoom2) {
203
+ case 2:
204
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
205
+ case 1:
206
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
207
+ case 0:
208
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
209
+ case -1:
210
+ return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
211
+ case -2:
212
+ return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
213
+ case -3:
214
+ return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
215
+ case -4:
216
+ return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
217
+ }
218
+ }
219
+
220
+ // src/domain/text.ts
221
+ function terminalText(text, multiline = false) {
222
+ return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
223
+ }
224
+
225
+ // src/render/width.ts
226
+ var WIDE_RANGES = [
227
+ [4352, 4447],
228
+ // Hangul Jamo
229
+ // Wide symbols scattered through the BMP — mostly emoji that predate the
230
+ // emoji planes (⌚ ⏰ ⚡ ✅ ✨ ❌ ❓ ⭐ ⬛ …).
231
+ [8986, 8987],
232
+ [9001, 9002],
233
+ [9193, 9196],
234
+ [9200, 9200],
235
+ [9203, 9203],
236
+ [9725, 9726],
237
+ [9748, 9749],
238
+ [9800, 9811],
239
+ [9855, 9855],
240
+ [9875, 9875],
241
+ [9889, 9889],
242
+ [9898, 9899],
243
+ [9917, 9918],
244
+ [9924, 9925],
245
+ [9934, 9934],
246
+ [9940, 9940],
247
+ [9962, 9962],
248
+ [9970, 9971],
249
+ [9973, 9973],
250
+ [9978, 9978],
251
+ [9981, 9981],
252
+ [9989, 9989],
253
+ [9994, 9995],
254
+ [10024, 10024],
255
+ [10060, 10060],
256
+ [10062, 10062],
257
+ [10067, 10069],
258
+ [10071, 10071],
259
+ [10133, 10135],
260
+ [10160, 10160],
261
+ [10175, 10175],
262
+ [11035, 11036],
263
+ [11088, 11088],
264
+ [11093, 11093],
265
+ [11904, 42191],
266
+ // CJK radicals .. Yi (covers CJK Unified Ideographs)
267
+ [43360, 43391],
268
+ [44032, 55203],
269
+ // Hangul syllables
270
+ [63744, 64255],
271
+ // CJK compatibility ideographs
272
+ [65040, 65049],
273
+ [65072, 65135],
274
+ [65280, 65376],
275
+ // fullwidth forms
276
+ [65504, 65510],
277
+ [127744, 128591],
278
+ // pictographs, transport, emoticons (🚀 🎯 😀 …)
279
+ [128640, 128767],
280
+ [129280, 129535],
281
+ // supplemental symbols (🤖 🧱 …)
282
+ [129648, 129791],
283
+ // symbols extended-A
284
+ [131072, 262141]
285
+ // CJK extension planes
286
+ ];
287
+ var ZERO_WIDTH_RANGES = [
288
+ [768, 879],
289
+ // combining diacritical marks (decomposed 'e' + ´)
290
+ [6832, 6911],
291
+ [7616, 7679],
292
+ [8203, 8207],
293
+ // zero-width space .. RLM, zero-width joiner among them
294
+ [8400, 8432],
295
+ // combining marks for symbols
296
+ [65024, 65039],
297
+ // variation selectors, VS16 (emoji presentation) included
298
+ [65056, 65071],
299
+ // combining half marks
300
+ [127995, 127999]
301
+ // emoji skin tone modifiers — always applied to a base
302
+ ];
303
+ function inRanges(cp, ranges) {
304
+ for (const [lo, hi] of ranges) {
305
+ if (cp >= lo && cp <= hi) return true;
306
+ }
307
+ return false;
308
+ }
309
+ function charWidth(cp) {
310
+ if (inRanges(cp, ZERO_WIDTH_RANGES)) return 0;
311
+ return inRanges(cp, WIDE_RANGES) ? 2 : 1;
312
+ }
313
+ function displayWidth(text) {
314
+ let w = 0;
315
+ for (const ch of text) w += charWidth(ch.codePointAt(0));
316
+ return w;
317
+ }
318
+ function fitWidth(s, width) {
319
+ s = terminalText(s);
320
+ if (displayWidth(s) <= width) return s;
321
+ let out = "";
322
+ let w = 0;
323
+ for (const ch of s) {
324
+ const cw = displayWidth(ch);
325
+ if (w + cw > width - 1) break;
326
+ out += ch;
327
+ w += cw;
328
+ }
329
+ return out + "\u2026";
330
+ }
331
+ function wrapWidth(s, width) {
332
+ const lines = [];
333
+ let line = "";
334
+ let w = 0;
335
+ for (const ch of terminalText(s.replace(/\r/g, "").replace(/\t/g, " "), true)) {
336
+ if (ch === "\n") {
337
+ lines.push(line);
338
+ line = "";
339
+ w = 0;
340
+ continue;
341
+ }
342
+ const cw = displayWidth(ch);
343
+ if (w + cw > width) {
344
+ lines.push(line);
345
+ line = "";
346
+ w = 0;
347
+ }
348
+ line += ch;
349
+ w += cw;
350
+ }
351
+ if (line !== "") lines.push(line);
352
+ return lines;
353
+ }
354
+
355
+ // src/render/layout.ts
356
+ var LABEL_BUDGET_MIN = 4;
357
+ function boxSpec(node, geo, unicode, neutral) {
358
+ const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
359
+ const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
360
+ const badgeW = displayWidth(badge);
361
+ const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
362
+ if (geo.mode === "constellation") {
363
+ return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
364
+ }
365
+ if (geo.mode === "detail" && geo.detail !== void 0) {
366
+ const budget2 = geo.detail;
367
+ const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
368
+ const extra = [];
369
+ if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
370
+ if (node.detail !== void 0) {
371
+ const wrapped = wrapWidth(node.detail, innerW - 2);
372
+ for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
373
+ const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
374
+ extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
375
+ }
376
+ }
377
+ return {
378
+ w: innerW + 2,
379
+ h: BOX_H + extra.length,
380
+ label: fitWidth(text, innerW - 4 - badgeW) + badge,
381
+ pad: 1,
382
+ borderless: false,
383
+ extra
384
+ };
385
+ }
386
+ const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
387
+ const label = fitWidth(text, budget) + badge;
388
+ return {
389
+ w: displayWidth(label) + 4 + 2 * geo.pad,
390
+ h: BOX_H,
391
+ label,
392
+ pad: geo.pad,
393
+ borderless: false,
394
+ extra: []
395
+ };
396
+ }
397
+ function layoutColumns(map, geo, unicode, neutral) {
398
+ const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
399
+ const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
400
+ const sized = /* @__PURE__ */ new Map();
401
+ const bandSized = bands.map(() => []);
402
+ for (const node of map.nodes) {
403
+ const spec = { node, ...boxSpec(node, geo, unicode, neutral) };
404
+ bandSized[bandIndexOf.get(node.layer)].push(spec);
405
+ sized.set(node.id, spec);
406
+ }
407
+ const columnOf = /* @__PURE__ */ new Map();
408
+ const lanes = [];
409
+ if (map.lanes.length === 0) {
410
+ for (const row of bandSized) {
411
+ let x = LEFT_MARGIN;
412
+ for (const spec of row) {
413
+ columnOf.set(spec, x);
414
+ x += spec.w + geo.boxGap;
415
+ }
416
+ }
417
+ } else {
418
+ const laneCount = map.lanes.length;
419
+ const laneGap = geo.boxGap + 2;
420
+ const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
421
+ const regions = laneCount + 1;
422
+ const grouped = bandSized.map((row) => {
423
+ const cells = Array.from({ length: regions }, () => []);
424
+ for (const spec of row) {
425
+ const lane = spec.node.lane;
426
+ cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(spec);
427
+ }
428
+ return cells;
429
+ });
430
+ const regionW = Array.from({ length: regions }, () => 0);
431
+ for (const cells of grouped) {
432
+ for (let i = 0; i < regions; i++) {
433
+ const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
434
+ regionW[i] = Math.max(regionW[i], rowW);
435
+ }
436
+ }
437
+ for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
438
+ let x0 = LEFT_MARGIN;
439
+ for (let i = 0; i < regions; i++) {
440
+ lanes.push({ x: x0, w: regionW[i] });
441
+ x0 += regionW[i] + laneGap;
442
+ }
443
+ for (const cells of grouped) {
444
+ for (let i = 0; i < regions; i++) {
445
+ let x = lanes[i].x;
446
+ for (const spec of cells[i]) {
447
+ columnOf.set(spec, x);
448
+ x += spec.w + geo.boxGap;
449
+ }
450
+ }
451
+ }
452
+ }
453
+ const placed = /* @__PURE__ */ new Map();
454
+ for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
455
+ const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
456
+ const boxOf = /* @__PURE__ */ new Map();
457
+ for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
458
+ const bandLabel = bands.map((l, i) => {
459
+ const row = bandBoxes[i];
460
+ const done = row.filter((b) => b.node.status === "done").length;
461
+ return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
462
+ });
463
+ let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
464
+ for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
465
+ for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
466
+ return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
467
+ }
468
+ function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
469
+ let y = 0;
470
+ if (hasTitle) y += 1 + geo.titleGap;
471
+ let laneHeaderY;
472
+ if (hasLanes) {
473
+ laneHeaderY = y;
474
+ y += 1 + geo.barGap;
475
+ }
476
+ const barY = [];
477
+ const gapTrackStartY = [];
478
+ const bandBoxes = [];
479
+ const placed = /* @__PURE__ */ new Map();
480
+ const gapCount = columns.bands.length - 1;
481
+ for (let b = 0; b < columns.bands.length; b++) {
482
+ barY.push(y);
483
+ y += 1 + geo.barGap;
484
+ const row = columns.bandBoxes[b];
485
+ for (const box of row) placed.set(box, { ...box, y });
486
+ bandBoxes.push(row.map((box) => placed.get(box)));
487
+ y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
488
+ if (b < gapCount) {
489
+ y += geo.breathe;
490
+ gapTrackStartY.push(y);
491
+ y += gapRowCount[b];
492
+ y += geo.breathe;
493
+ }
494
+ }
495
+ const boxOf = /* @__PURE__ */ new Map();
496
+ for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
497
+ return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
498
+ }
499
+
500
+ // src/render/routing.ts
501
+ function routeEdges(map, columns) {
502
+ const { bandIndexOf, bandBoxes, boxOf, contentWidth } = columns;
503
+ const pending = map.edges.map((e) => {
504
+ const from = boxOf.get(e.from);
505
+ const to = boxOf.get(e.to);
506
+ return {
507
+ from,
508
+ to,
509
+ fromBand: bandIndexOf.get(from.node.layer),
510
+ toBand: bandIndexOf.get(to.node.layer)
511
+ };
512
+ });
513
+ const gapVerticals = Array.from(
514
+ { length: Math.max(0, columns.bands.length - 1) },
515
+ () => /* @__PURE__ */ new Map()
516
+ );
517
+ const verticalFree = (gap, x, edge) => {
518
+ const owner = gapVerticals[gap]?.get(x);
519
+ return owner === void 0 || owner === edge;
520
+ };
521
+ const takeVertical = (gap, x, edge) => {
522
+ gapVerticals[gap]?.set(x, edge);
523
+ };
524
+ const claimedColumns = /* @__PURE__ */ new Map();
525
+ const isFree = (box, x) => !(claimedColumns.get(box)?.has(x) ?? false);
526
+ const claim = (box, x) => {
527
+ let set = claimedColumns.get(box);
528
+ if (!set) claimedColumns.set(box, set = /* @__PURE__ */ new Set());
529
+ set.add(x);
530
+ return x;
531
+ };
532
+ for (const r of pending) {
533
+ if (r.toBand - r.fromBand !== 1) continue;
534
+ const lo = Math.max(r.from.x + 1, r.to.x + 1);
535
+ const hi = Math.min(r.from.x + r.from.w - 2, r.to.x + r.to.w - 2);
536
+ if (lo > hi) continue;
537
+ const mid = Math.floor((lo + hi) / 2);
538
+ for (let d = 0; d <= hi - lo && r.straightX === void 0; d++) {
539
+ for (const x of d === 0 ? [mid] : [mid - d, mid + d]) {
540
+ if (x >= lo && x <= hi && isFree(r.from, x) && isFree(r.to, x) && verticalFree(r.fromBand, x, r)) {
541
+ r.straightX = claim(r.to, claim(r.from, x));
542
+ takeVertical(r.fromBand, x, r);
543
+ break;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ const bent = pending.filter((r) => r.straightX === void 0);
549
+ const outgoing = /* @__PURE__ */ new Map();
550
+ const incoming = /* @__PURE__ */ new Map();
551
+ for (const r of bent) {
552
+ outgoing.set(r.from, [...outgoing.get(r.from) ?? [], r]);
553
+ incoming.set(r.to, [...incoming.get(r.to) ?? [], r]);
554
+ }
555
+ const freeSlot = (box, k, n, edge, gap) => {
556
+ const lo = box.x + 1;
557
+ const hi = box.x + box.w - 2;
558
+ const ideal = box.x + Math.min(box.w - 2, Math.max(1, Math.round((k + 1) * (box.w - 1) / (n + 1))));
559
+ for (let d = 0; d <= hi - lo; d++) {
560
+ for (const x of d === 0 ? [ideal] : [ideal - d, ideal + d]) {
561
+ if (x >= lo && x <= hi && isFree(box, x) && verticalFree(gap, x, edge)) {
562
+ takeVertical(gap, x, edge);
563
+ return claim(box, x);
564
+ }
565
+ }
566
+ }
567
+ return ideal;
568
+ };
569
+ for (const r of bent) {
570
+ const outs = outgoing.get(r.from);
571
+ const ins = incoming.get(r.to);
572
+ r.exitX = freeSlot(r.from, outs.indexOf(r), outs.length, r, r.fromBand);
573
+ r.entryX = freeSlot(r.to, ins.indexOf(r), ins.length, r, r.toBand - 1);
574
+ }
575
+ const usedDescent = /* @__PURE__ */ new Set();
576
+ let fallbackCount = 0;
577
+ const blockedByBox = (band, x) => bandBoxes[band].some((b) => x >= b.x && x <= b.x + b.w - 1);
578
+ const descentGapsFree = (r, c) => {
579
+ for (let g = r.fromBand; g <= r.toBand - 1; g++) {
580
+ if (!verticalFree(g, c, r)) return false;
581
+ }
582
+ return true;
583
+ };
584
+ for (const r of bent.filter((e) => e.toBand - e.fromBand > 1)) {
585
+ const ex = r.entryX;
586
+ let chosen;
587
+ for (let d = 0; d <= contentWidth && chosen === void 0; d++) {
588
+ for (const c of d === 0 ? [ex] : [ex - d, ex + d]) {
589
+ if (c < LEFT_MARGIN || c > contentWidth + 1 || usedDescent.has(c)) continue;
590
+ if (!descentGapsFree(r, c)) continue;
591
+ let blocked = false;
592
+ for (let b = r.fromBand + 1; b < r.toBand && !blocked; b++) blocked = blockedByBox(b, c);
593
+ if (!blocked) {
594
+ chosen = c;
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
600
+ usedDescent.add(chosen);
601
+ for (let g = r.fromBand; g <= r.toBand - 1; g++) takeVertical(g, chosen, r);
602
+ r.descentX = chosen;
603
+ }
604
+ const gapCount = Math.max(0, columns.bands.length - 1);
605
+ const gapSegments = Array.from(
606
+ { length: gapCount },
607
+ () => []
608
+ );
609
+ for (const r of bent) {
610
+ const sx = r.exitX;
611
+ const ex = r.entryX;
612
+ if (r.descentX === void 0) {
613
+ gapSegments[r.toBand - 1].push({
614
+ edge: r,
615
+ kind: "landing",
616
+ segment: { lo: Math.min(sx, ex), hi: Math.max(sx, ex) }
617
+ });
618
+ } else {
619
+ const c = r.descentX;
620
+ gapSegments[r.fromBand].push({ edge: r, kind: "exit", segment: { lo: Math.min(sx, c), hi: Math.max(sx, c) } });
621
+ gapSegments[r.toBand - 1].push({
622
+ edge: r,
623
+ kind: "landing",
624
+ segment: { lo: Math.min(c, ex), hi: Math.max(c, ex) }
625
+ });
626
+ }
627
+ }
628
+ const exitRow = /* @__PURE__ */ new Map();
629
+ const landingRow = /* @__PURE__ */ new Map();
630
+ const gapRowCount = gapSegments.map((entries) => {
631
+ const rowEnds = [];
632
+ for (const e of [...entries].sort((a, b) => a.segment.lo - b.segment.lo)) {
633
+ let row = rowEnds.findIndex((end) => e.segment.lo > end + 1);
634
+ if (row === -1) {
635
+ rowEnds.push(e.segment.hi);
636
+ row = rowEnds.length - 1;
637
+ } else {
638
+ rowEnds[row] = Math.max(rowEnds[row], e.segment.hi);
639
+ }
640
+ (e.kind === "exit" ? exitRow : landingRow).set(e.edge, row);
641
+ }
642
+ return rowEnds.length;
643
+ });
644
+ const edges = pending.map((r) => {
645
+ const common = { from: r.from, to: r.to, fromBand: r.fromBand, toBand: r.toBand };
646
+ if (r.straightX !== void 0) return { ...common, kind: "straight", x: r.straightX };
647
+ if (r.descentX === void 0) {
648
+ return { ...common, kind: "dogleg", exitX: r.exitX, entryX: r.entryX, landingRow: landingRow.get(r) };
649
+ }
650
+ return {
651
+ ...common,
652
+ kind: "thread",
653
+ exitX: r.exitX,
654
+ entryX: r.entryX,
655
+ descentX: r.descentX,
656
+ exitRow: exitRow.get(r),
657
+ landingRow: landingRow.get(r)
658
+ };
659
+ });
660
+ return { edges, gapRowCount, fallbackCount };
661
+ }
662
+ function edgePolyline(edge, rows) {
663
+ const from = rows.boxOf.get(edge.from.node.id);
664
+ const to = rows.boxOf.get(edge.to.node.id);
665
+ const sy = from.y + from.h - 1;
666
+ const ey = to.y;
667
+ if (edge.kind === "straight") {
668
+ return [
669
+ [edge.x, sy],
670
+ [edge.x, ey]
671
+ ];
672
+ }
673
+ const landingY = rows.gapTrackStartY[edge.toBand - 1] + edge.landingRow;
674
+ if (edge.kind === "dogleg") {
675
+ return [
676
+ [edge.exitX, sy],
677
+ [edge.exitX, landingY],
678
+ [edge.entryX, landingY],
679
+ [edge.entryX, ey]
680
+ ];
681
+ }
682
+ const exitY = rows.gapTrackStartY[edge.fromBand] + edge.exitRow;
683
+ return [
684
+ [edge.exitX, sy],
685
+ [edge.exitX, exitY],
686
+ [edge.descentX, exitY],
687
+ [edge.descentX, landingY],
688
+ [edge.entryX, landingY],
689
+ [edge.entryX, ey]
690
+ ];
691
+ }
692
+
693
+ // src/preview/text.ts
694
+ function xml(text) {
695
+ return text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
696
+ }
697
+
698
+ // src/preview/svg.ts
699
+ var X = 8;
700
+ var Y = 26;
701
+ var TOP = 20;
702
+ var PALETTES = {
703
+ planned: ["#f5f7fa", "#98a3b2", "#566477"],
704
+ "in-progress": ["#fff4d9", "#c48c24", "#805910"],
705
+ done: ["#e9f5ee", "#67a883", "#286247"],
706
+ regressed: ["#fdecec", "#cc7575", "#923d3d"],
707
+ unverified: ["#fff6e8", "#b49a77", "#785e3e"],
708
+ neutral: ["#f2f5f9", "#a1adbc", "#364558"]
709
+ };
710
+ function renderMapSvg(map) {
711
+ const neutral = isNeutralKind(map);
712
+ const originals = new Map(map.nodes.map((n) => [n.id, n]));
713
+ const oriented = flipForSequence(map);
714
+ const shaped = { ...oriented, nodes: oriented.nodes.map((n) => {
715
+ const label = fitWidth(n.label, 32);
716
+ return { ...n, label: label + " ".repeat(Math.max(0, 20 - displayWidth(label))) };
717
+ }) };
718
+ const geo = { ...zoomGeometry(0), boxGap: 6 };
719
+ const columns = layoutColumns(shaped, geo, true, neutral);
720
+ const routing = routeEdges(shaped, columns);
721
+ const rows = layoutRows(columns, geo, routing.gapRowCount, false, map.lanes.length > 0);
722
+ const width = Math.max(440, (columns.contentWidth + 4 + routing.fallbackCount * 2) * X);
723
+ const height = Math.max(100, TOP + rows.legendY * Y);
724
+ const parts = [
725
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="map-title map-desc">`,
726
+ `<title id="map-title">${xml(map.title ?? "\u6885\u52D2\u65AF\u5730\u56FE")}</title>`,
727
+ `<desc id="map-desc">${map.nodes.length} \u4E2A\u8282\u70B9\uFF0C${map.edges.length} \u6761\u4F9D\u8D56\u3002\u5B8C\u6574\u8BF4\u660E\u4E0E\u9A8C\u8BC1\u8BB0\u5F55\u5728\u5730\u56FE\u6587\u6863\u4E2D\u3002</desc>`,
728
+ '<defs><marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 L8 4 L0 8 Z" fill="#8995a5"/></marker></defs>',
729
+ `<rect width="${width}" height="${height}" fill="#ffffff"/>`,
730
+ '<g font-family="Segoe UI, Microsoft YaHei, Noto Sans CJK SC, sans-serif">'
731
+ ];
732
+ if (map.nodes.length === 0) parts.push('<text x="20" y="52" font-size="14" fill="#657286">\u5C1A\u672A\u58F0\u660E\u6A21\u5757\uFF0C\u7B49\u5F85\u5730\u56FE\u66F4\u65B0\u3002</text>');
733
+ columns.bands.forEach((band, i) => {
734
+ const top = TOP + rows.barY[i] * Y;
735
+ const boxes = rows.bandBoxes[i];
736
+ const bottom = boxes.reduce((max, b) => Math.max(max, TOP + (b.y + b.h - 1) * Y), top + 60);
737
+ parts.push(`<rect x="8" y="${top - 10}" width="${width - 16}" height="${bottom - top + 26}" rx="5" fill="#fafbfc"/>`);
738
+ parts.push(`<text x="16" y="${top + 5}" font-size="12" fill="#6a7687">${xml(fitWidth(band.name, Math.floor((width - 40) / X)))}</text>`);
739
+ });
740
+ if (rows.laneHeaderY !== void 0) map.lanes.forEach((lane, i) => {
741
+ const region = columns.lanes[i];
742
+ parts.push(`<text x="${(region.x + region.w / 2) * X}" y="${TOP + rows.laneHeaderY * Y + 5}" text-anchor="middle" font-size="12" fill="#566477">${xml(fitWidth(lane.label, region.w))}</text>`);
743
+ });
744
+ for (const edge of routing.edges) {
745
+ const points = edgePolyline(edge, rows).map(([x, y]) => `${x * X},${TOP + y * Y}`).join(" ");
746
+ parts.push(`<polyline points="${points}" fill="none" stroke="#8995a5" stroke-width="1.4" marker-end="url(#arrow)"/>`);
747
+ }
748
+ for (const box of rows.boxOf.values()) {
749
+ const node = originals.get(box.node.id);
750
+ const palette = neutral ? PALETTES.neutral : node.status === "done" && !isVerified(node) ? PALETTES.unverified : PALETTES[node.status];
751
+ const x = box.x * X, y = TOP + box.y * Y, w = (box.w - 1) * X, h = (box.h - 1) * Y;
752
+ const dash = !neutral && node.status === "planned" ? ' stroke-dasharray="5 4"' : "";
753
+ parts.push(`<g data-node="${xml(node.id)}"><title>${xml(node.label)}${neutral ? "" : ` \xB7 ${xml(statusText(node))}`}</title>`);
754
+ parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="5" fill="${palette[0]}" stroke="${palette[1]}" stroke-width="1.5"${dash}/>`);
755
+ parts.push(`<text x="${x + w / 2}" y="${y + 21}" text-anchor="middle" font-size="14" fill="${palette[2]}">${xml(box.label.trim())}</text>`);
756
+ const secondary = neutral ? node.kind ?? node.id : statusText(node);
757
+ parts.push(`<text x="${x + w / 2}" y="${y + 40}" text-anchor="middle" font-size="11" fill="${palette[2]}">${xml(fitWidth(secondary, box.w - 4))}</text></g>`);
758
+ }
759
+ parts.push("</g></svg>");
760
+ return parts.join("\n");
761
+ }
762
+
763
+ // src/web/scene.ts
764
+ var CARD_W = 248;
765
+ var CARD_H = 94;
766
+ var GAP = 36;
767
+ var ROW = 160;
768
+ var MARGIN = 32;
769
+ function layoutWebScene(source, overview = false) {
770
+ const map = overview ? aggregateMap(source) ?? source : source;
771
+ const oriented = flipForSequence(map);
772
+ const layers = [...oriented.layers].sort((a, b) => b.rank - a.rank);
773
+ const rows = layers.map((layer) => map.nodes.filter((node) => node.layer === layer.id));
774
+ const laneKeys = map.lanes.length ? [...map.lanes.map((lane) => lane.id), ""] : [];
775
+ const laneWidths = laneKeys.map((id) => Math.max(1, ...rows.map((row) => row.filter((n) => (n.lane ?? "") === id).length)) * (CARD_W + GAP));
776
+ if (laneKeys.length && !map.nodes.some((n) => n.lane === void 0)) {
777
+ laneKeys.pop();
778
+ laneWidths.pop();
779
+ }
780
+ const contentWidth = Math.max(568, laneKeys.length ? laneWidths.reduce((sum, w) => sum + w, 0) - GAP : Math.max(0, ...rows.map((row) => row.length)) * (CARD_W + GAP) - GAP);
781
+ const header = map.lanes.length ? 46 : 0;
782
+ const nodes = [];
783
+ rows.forEach((row, index) => {
784
+ if (!laneKeys.length) {
785
+ const start = MARGIN + (contentWidth - (row.length * (CARD_W + GAP) - GAP)) / 2;
786
+ row.forEach((node, i) => nodes.push({ node, x: start + i * (CARD_W + GAP), y: header + index * ROW + 48, w: CARD_W, h: CARD_H }));
787
+ } else {
788
+ let x = MARGIN;
789
+ laneKeys.forEach((lane, i) => {
790
+ row.filter((n) => (n.lane ?? "") === lane).forEach((node, j) => nodes.push({ node, x: x + j * (CARD_W + GAP), y: header + index * ROW + 48, w: CARD_W, h: CARD_H }));
791
+ x += laneWidths[i];
792
+ });
793
+ }
794
+ });
795
+ const nodeOf = new Map(nodes.map((node) => [node.node.id, node]));
796
+ const bandOf = new Map(layers.map((layer, i) => [layer.id, i]));
797
+ let skipCount = 0;
798
+ const routed = oriented.edges.map((edge) => {
799
+ const from = nodeOf.get(edge.from), to = nodeOf.get(edge.to);
800
+ const exits = oriented.edges.filter((e) => e.from === edge.from);
801
+ const entries = oriented.edges.filter((e) => e.to === edge.to);
802
+ const x1 = from.x + from.w * (exits.indexOf(edge) + 1) / (exits.length + 1);
803
+ const x2 = to.x + to.w * (entries.indexOf(edge) + 1) / (entries.length + 1);
804
+ const y1 = from.y + from.h, y2 = to.y, mid = (y1 + y2) / 2;
805
+ if (bandOf.get(to.node.layer) - bandOf.get(from.node.layer) > 1) {
806
+ const side = MARGIN + contentWidth + 24 + skipCount++ * 18;
807
+ return {
808
+ from: edge.from,
809
+ to: edge.to,
810
+ label: edge.label ?? "",
811
+ x: side,
812
+ y: mid,
813
+ path: `M ${x1} ${y1} V ${y1 + 18} Q ${x1} ${y1 + 26} ${x1 + 8} ${y1 + 26} H ${side - 8} Q ${side} ${y1 + 26} ${side} ${y1 + 34} V ${y2 - 28} Q ${side} ${y2 - 20} ${side - 8} ${y2 - 20} H ${x2 + 8} Q ${x2} ${y2 - 20} ${x2} ${y2 - 12} V ${y2}`
814
+ };
815
+ }
816
+ return {
817
+ from: edge.from,
818
+ to: edge.to,
819
+ label: edge.label ?? "",
820
+ x: (x1 + x2) / 2,
821
+ y: mid,
822
+ path: `M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`
823
+ };
824
+ });
825
+ const occupied = [];
826
+ const edges = routed.map((edge) => {
827
+ if (!edge.label) return edge;
828
+ const half = displayWidth(edge.label) * 2.8 + 6;
829
+ const offset = [0, 14, -14, 28, -28].find((offset2) => !occupied.some((label) => Math.abs(label.y - edge.y - offset2) < 12 && Math.abs(label.x - edge.x) < label.half + half)) ?? 0;
830
+ const positioned = { ...edge, y: edge.y + offset };
831
+ occupied.push({ x: edge.x, y: positioned.y, half });
832
+ return positioned;
833
+ });
834
+ let laneX = MARGIN;
835
+ const lanes = map.lanes.map((lane, i) => {
836
+ const value = { label: lane.label, x: laneX, width: laneWidths[i] - GAP };
837
+ laneX += laneWidths[i];
838
+ return value;
839
+ });
840
+ return {
841
+ width: contentWidth + MARGIN * 2 + (skipCount ? 24 + skipCount * 18 : 0),
842
+ height: Math.max(220, header + layers.length * ROW),
843
+ nodes,
844
+ edges,
845
+ lanes,
846
+ neutral: isNeutralKind(source),
847
+ bands: layers.map((layer, i) => ({ id: layer.id, name: layer.name, rank: source.layers.find((l) => l.id === layer.id).rank, y: header + i * ROW, count: rows[i].length }))
848
+ };
849
+ }
850
+
851
+ // src/web/view.ts
852
+ var STATUS_NAMES = { planned: "\u5F85\u5F00\u53D1", "in-progress": "\u8FDB\u884C\u4E2D", done: "\u5DF2\u9A8C\u8BC1", regressed: "\u6709\u56DE\u5F52", unverified: "\u5F85\u9A8C\u8BC1", neutral: "\u6A21\u5757" };
853
+ function stateOf(node, map) {
854
+ if (map.kind && map.kind !== "dev") return "neutral";
855
+ const group = map.groups.find((g) => g.id === node.id);
856
+ const verified = group ? map.nodes.filter((n) => n.group === group.id).every(isVerified) : isVerified(node);
857
+ return node.status === "done" && !verified ? "unverified" : node.status;
858
+ }
859
+ function renderStage(map, scene2) {
860
+ return `<svg class="map-scene" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" preserveAspectRatio="none" viewBox="0 0 ${scene2.width} ${scene2.height}" role="group" aria-label="\u77E2\u91CF\u4F9D\u8D56\u5730\u56FE"><defs><marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto"><path d="M0 0L8 4L0 8Z" fill="context-stroke"/></marker></defs>` + scene2.bands.map((band) => `<g class="band" transform="translate(0 ${band.y})"><line x2="${scene2.width}"/><rect class="band-rank-box" x="0" y="10" width="29" height="18" rx="3"/><text class="band-rank" x="14.5" y="23" text-anchor="middle">L${band.rank}</text><text x="40" y="24">${xml(band.name)}</text><text class="band-count" x="${scene2.width}" y="24" text-anchor="end">${band.count} \u4E2A\u6A21\u5757</text></g>`).join("") + scene2.lanes.map((lane) => `<text class="lane-label" x="${lane.x + lane.width / 2}" y="16" text-anchor="middle">${xml(lane.label)}</text>`).join("") + '<g class="wires" aria-hidden="true">' + scene2.edges.map((edge) => `<g class="edge" data-from="${xml(edge.from)}" data-to="${xml(edge.to)}"><path d="${edge.path}" marker-end="url(#arrow)"/>${edge.label ? `<text x="${edge.x}" y="${edge.y - 7}" text-anchor="middle">${xml(edge.label)}</text>` : ""}</g>`).join("") + "</g>" + scene2.nodes.map((box) => {
861
+ const n = box.node, status = stateOf(n, map), group = map.groups.find((g) => g.id === n.id);
862
+ const caption = group ? `${map.nodes.filter((m) => m.group === group.id).length} \u4E2A\u6A21\u5757` : n.id;
863
+ const badge = n.submap ? "\u5B50\u5730\u56FE \u2197" : n.kind ? kindGlyph(n.kind, true) ?? "" : "";
864
+ return `<g class="node ${status}" data-node="${xml(n.id)}" transform="translate(${box.x} ${box.y})" role="button" tabindex="0" aria-pressed="false" aria-label="${xml(n.label)} \xB7 ${STATUS_NAMES[status]}"><title>${xml(n.label)}${n.submap ? " \xB7 \u53CC\u51FB\u6253\u5F00\u5B50\u5730\u56FE" : ""}</title><rect class="node-card" width="${box.w}" height="${box.h}" rx="9"/><g class="node-head"><circle class="status-dot" cx="19" cy="19" r="3"/><text x="30" y="23">${STATUS_NAMES[status]}</text><text class="node-kind" x="${box.w - 15}" y="23" text-anchor="end">${xml(fitWidth(group ? "\u5206\u7EC4" : n.kind ?? "", 22))}</text></g><text class="node-title" x="15" y="51">${xml(fitWidth(n.label, Math.floor((box.w - 30) / 8.5)))}</text><text class="node-foot" x="15" y="78">${xml(fitWidth(caption, badge ? 28 : 40))}</text>${badge ? `<text class="submap-badge" x="${box.w - 15}" y="78" text-anchor="end">${xml(badge)}</text>` : ""}</g>`;
865
+ }).join("") + "</svg>";
866
+ }
867
+ function renderInspector(map, id) {
868
+ const info = id ? focusInfo(map, id) : void 0;
869
+ const neutral = map.kind !== void 0 && map.kind !== "dev";
870
+ if (!info) {
871
+ const verified = map.nodes.filter(isVerified).length;
872
+ return `<div class="inspector-heading"><div><span class="eyebrow">${neutral ? "MAP OVERVIEW" : "BUILD OVERVIEW"}</span><h2>\u5730\u56FE\u6982\u89C8</h2></div><span class="quiet">\u70B9\u51FB\u8282\u70B9\u56FA\u5B9A\u8BE6\u60C5</span></div><div class="overview-grid"><div><strong>${map.nodes.length}</strong><span>\u6A21\u5757</span></div><div><strong>${map.edges.length}</strong><span>\u4F9D\u8D56\u5173\u7CFB</span></div><div><strong>${map.layers.length}</strong><span>\u5C42\u7EA7</span></div><div><strong>${neutral ? map.groups.length : `${verified}/${map.nodes.length}`}</strong><span>${neutral ? "\u5206\u7EC4" : "\u5DF2\u9A8C\u8BC1"}</span></div></div><p class="inspector-hint">${neutral ? "\u9009\u62E9\u4E00\u4E2A\u6A21\u5757\uFF0C\u67E5\u770B\u5B83\u7684\u804C\u8D23\u4E0E\u4E0A\u4E0B\u6E38\u5173\u7CFB\u3002" : map.nodes.some((n) => n.status === "in-progress") ? `\u6B63\u5728\u6784\u5EFA\uFF1A${map.nodes.filter((n) => n.status === "in-progress").map((n) => xml(n.label)).join("\u3001")}` : "\u6A21\u5757\u72B6\u6001\u6765\u81EA\u9879\u76EE\u5730\u56FE\uFF1B\u9A8C\u8BC1\u8BB0\u5F55\u4F1A\u4FDD\u7559\u5728\u8282\u70B9\u8BE6\u60C5\u4E2D\u3002"}</p>`;
873
+ }
874
+ const node = info.kind === "node" ? info.node : void 0;
875
+ const title = node?.label ?? (info.kind === "group" ? info.group.label : "");
876
+ const neighbors = (label, refs) => `<div class="relations"><span class="eyebrow">${label} \xB7 ${refs.length}</span><div>${refs.length ? refs.map((ref) => `<button class="relation" data-select="${xml(ref.id)}">${xml(ref.label)}${ref.edgeLabel ? `<small>${xml(ref.edgeLabel)}</small>` : ""}<span>\u2197</span></button>`).join("") : '<span class="quiet">\u65E0</span>'}</div></div>`;
877
+ return `<div class="inspector-heading"><div><span class="eyebrow">${xml(info.layerName)}${info.kind === "node" && info.laneLabel ? ` / ${xml(info.laneLabel)}` : ""}</span><h2>${xml(title)}</h2></div><button class="icon-button" data-clear aria-label="\u53D6\u6D88\u9009\u62E9">\xD7</button></div>` + (node ? `<p class="node-description">${xml(node.detail ?? "\u5C1A\u672A\u586B\u5199\u6A21\u5757\u8BF4\u660E\u3002")}</p>${node.evidence ? `<div class="evidence ${stateOf(node, map)}"><span>${node.status === "regressed" ? "\u56DE\u5F52\u8BB0\u5F55" : "\u9A8C\u8BC1\u8BB0\u5F55"}</span><p>${xml(node.evidence)}</p></div>` : ""}${node.submap ? `<button class="dive-button" data-dive="${xml(node.submap)}">\u6253\u5F00\u5B50\u5730\u56FE <span>\u2197</span></button>` : ""}` : `<div class="group-members">${info.kind === "group" ? info.members.map((member) => `<button class="relation" data-select="${xml(member.id)}">${xml(member.label)}<small>${STATUS_NAMES[stateOf(member, map)]}</small></button>`).join("") : ""}</div>`) + `<div class="neighbor-grid">${neighbors(map.kind === "sequence" ? "\u6B64\u524D\u4E8B\u4EF6" : "\u4F9D\u8D56\u4E8E", info.uses)}${neighbors(map.kind === "sequence" ? "\u540E\u7EED\u4E8B\u4EF6" : "\u88AB\u4F9D\u8D56", info.usedBy)}</div>`;
878
+ }
879
+
880
+ // src/web/viewport.ts
881
+ function svgViewBox(view2, width, height) {
882
+ return `${-view2.x / view2.scale} ${-view2.y / view2.scale} ${Math.max(1, width) / view2.scale} ${Math.max(1, height) / view2.scale}`;
883
+ }
884
+ var clampScale = (scale) => Math.max(0.15, Math.min(2.5, scale));
885
+ function fitViewport(width, height, sceneWidth, sceneHeight) {
886
+ const scale = Math.max(0.15, Math.min(1, (width - 48) / sceneWidth, (height - 32) / sceneHeight));
887
+ return { scale, x: (width - sceneWidth * scale) / 2, y: Math.max(16, (height - sceneHeight * scale) / 2) };
888
+ }
889
+ function zoomViewport(view2, scale, x, y) {
890
+ const next = clampScale(scale), ratio = next / view2.scale;
891
+ return { scale: next, x: x - (x - view2.x) * ratio, y: y - (y - view2.y) * ratio };
892
+ }
893
+
894
+ // src/web/zoom.ts
895
+ function showsOverview(state, hasGroups) {
896
+ return hasGroups && (state.overview || state.viewport.scale < 0.55);
897
+ }
898
+ function zoomScene(state, factor, x, y) {
899
+ return {
900
+ viewport: zoomViewport(state.viewport, state.viewport.scale * factor, x, y),
901
+ // Overview and fit can aggregate a scene explicitly. Moving closer resumes
902
+ // scale-driven detail instead of leaving that earlier choice latched on.
903
+ overview: state.overview && factor <= 1
904
+ };
905
+ }
906
+
907
+ // src/web/app.ts
908
+ var element = (id) => document.getElementById(id);
909
+ var viewport = element("viewport");
910
+ var stage = element("stage");
911
+ var inspector = element("inspector");
912
+ var pages = element("pages");
913
+ var search = element("search");
914
+ var dialog = element("dialog");
915
+ var base = new URL(".", window.location.href);
916
+ var kinds = { dev: "\u5F00\u53D1\u5730\u56FE", architecture: "\u67B6\u6784\u5730\u56FE", dataflow: "\u6570\u636E\u6D41", "behavior-tree": "\u884C\u4E3A\u6811", sequence: "\u65F6\u5E8F\u5730\u56FE" };
917
+ var snapshot = { project: "", pages: [] };
918
+ var revision = "";
919
+ var etag = "";
920
+ var currentId = new URLSearchParams(location.search).get("page") ?? "";
921
+ var follow = true;
922
+ var hover;
923
+ var statusFilter = "all";
924
+ var scene;
925
+ var renderedOverview = false;
926
+ var parents = [];
927
+ var toastTimer;
928
+ var deletion;
929
+ var views = /* @__PURE__ */ new Map();
930
+ function view() {
931
+ let value = views.get(currentId);
932
+ if (!value) {
933
+ value = { viewport: { x: 0, y: 0, scale: 1 }, selected: void 0, overview: false, fitted: false };
934
+ views.set(currentId, value);
935
+ }
936
+ return value;
937
+ }
938
+ var current = () => snapshot.pages.find((page) => page.id === currentId);
939
+ function toast(message) {
940
+ element("toast").textContent = message;
941
+ element("toast").hidden = false;
942
+ clearTimeout(toastTimer);
943
+ toastTimer = setTimeout(() => {
944
+ element("toast").hidden = true;
945
+ }, 4500);
946
+ }
947
+ function setFollow(enabled) {
948
+ follow = enabled;
949
+ element("follow").classList.toggle("active", enabled);
950
+ element("follow").setAttribute("aria-pressed", String(enabled));
951
+ element("follow").textContent = enabled ? "\u25CF \u81EA\u52A8\u8DDF\u968F" : "\u2316 \u5F53\u524D\u9875\u5DF2\u56FA\u5B9A";
952
+ }
953
+ function switchPage(id, manual = true, dive = false) {
954
+ if (!snapshot.pages.some((page) => page.id === id)) {
955
+ toast("\u8FD9\u5F20\u5B50\u5730\u56FE\u5C1A\u672A\u521B\u5EFA\uFF0C\u6216\u5DF2\u88AB\u5220\u9664\u3002");
956
+ return;
957
+ }
958
+ if (manual) setFollow(false);
959
+ if (dive) parents.push(currentId);
960
+ else parents = [];
961
+ currentId = id;
962
+ hover = void 0;
963
+ search.value = "";
964
+ statusFilter = "all";
965
+ const url = new URL(location.href);
966
+ url.searchParams.set("page", id);
967
+ history.replaceState(null, "", url);
968
+ render();
969
+ }
970
+ function goBack() {
971
+ const parent = parents.pop();
972
+ if (parent === void 0) return;
973
+ if (!snapshot.pages.some((page) => page.id === parent)) {
974
+ toast("\u7236\u5730\u56FE\u5DF2\u88AB\u5220\u9664\u3002");
975
+ return;
976
+ }
977
+ const stack = [...parents];
978
+ switchPage(parent);
979
+ parents = stack;
980
+ renderHeading();
981
+ }
982
+ function renderHeading() {
983
+ const page = current(), map = page?.map;
984
+ const terminalUrl = new URL(base);
985
+ terminalUrl.searchParams.set("view", "terminal");
986
+ if (currentId && currentId !== "_default") terminalUrl.searchParams.set("page", currentId);
987
+ element("terminal-mode").href = terminalUrl.href;
988
+ element("project").textContent = snapshot.project;
989
+ element("title").textContent = page?.title ?? "\u5730\u56FE\u5DF2\u4E0D\u5B58\u5728";
990
+ document.title = `${page?.title ?? "\u5730\u56FE"} \xB7 Mellos`;
991
+ element("kind").textContent = kinds[map?.kind ?? "dev"];
992
+ pages.innerHTML = snapshot.pages.map((p) => `<option value="${xml(p.id)}">${xml(p.title)}</option>`).join("");
993
+ pages.value = currentId;
994
+ const neutral = !!map?.kind && map.kind !== "dev", verified = map?.nodes.filter(isVerified).length ?? 0, total = map?.nodes.length ?? 0;
995
+ element("summary").innerHTML = neutral ? `<b>${total}</b> \u4E2A\u6A21\u5757 \xB7 <b>${map?.edges.length ?? 0}</b> \u6761\u5173\u7CFB` : `<b>${verified} / ${total}</b> \u5DF2\u9A8C\u8BC1 \xB7 <b>${map?.nodes.filter((n) => n.status === "in-progress").length ?? 0}</b> \u8FDB\u884C\u4E2D`;
996
+ element("progress").hidden = neutral;
997
+ element("progress").firstElementChild.style.width = `${total ? verified / total * 100 : 0}%`;
998
+ element("filters").hidden = neutral;
999
+ element("breadcrumbs").innerHTML = parents.map((id, i) => `<button data-parent="${i}">\u2190 ${xml(snapshot.pages.find((p) => p.id === id)?.title ?? id)}</button><span>\u203A</span>`).join("") + (parents.length ? `<span>${xml(page?.title ?? "")}</span>` : "");
1000
+ element("export").disabled = !map;
1001
+ element("delete").disabled = !page || !page.modified && !page.error;
1002
+ }
1003
+ function render() {
1004
+ renderHeading();
1005
+ const page = current(), map = page?.map;
1006
+ scene = void 0;
1007
+ stage.innerHTML = "";
1008
+ const empty = element("empty");
1009
+ empty.hidden = !!map?.nodes.length;
1010
+ empty.textContent = page?.error ? `\u5730\u56FE\u8BFB\u53D6\u5931\u8D25
1011
+ ${page.error}
1012
+ \u4FEE\u590D\u6E90\u6587\u4EF6\u540E\u4F1A\u81EA\u52A8\u6062\u590D\u3002` : !page ? "\u5F53\u524D\u5730\u56FE\u5DF2\u88AB\u5220\u9664\u3002\n\u4ECE\u53F3\u4E0A\u89D2\u9009\u62E9\u5176\u4ED6\u5730\u56FE\u3002" : "\u5730\u56FE\u5DF2\u8FDE\u63A5\n\u58F0\u660E\u6A21\u5757\u540E\uFF0C\u5B83\u4EEC\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\u3002";
1013
+ if (!map) {
1014
+ inspector.innerHTML = '<p class="quiet">\u6B64\u9875\u9762\u6682\u65E0\u53EF\u663E\u793A\u7684\u5730\u56FE\u6570\u636E\u3002</p>';
1015
+ element("minimap").innerHTML = "";
1016
+ return;
1017
+ }
1018
+ const state = view();
1019
+ if (state.selected && !focusInfo(map, state.selected)) state.selected = void 0;
1020
+ if (hover && !focusInfo(map, hover)) hover = void 0;
1021
+ renderedOverview = showsOverview(state, map.groups.length > 0);
1022
+ scene = layoutWebScene(map, renderedOverview);
1023
+ stage.innerHTML = renderStage(map, scene);
1024
+ if (!state.fitted) {
1025
+ const fitted = fitViewport(viewport.clientWidth, viewport.clientHeight - 45, scene.width, scene.height);
1026
+ state.viewport = fitted.scale >= 0.85 ? fitted : { scale: 0.85, x: (viewport.clientWidth - scene.width * 0.85) / 2, y: 18 };
1027
+ state.fitted = true;
1028
+ }
1029
+ element("overview").setAttribute("aria-pressed", String(renderedOverview));
1030
+ element("overview").disabled = map.groups.length === 0;
1031
+ element("overview").title = map.groups.length ? "\u5C06\u540C\u7EC4\u6A21\u5757\u805A\u5408\u663E\u793A" : "\u8FD9\u5F20\u5730\u56FE\u5C1A\u672A\u58F0\u660E\u5206\u7EC4";
1032
+ transform();
1033
+ updateFocus();
1034
+ }
1035
+ function transform() {
1036
+ const v = view().viewport;
1037
+ stage.querySelector(".map-scene")?.setAttribute("viewBox", svgViewBox(v, viewport.clientWidth, viewport.clientHeight));
1038
+ element("zoom-label").textContent = `${Math.round(v.scale * 100)}%`;
1039
+ viewport.style.backgroundPosition = `${v.x}px ${v.y}px`;
1040
+ viewport.style.backgroundSize = `${20 * v.scale}px ${20 * v.scale}px`;
1041
+ if (scene) element("minimap").innerHTML = `<svg viewBox="0 0 ${scene.width} ${scene.height}" aria-label="\u5730\u56FE\u7F29\u7565\u56FE">${scene.nodes.map((n) => `<rect class="mini-node" x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="10"/>`).join("")}<rect class="mini-window" x="${-v.x / v.scale}" y="${-v.y / v.scale}" width="${viewport.clientWidth / v.scale}" height="${viewport.clientHeight / v.scale}"/></svg>`;
1042
+ }
1043
+ function updateFocus() {
1044
+ const map = current()?.map;
1045
+ if (!map) return;
1046
+ const focus = view().selected ?? hover, query = search.value.trim().toLocaleLowerCase();
1047
+ const related = new Set(focus ? [focus] : []);
1048
+ for (const edge of scene?.edges ?? []) {
1049
+ if (edge.from === focus) related.add(edge.to);
1050
+ if (edge.to === focus) related.add(edge.from);
1051
+ }
1052
+ const visible = /* @__PURE__ */ new Set();
1053
+ for (const item of scene?.nodes ?? []) {
1054
+ const n = item.node;
1055
+ const matchesQuery = !query || [n.label, n.id, n.detail, n.evidence].some((text) => text?.toLocaleLowerCase().includes(query)) || map.nodes.some((member) => member.group === n.id && [member.label, member.detail].some((t) => t?.toLocaleLowerCase().includes(query)));
1056
+ if (matchesQuery && (statusFilter === "all" || stateOf(n, map) === statusFilter)) visible.add(n.id);
1057
+ }
1058
+ stage.querySelectorAll("[data-node]").forEach((button) => {
1059
+ const id = button.dataset.node;
1060
+ button.classList.toggle("focus", id === focus);
1061
+ button.classList.toggle("muted-node", !visible.has(id) || !!focus && !related.has(id));
1062
+ button.setAttribute("aria-pressed", String(id === view().selected));
1063
+ });
1064
+ stage.querySelectorAll(".edge").forEach((edge) => {
1065
+ const from = edge.dataset.from, to = edge.dataset.to;
1066
+ const focused = from === focus || to === focus;
1067
+ edge.classList.toggle("focus", focused);
1068
+ edge.classList.toggle("muted-edge", !visible.has(from) || !visible.has(to) || !!focus && !focused);
1069
+ });
1070
+ element("filters").querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.status === statusFilter));
1071
+ element("match-count").textContent = query || statusFilter !== "all" ? `${visible.size} \u4E2A\u5339\u914D\u6A21\u5757` : "";
1072
+ const content = renderInspector(map, focus);
1073
+ if (inspector.innerHTML !== content) inspector.innerHTML = content;
1074
+ }
1075
+ function select(id) {
1076
+ view().selected = id;
1077
+ hover = void 0;
1078
+ if (id && scene && !scene.nodes.some((n) => n.node.id === id)) {
1079
+ view().overview = false;
1080
+ view().viewport = { ...view().viewport, scale: Math.max(0.65, view().viewport.scale) };
1081
+ render();
1082
+ }
1083
+ const box = id ? scene?.nodes.find((n) => n.node.id === id) : void 0;
1084
+ if (box) {
1085
+ const v = view().viewport, x = box.x * v.scale + v.x, y = box.y * v.scale + v.y;
1086
+ if (x < 0 || y < 0 || x + box.w * v.scale > viewport.clientWidth || y + box.h * v.scale > viewport.clientHeight) {
1087
+ view().viewport = { ...v, x: viewport.clientWidth / 2 - (box.x + box.w / 2) * v.scale, y: viewport.clientHeight / 2 - (box.y + box.h / 2) * v.scale };
1088
+ transform();
1089
+ }
1090
+ }
1091
+ updateFocus();
1092
+ }
1093
+ function fit() {
1094
+ if (!scene) return;
1095
+ let fitted = fitViewport(viewport.clientWidth, viewport.clientHeight - 45, scene.width, scene.height);
1096
+ if (fitted.scale < 0.55 && !renderedOverview && current()?.map?.groups.length) {
1097
+ view().overview = true;
1098
+ render();
1099
+ fitted = fitViewport(viewport.clientWidth, viewport.clientHeight - 45, scene.width, scene.height);
1100
+ }
1101
+ view().viewport = fitted;
1102
+ transform();
1103
+ }
1104
+ function zoom(factor, x = viewport.clientWidth / 2, y = viewport.clientHeight / 2) {
1105
+ Object.assign(view(), zoomScene(view(), factor, x, y));
1106
+ if (showsOverview(view(), !!current()?.map?.groups.length) !== renderedOverview) render();
1107
+ else transform();
1108
+ }
1109
+ pages.addEventListener("change", () => switchPage(pages.value));
1110
+ element("follow").addEventListener("click", () => setFollow(!follow));
1111
+ element("theme").addEventListener("click", () => {
1112
+ document.documentElement.dataset.theme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
1113
+ });
1114
+ search.addEventListener("input", () => {
1115
+ view().selected = void 0;
1116
+ updateFocus();
1117
+ });
1118
+ search.addEventListener("keydown", (event) => {
1119
+ if (event.key === "Enter") {
1120
+ const first = stage.querySelector(".node:not(.muted-node)");
1121
+ if (first?.dataset.node) select(first.dataset.node);
1122
+ }
1123
+ });
1124
+ element("filters").addEventListener("click", (event) => {
1125
+ const button = event.target.closest("[data-status]");
1126
+ if (button) {
1127
+ statusFilter = button.dataset.status;
1128
+ view().selected = void 0;
1129
+ updateFocus();
1130
+ }
1131
+ });
1132
+ element("overview").addEventListener("click", () => {
1133
+ view().overview = !renderedOverview;
1134
+ if (!view().overview) view().viewport = { ...view().viewport, scale: Math.max(0.65, view().viewport.scale) };
1135
+ render();
1136
+ if (view().overview) fit();
1137
+ });
1138
+ element("fit").addEventListener("click", fit);
1139
+ element("zoom-in").addEventListener("click", () => zoom(1.2));
1140
+ element("zoom-out").addEventListener("click", () => zoom(1 / 1.2));
1141
+ stage.addEventListener("click", (event) => {
1142
+ const node = event.target.closest("[data-node]");
1143
+ if (node) select(view().selected === node.dataset.node ? void 0 : node.dataset.node);
1144
+ });
1145
+ stage.addEventListener("keydown", (event) => {
1146
+ const node = event.target.closest("[data-node]");
1147
+ if (node && (event.key === "Enter" || event.key === " ")) {
1148
+ event.preventDefault();
1149
+ select(view().selected === node.dataset.node ? void 0 : node.dataset.node);
1150
+ }
1151
+ });
1152
+ stage.addEventListener("dblclick", (event) => {
1153
+ const node = event.target.closest("[data-node]");
1154
+ const target = current()?.map?.nodes.find((n) => n.id === node?.dataset.node)?.submap;
1155
+ if (target) switchPage(target, true, true);
1156
+ });
1157
+ stage.addEventListener("pointerover", (event) => {
1158
+ const node = event.target.closest("[data-node]");
1159
+ if (node && hover !== node.dataset.node) {
1160
+ hover = node.dataset.node;
1161
+ updateFocus();
1162
+ }
1163
+ });
1164
+ stage.addEventListener("pointerout", (event) => {
1165
+ if (!event.relatedTarget?.closest?.("[data-node]")) {
1166
+ hover = void 0;
1167
+ updateFocus();
1168
+ }
1169
+ });
1170
+ inspector.addEventListener("click", (event) => {
1171
+ const target = event.target.closest("button");
1172
+ if (target?.hasAttribute("data-clear")) select(void 0);
1173
+ if (target?.dataset.select) select(target.dataset.select);
1174
+ if (target?.dataset.dive) switchPage(target.dataset.dive, true, true);
1175
+ });
1176
+ element("breadcrumbs").addEventListener("click", (event) => {
1177
+ const target = event.target.closest("[data-parent]");
1178
+ if (!target) return;
1179
+ const index = Number(target.dataset.parent), id = parents[index], stack = parents.slice(0, index);
1180
+ if (id !== void 0) {
1181
+ switchPage(id);
1182
+ parents = stack;
1183
+ renderHeading();
1184
+ }
1185
+ });
1186
+ viewport.addEventListener("wheel", (event) => {
1187
+ event.preventDefault();
1188
+ const bounds = viewport.getBoundingClientRect();
1189
+ zoom(Math.exp(-Math.max(-100, Math.min(100, event.deltaY)) * 3e-3), event.clientX - bounds.left, event.clientY - bounds.top);
1190
+ }, { passive: false });
1191
+ var drag;
1192
+ viewport.addEventListener("pointerdown", (event) => {
1193
+ if (event.button !== 0 || event.target.closest("[data-node]")) return;
1194
+ drag = { x: event.clientX, y: event.clientY, origin: view().viewport, moved: false };
1195
+ viewport.setPointerCapture(event.pointerId);
1196
+ viewport.classList.add("dragging");
1197
+ });
1198
+ viewport.addEventListener("pointermove", (event) => {
1199
+ if (!drag) return;
1200
+ const dx = event.clientX - drag.x, dy = event.clientY - drag.y;
1201
+ drag.moved ||= Math.abs(dx) + Math.abs(dy) > 4;
1202
+ view().viewport = { ...drag.origin, x: drag.origin.x + dx, y: drag.origin.y + dy };
1203
+ transform();
1204
+ });
1205
+ viewport.addEventListener("pointerup", () => {
1206
+ if (drag && !drag.moved) select(void 0);
1207
+ drag = void 0;
1208
+ viewport.classList.remove("dragging");
1209
+ });
1210
+ viewport.addEventListener("pointercancel", () => {
1211
+ drag = void 0;
1212
+ viewport.classList.remove("dragging");
1213
+ });
1214
+ element("minimap").addEventListener("click", (event) => {
1215
+ if (!scene) return;
1216
+ const svg = element("minimap").querySelector("svg"), rect = svg.getBoundingClientRect();
1217
+ const x = (event.clientX - rect.left) / rect.width * scene.width, y = (event.clientY - rect.top) / rect.height * scene.height;
1218
+ view().viewport = { ...view().viewport, x: viewport.clientWidth / 2 - x * view().viewport.scale, y: viewport.clientHeight / 2 - y * view().viewport.scale };
1219
+ transform();
1220
+ });
1221
+ var dividerDrag;
1222
+ var divider = element("divider");
1223
+ function resizeInspector(height) {
1224
+ const value = Math.max(100, Math.min(innerHeight * 0.5, height));
1225
+ document.documentElement.style.setProperty("--detail-height", `${value}px`);
1226
+ divider.setAttribute("aria-valuenow", String(Math.round(value)));
1227
+ }
1228
+ divider.addEventListener("pointerdown", (event) => {
1229
+ dividerDrag = { y: event.clientY, height: inspector.clientHeight };
1230
+ divider.setPointerCapture(event.pointerId);
1231
+ });
1232
+ divider.addEventListener("pointermove", (event) => {
1233
+ if (dividerDrag) resizeInspector(dividerDrag.height + dividerDrag.y - event.clientY);
1234
+ });
1235
+ divider.addEventListener("pointerup", () => {
1236
+ dividerDrag = void 0;
1237
+ });
1238
+ divider.addEventListener("keydown", (event) => {
1239
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
1240
+ event.preventDefault();
1241
+ event.stopPropagation();
1242
+ resizeInspector(inspector.clientHeight + (event.key === "ArrowUp" ? 20 : -20));
1243
+ }
1244
+ });
1245
+ new ResizeObserver(() => {
1246
+ if (scene) transform();
1247
+ }).observe(viewport);
1248
+ element("export").addEventListener("click", () => {
1249
+ const map = current()?.map;
1250
+ if (!map) return;
1251
+ const url = URL.createObjectURL(new Blob([renderMapSvg(map)], { type: "image/svg+xml" }));
1252
+ const link = document.createElement("a");
1253
+ link.href = url;
1254
+ link.download = `${currentId || "mellos-map"}.svg`;
1255
+ link.click();
1256
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
1257
+ });
1258
+ element("help").addEventListener("click", () => {
1259
+ deletion = void 0;
1260
+ element("dialog-confirm").hidden = true;
1261
+ element("dialog-title").textContent = "\u4F7F\u7528\u5730\u56FE";
1262
+ element("dialog-body").textContent = "\u6EDA\u8F6E / + \u2212\uFF1A\u7F29\u653E\uFF0C\u4F4E\u4E8E 55% \u65F6\u805A\u5408\u5206\u7EC4\n\u62D6\u52A8\u753B\u5E03\uFF1A\u5E73\u79FB \xB7 0\uFF1A\u9002\u5E94\u7A97\u53E3\n\u60AC\u505C\uFF1A\u9884\u89C8\u5173\u7CFB \xB7 \u70B9\u51FB\uFF1A\u56FA\u5B9A\u8BE6\u60C5\n\u53CC\u51FB\u5B50\u5730\u56FE\u8282\u70B9\uFF1A\u4E0B\u6F5C \xB7 Backspace\uFF1A\u8FD4\u56DE\n/\uFF1A\u641C\u7D22 \xB7 F\uFF1A\u5F00\u5173\u81EA\u52A8\u8DDF\u968F\n\u65B9\u5411\u952E\uFF1A\u79FB\u52A8\u89C6\u53E3 \xB7 Esc\uFF1A\u53D6\u6D88\u9009\u62E9\n\u62D6\u52A8\u8BE6\u60C5\u4E0A\u65B9\u7684\u5206\u9694\u6761\uFF1A\u8C03\u6574\u9762\u677F\u9AD8\u5EA6\n\n\u5730\u56FE\u53D8\u66F4\u4F1A\u81EA\u52A8\u540C\u6B65\u3002\u624B\u52A8\u5207\u9875\u4F1A\u56FA\u5B9A\u5F53\u524D\u9875\uFF1B\u5F00\u542F\u81EA\u52A8\u8DDF\u968F\u540E\uFF0C\u4F1A\u8DDF\u968F\u4E0B\u4E00\u6B21\u5730\u56FE\u66F4\u65B0\u3002";
1263
+ dialog.showModal();
1264
+ });
1265
+ element("delete").addEventListener("click", () => {
1266
+ const page = current();
1267
+ if (!page) return;
1268
+ deletion = { page, revision };
1269
+ element("dialog-title").textContent = "\u5220\u9664\u8FD9\u5F20\u5730\u56FE\uFF1F";
1270
+ element("dialog-body").textContent = `\u5C06\u5220\u9664\u300C${page.title}\u300D\u7684\u5730\u56FE\u6587\u4EF6\u3002\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\uFF1B\u5176\u4ED6\u5730\u56FE\u4E2D\u7684\u5B50\u5730\u56FE\u94FE\u63A5\u4F1A\u4FDD\u7559\u3002`;
1271
+ element("dialog-confirm").hidden = false;
1272
+ dialog.showModal();
1273
+ });
1274
+ element("dialog-cancel").addEventListener("click", () => dialog.close());
1275
+ element("dialog-confirm").addEventListener("click", async () => {
1276
+ if (!deletion) return;
1277
+ const confirmed = deletion;
1278
+ deletion = void 0;
1279
+ dialog.close();
1280
+ try {
1281
+ const response = await fetch(new URL(`api/pages/${confirmed.page.id || "_default"}`, base), { method: "DELETE", headers: { "If-Match": `"${confirmed.revision}"` }, signal: AbortSignal.timeout(5e3) });
1282
+ const result = await response.json();
1283
+ if (!response.ok) throw new Error(result.error ?? "\u5220\u9664\u5931\u8D25");
1284
+ toast(result.warning ?? "\u5730\u56FE\u5DF2\u5220\u9664\u3002");
1285
+ etag = "";
1286
+ } catch (error) {
1287
+ toast(String(error));
1288
+ }
1289
+ });
1290
+ document.addEventListener("keydown", (event) => {
1291
+ if (dialog.open || event.target.matches("input,select,textarea")) return;
1292
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
1293
+ if (event.key === "/") {
1294
+ event.preventDefault();
1295
+ search.focus();
1296
+ } else if (event.key === "+" || event.key === "=") {
1297
+ event.preventDefault();
1298
+ zoom(1.2);
1299
+ } else if (event.key === "-") {
1300
+ event.preventDefault();
1301
+ zoom(1 / 1.2);
1302
+ } else if (event.key === "0") fit();
1303
+ else if (event.key.toLowerCase() === "f") setFollow(!follow);
1304
+ else if (event.key === "Escape") {
1305
+ if (view().selected) select(void 0);
1306
+ else goBack();
1307
+ } else if (event.key === "Backspace") {
1308
+ event.preventDefault();
1309
+ goBack();
1310
+ } else if (event.key.startsWith("Arrow")) {
1311
+ event.preventDefault();
1312
+ const v = view().viewport;
1313
+ view().viewport = { ...v, x: v.x + (event.key === "ArrowLeft" ? 40 : event.key === "ArrowRight" ? -40 : 0), y: v.y + (event.key === "ArrowUp" ? 40 : event.key === "ArrowDown" ? -40 : 0) };
1314
+ transform();
1315
+ }
1316
+ });
1317
+ async function poll() {
1318
+ try {
1319
+ const response = await fetch(new URL("api/state", base), { headers: etag ? { "If-None-Match": etag } : {}, signal: AbortSignal.timeout(5e3) });
1320
+ if (response.status !== 304 && !response.ok) throw new Error(`HTTP ${response.status}`);
1321
+ element("connection").classList.remove("offline");
1322
+ element("connection").innerHTML = "<i></i>\u5B9E\u65F6\u540C\u6B65";
1323
+ if (response.status !== 304) {
1324
+ const result = await response.json();
1325
+ etag = response.headers.get("ETag") ?? "";
1326
+ if (result.revision !== revision) {
1327
+ const initial = !revision, previous = snapshot, priorPage = current(), priorId = currentId;
1328
+ revision = result.revision;
1329
+ snapshot = result.value;
1330
+ if (initial && !new URLSearchParams(location.search).has("page")) currentId = mostRecentKey(snapshot.pages.map((p) => p.id), (id) => snapshot.pages.find((p) => p.id === id)?.modified) ?? "";
1331
+ const changed = snapshot.pages.filter((p) => p.modified > (previous.pages.find((old) => old.id === p.id)?.modified ?? 0));
1332
+ if (!initial && follow && !parents.length && changed.length) currentId = mostRecentKey(changed.map((p) => p.id), (id) => changed.find((p) => p.id === id)?.modified) ?? currentId;
1333
+ if (priorId !== currentId) {
1334
+ hover = void 0;
1335
+ search.value = "";
1336
+ statusFilter = "all";
1337
+ const url = new URL(location.href);
1338
+ url.searchParams.set("page", currentId);
1339
+ history.replaceState(null, "", url);
1340
+ }
1341
+ if (initial || JSON.stringify(priorPage) !== JSON.stringify(current())) render();
1342
+ else renderHeading();
1343
+ element("updated").textContent = `\u5DF2\u540C\u6B65 ${(/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })}`;
1344
+ }
1345
+ }
1346
+ } catch {
1347
+ element("connection").classList.add("offline");
1348
+ element("connection").innerHTML = "<i></i>\u8FDE\u63A5\u5DF2\u65AD\u5F00";
1349
+ element("updated").textContent = "\u6B63\u5728\u91CD\u8FDE \xB7 \u663E\u793A\u4E0A\u6B21\u6570\u636E";
1350
+ } finally {
1351
+ setTimeout(() => {
1352
+ void poll();
1353
+ }, 800);
1354
+ }
1355
+ }
1356
+ void poll();