antri_cli 1.57.6 → 1.57.7

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.
@@ -45,23 +45,34 @@ export class ArtifactManager {
45
45
  saveArtifact(artifact) {
46
46
  this.ensureDirectory();
47
47
  this.artifacts.set(artifact.id, artifact);
48
- // Also write a standalone file for easy browser viewing
48
+ // Also write a standalone HTML file for instant browser viewing
49
49
  try {
50
- const ext = artifact.type === 'html' ? 'html' : 'txt';
51
- const filePath = path.join(this.baseDir, `${artifact.id}.${ext}`);
52
- if (artifact.type === 'html') {
53
- fs.writeFileSync(filePath, artifact.content, 'utf-8');
50
+ const fullHtml = this.getArtifactHtml(artifact);
51
+ const filePath = path.join(this.baseDir, `${artifact.id}.html`);
52
+ fs.writeFileSync(filePath, fullHtml, 'utf-8');
53
+ }
54
+ catch { }
55
+ this.persistIndex();
56
+ return artifact;
57
+ }
58
+ getArtifactHtml(artifact) {
59
+ if (artifact.type === 'html') {
60
+ let content = artifact.content.trim();
61
+ if (!content.toLowerCase().includes('<!doctype html>') && !content.toLowerCase().includes('<html')) {
62
+ content = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${artifact.title}</title><style>body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#0f172a;color:#f8fafc;padding:16px;}</style></head><body>${content}</body></html>`;
54
63
  }
55
- else if (artifact.type === 'mindmap') {
56
- const htmlMindmap = `<!DOCTYPE html>
64
+ return content;
65
+ }
66
+ if (artifact.type === 'mindmap') {
67
+ return `<!DOCTYPE html>
57
68
  <html lang="en">
58
69
  <head>
59
70
  <meta charset="UTF-8">
60
71
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
61
72
  <title>${artifact.title}</title>
62
- <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
63
- <script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17.2"></script>
64
- <script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17.2"></script>
73
+ <script src="https://cdn.jsdelivr.net/npm/d3@7.8.5/dist/d3.min.js"></script>
74
+ <script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17.2/dist/browser/index.js"></script>
75
+ <script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17.2/dist/browser/index.js"></script>
65
76
  <style>
66
77
  * { box-sizing: border-box; margin: 0; padding: 0; }
67
78
  :root {
@@ -81,6 +92,8 @@ export class ArtifactManager {
81
92
  --root-bg: #4f46e5;
82
93
  --root-border: #4338ca;
83
94
  --root-text: #ffffff;
95
+ --pillar-bg: #f1f5f9;
96
+ --pillar-border: #94a3b8;
84
97
  --link-stroke: #818cf8;
85
98
  --badge-bg: rgba(168, 85, 247, 0.12);
86
99
  --badge-color: #7e22ce;
@@ -103,13 +116,15 @@ export class ArtifactManager {
103
116
  --root-bg: #6366f1;
104
117
  --root-border: #818cf8;
105
118
  --root-text: #ffffff;
119
+ --pillar-bg: #0f172a;
120
+ --pillar-border: #475569;
106
121
  --link-stroke: #6366f1;
107
122
  --badge-bg: rgba(168, 85, 247, 0.2);
108
123
  --badge-color: #c084fc;
109
124
  --badge-border: rgba(168, 85, 247, 0.35);
110
125
  }
111
126
  body {
112
- margin: 0; padding: 20px;
127
+ margin: 0; padding: 16px;
113
128
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
114
129
  background: var(--bg-page);
115
130
  color: var(--text-main);
@@ -117,31 +132,31 @@ export class ArtifactManager {
117
132
  overflow-x: hidden;
118
133
  transition: background 0.3s ease;
119
134
  }
120
- .header { text-align: center; margin-bottom: 14px; }
121
- h1 { font-size: 19px; color: var(--text-main); margin: 0 0 6px 0; font-weight: 800; letter-spacing: -0.3px; }
135
+ .header { text-align: center; margin-bottom: 12px; }
136
+ h1 { font-size: 18px; color: var(--text-main); margin: 0 0 6px 0; font-weight: 800; letter-spacing: -0.3px; }
122
137
  .badge {
123
138
  font-size: 11px;
124
139
  background: var(--badge-bg);
125
140
  color: var(--badge-color);
126
- padding: 4px 12px; border-radius: 9999px;
141
+ padding: 3px 12px; border-radius: 9999px;
127
142
  border: 1px solid var(--badge-border);
128
143
  text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px;
129
144
  display: inline-block; margin-bottom: 6px;
130
145
  }
131
146
  .controls {
132
- display: flex; gap: 8px; margin-bottom: 14px; align-items: center; flex-wrap: wrap; justify-content: center;
147
+ display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; justify-content: center;
133
148
  }
134
149
  .btn {
135
150
  background: var(--btn-bg); border: 1px solid var(--btn-border);
136
- color: var(--btn-text); padding: 7px 14px; border-radius: 9px; font-size: 12px;
151
+ color: var(--btn-text); padding: 6px 14px; border-radius: 8px; font-size: 12px;
137
152
  font-weight: 700; cursor: pointer; transition: all 0.15s; display: flex; align-items: center; gap: 6px;
138
153
  box-shadow: 0 2px 4px rgba(0,0,0,0.03);
139
154
  }
140
155
  .btn:hover { background: var(--btn-hover); transform: translateY(-1px); }
141
156
  .btn:active { transform: translateY(0); }
142
157
  .viewer-card {
143
- width: 100%; max-width: 1200px; height: 75vh; min-height: 540px;
144
- position: relative; overflow: hidden; border-radius: 18px;
158
+ width: 100%; max-width: 1300px; height: 76vh; min-height: 520px;
159
+ position: relative; overflow: hidden; border-radius: 16px;
145
160
  background: var(--bg-viewport); backdrop-filter: blur(20px);
146
161
  border: 1px solid var(--border-viewport);
147
162
  box-shadow: var(--shadow-viewport);
@@ -151,51 +166,104 @@ export class ArtifactManager {
151
166
  width: 100%; height: 100%;
152
167
  display: block;
153
168
  cursor: grab;
169
+ user-select: none;
154
170
  }
155
171
  #mindmapSvg:active { cursor: grabbing; }
172
+
173
+ /* Native SVG Mindmap Node Styles */
174
+ .mindmap-link {
175
+ fill: none;
176
+ stroke: var(--link-stroke);
177
+ stroke-width: 2.2px;
178
+ stroke-linecap: round;
179
+ transition: stroke 0.2s ease;
180
+ }
181
+ .node-rect {
182
+ fill: var(--node-bg);
183
+ stroke: var(--node-border);
184
+ stroke-width: 1.5px;
185
+ transition: all 0.2s ease;
186
+ cursor: pointer;
187
+ }
188
+ .node-rect:hover {
189
+ stroke: var(--root-bg);
190
+ stroke-width: 2px;
191
+ }
192
+ .node-rect.root {
193
+ fill: var(--root-bg);
194
+ stroke: var(--root-border);
195
+ stroke-width: 2px;
196
+ }
197
+ .node-rect.pillar {
198
+ fill: var(--pillar-bg);
199
+ stroke: var(--pillar-border);
200
+ stroke-width: 1.8px;
201
+ }
202
+ .node-text {
203
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
204
+ font-size: 13px;
205
+ font-weight: 600;
206
+ fill: var(--node-text);
207
+ pointer-events: none;
208
+ user-select: none;
209
+ }
210
+ .depth-0 .node-text {
211
+ font-size: 15px;
212
+ font-weight: 800;
213
+ fill: var(--root-text);
214
+ }
215
+ .depth-1 .node-text {
216
+ font-size: 13.5px;
217
+ font-weight: 700;
218
+ }
219
+ .badge-circle {
220
+ fill: var(--root-bg);
221
+ stroke: var(--bg-viewport);
222
+ stroke-width: 1.5px;
223
+ transition: transform 0.15s ease;
224
+ }
225
+ .badge-circle:hover {
226
+ transform: scale(1.15);
227
+ }
228
+ .badge-text {
229
+ font-family: monospace;
230
+ font-size: 12px;
231
+ font-weight: bold;
232
+ fill: #ffffff;
233
+ pointer-events: none;
234
+ }
235
+
236
+ /* Markmap CSS Overrides */
237
+ foreignObject {
238
+ overflow: visible !important;
239
+ }
156
240
  .markmap-foreign {
157
241
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
158
- font-size: 13.5px !important;
242
+ font-size: 13px !important;
159
243
  font-weight: 600 !important;
160
- line-height: 1.4 !important;
244
+ line-height: 1.35 !important;
161
245
  color: var(--node-text) !important;
162
246
  background: var(--node-bg) !important;
163
247
  border: 1.5px solid var(--node-border) !important;
164
248
  border-radius: 8px !important;
165
- padding: 6px 14px !important;
249
+ padding: 5px 12px !important;
166
250
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04) !important;
167
- display: inline-flex !important;
168
- align-items: center !important;
169
- white-space: nowrap !important;
170
- transition: all 0.2s ease !important;
251
+ display: inline-block !important;
252
+ white-space: normal !important;
253
+ max-width: 320px !important;
254
+ word-break: break-word !important;
171
255
  }
172
256
  .markmap-node[data-depth="0"] .markmap-foreign {
173
257
  background: var(--root-bg) !important;
174
258
  color: var(--root-text) !important;
175
259
  border-color: var(--root-border) !important;
176
- font-size: 16px !important;
260
+ font-size: 15px !important;
177
261
  font-weight: 800 !important;
178
262
  border-radius: 10px !important;
179
- padding: 8px 18px !important;
263
+ padding: 7px 16px !important;
180
264
  box-shadow: 0 4px 14px rgba(79, 70, 229, 0.35) !important;
181
265
  }
182
- .markmap-node[data-depth="1"] .markmap-foreign {
183
- font-weight: 700 !important;
184
- border-width: 1.8px !important;
185
- }
186
- .markmap-link {
187
- fill: none !important;
188
- stroke: var(--link-stroke) !important;
189
- stroke-width: 2px !important;
190
- stroke-linecap: round !important;
191
- }
192
- .markmap-node > circle {
193
- fill: var(--root-bg) !important;
194
- stroke: var(--root-border) !important;
195
- stroke-width: 2px !important;
196
- r: 6 !important;
197
- cursor: pointer !important;
198
- }
266
+
199
267
  .help-hint {
200
268
  position: absolute; bottom: 12px; right: 14px;
201
269
  font-size: 11px; color: var(--text-muted); pointer-events: none;
@@ -222,178 +290,292 @@ export class ArtifactManager {
222
290
  </div>
223
291
  <div class="viewer-card" id="viewerCard">
224
292
  <svg id="mindmapSvg"></svg>
225
- <div class="help-hint">🖱️ Drag to pan · Scroll to zoom · Click nodes with circle handles to collapse/expand</div>
293
+ <div class="help-hint">🖱️ Drag to pan · Scroll to zoom · Click +/- handles to collapse branches</div>
226
294
  </div>
227
295
  <script>
228
296
  const rawContent = ${JSON.stringify(artifact.content.trim())};
229
297
 
230
- function parseToMarkdown(content) {
231
- if (content.startsWith('#') || content.startsWith('-') || content.includes('\\n#') || content.includes('\\n-')) {
232
- return content;
233
- }
298
+ function parseTree(content) {
234
299
  const lines = content.split('\\n');
235
- const mdLines = [];
300
+ const rootNode = { name: '${artifact.title.replace(/'/g, "\\'")}', children: [], collapsed: false };
301
+ const stack = [{ node: rootNode, depth: 0 }];
302
+
236
303
  let baseIndent = -1;
237
- for (let line of lines) {
304
+ for (let rawLine of lines) {
305
+ const line = rawLine.replace(/\\r$/, '');
238
306
  const trimmed = line.trim();
239
307
  if (!trimmed || trimmed === 'mindmap') continue;
308
+
240
309
  const indent = line.search(/\\S/);
241
310
  if (baseIndent === -1) baseIndent = indent;
242
311
  const rel = Math.max(0, indent - baseIndent);
243
- const depth = Math.floor(rel / 2);
244
-
312
+ let depth = Math.floor(rel / 2);
313
+
245
314
  let clean = trimmed
315
+ .replace(/^#+\\s*/, '')
316
+ .replace(/^[-*]\\s*/, '')
246
317
  .replace(/^root\\(\\((.*?)\\)\\)$/, '$1')
247
318
  .replace(/^\\(\\((.*?)\\)\\)$/, '$1')
248
319
  .replace(/^\\[(.*?)\\]$/, '$1')
249
320
  .replace(/^\\((.*?)\\)$/, '$1')
250
321
  .replace(/^\\)\\)(.*?)\\(\\($/, '$1')
251
322
  .replace(/^\\)(.*?)\\($/, '$1')
252
- .replace(/^\\{\\{(.*?)\\}\\}/, '$1');
253
-
323
+ .replace(/^\\{\\{(.*?)\\}\\}/, '$1')
324
+ .trim();
325
+
254
326
  if (depth === 0) {
255
- mdLines.push('# ' + clean);
256
- } else if (depth === 1) {
257
- mdLines.push('## ' + clean);
258
- } else {
259
- mdLines.push(' '.repeat(depth - 1) + '- ' + clean);
260
- }
261
- }
262
- return mdLines.join('\\n');
263
- }
264
-
265
- const markdownText = parseToMarkdown(rawContent);
266
- let mmInstance = null;
267
-
268
- function renderMarkmap() {
269
- try {
270
- if (window.markmap && window.markmap.Transformer) {
271
- const { Transformer, Markmap } = window.markmap;
272
- const transformer = new Transformer();
273
- const { root } = transformer.transform(markdownText);
274
- const svgEl = document.getElementById('mindmapSvg');
275
- svgEl.innerHTML = '';
276
- mmInstance = Markmap.create(svgEl, {
277
- autoFit: true,
278
- duration: 250,
279
- nodeMinHeight: 28,
280
- spacingVertical: 14,
281
- spacingHorizontal: 85,
282
- paddingX: 16
283
- }, root);
284
- return;
327
+ rootNode.name = clean || rootNode.name;
328
+ continue;
285
329
  }
286
- } catch (err) {
287
- console.warn('Markmap load error:', err);
330
+
331
+ const newNode = { name: clean, children: [], collapsed: false };
332
+ while (stack.length > depth) stack.pop();
333
+ stack[stack.length - 1].node.children.push(newNode);
334
+ stack.push({ node: newNode, depth: depth });
288
335
  }
289
- renderFallbackTree();
336
+
337
+ return rootNode;
290
338
  }
291
339
 
292
- function renderFallbackTree() {
293
- const svg = document.getElementById('mindmapSvg');
294
- svg.innerHTML = '';
295
- const lines = markdownText.split('\\n').filter(l => l.trim().length > 0);
296
- const rootNode = { name: '${artifact.title}', children: [] };
297
- const stack = [{ node: rootNode, depth: 0 }];
340
+ const rootTree = parseTree(rawContent);
341
+ const svgEl = document.getElementById('mindmapSvg');
342
+ const viewerCard = document.getElementById('viewerCard');
298
343
 
299
- lines.forEach((line) => {
300
- let depth = 1;
301
- let text = line.trim();
302
- if (text.startsWith('# ')) {
303
- rootNode.name = text.replace('# ', '').trim();
304
- return;
305
- } else if (text.startsWith('## ')) {
306
- depth = 1;
307
- text = text.replace('## ', '').trim();
308
- } else {
309
- const leadSpaces = line.search(/\\S/);
310
- depth = 2 + Math.floor(leadSpaces / 2);
311
- text = text.replace(/^[-*]\\s*/, '').trim();
344
+ let currentScale = 1;
345
+ let currentX = 0;
346
+ let currentY = 0;
347
+ let isPanning = false;
348
+ let startPointerX = 0;
349
+ let startPointerY = 0;
350
+ let startPosX = 0;
351
+ let startPosY = 0;
352
+ let bbox = { minX: 0, maxX: 800, minY: 0, maxY: 600 };
353
+
354
+ function renderTree() {
355
+ svgEl.innerHTML = '';
356
+
357
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
358
+ g.setAttribute('id', 'viewportGroup');
359
+ svgEl.appendChild(g);
360
+
361
+ // Measure node pill dimensions based on text length
362
+ function measure(node, depth = 0) {
363
+ node.depth = depth;
364
+ const textLen = (node.name || '').length;
365
+ const isRoot = depth === 0;
366
+ const isPillar = depth === 1;
367
+ const fontFactor = isRoot ? 9.5 : isPillar ? 8.2 : 7.6;
368
+ const pad = isRoot ? 40 : 28;
369
+ node.w = Math.max(isRoot ? 160 : 120, Math.min(360, textLen * fontFactor + pad));
370
+ node.h = isRoot ? 44 : isPillar ? 38 : 34;
371
+ if (node.children && !node.collapsed) {
372
+ node.children.forEach(c => measure(c, depth + 1));
312
373
  }
313
- const newNode = { name: text, children: [] };
314
- while (stack.length > depth) stack.pop();
315
- stack[stack.length - 1].node.children.push(newNode);
316
- stack.push({ node: newNode, depth: depth });
317
- });
374
+ }
375
+ measure(rootTree, 0);
318
376
 
377
+ // Layout coordinates
319
378
  let curY = 40;
320
- function layout(node, depth = 0) {
321
- node.depth = depth;
322
- node.x = 60 + depth * 220;
323
- if (!node.children || node.children.length === 0) {
324
- node.y = curY;
325
- curY += 46;
379
+ const HORIZ_GAP = 75;
380
+ const VERT_GAP = 14;
381
+
382
+ function layout(node, startX = 40) {
383
+ node.x = startX;
384
+ if (!node.children || node.children.length === 0 || node.collapsed) {
385
+ node.y = curY + node.h / 2;
386
+ curY += node.h + VERT_GAP;
326
387
  } else {
327
- node.children.forEach(c => layout(c, depth + 1));
388
+ const nextX = startX + node.w + HORIZ_GAP;
389
+ node.children.forEach(c => layout(c, nextX));
328
390
  const firstY = node.children[0].y;
329
391
  const lastY = node.children[node.children.length - 1].y;
330
392
  node.y = (firstY + lastY) / 2;
331
393
  }
332
394
  }
333
- layout(rootNode);
395
+ layout(rootTree, 40);
334
396
 
335
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
336
- g.setAttribute('id', 'treeGroup');
337
- svg.appendChild(g);
397
+ // Calculate Bounding Box
398
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
399
+ function findBounds(node) {
400
+ minX = Math.min(minX, node.x);
401
+ maxX = Math.max(maxX, node.x + node.w + 20);
402
+ minY = Math.min(minY, node.y - node.h / 2);
403
+ maxY = Math.max(maxY, node.y + node.h / 2);
404
+ if (node.children && !node.collapsed) {
405
+ node.children.forEach(findBounds);
406
+ }
407
+ }
408
+ findBounds(rootTree);
409
+ bbox = { minX, maxX, minY, maxY };
338
410
 
411
+ // Draw links and nodes
339
412
  function draw(node) {
340
- if (node.children) {
341
- node.children.forEach(c => {
413
+ if (node.children && node.children.length > 0 && !node.collapsed) {
414
+ node.children.forEach(child => {
415
+ const x1 = node.x + node.w;
416
+ const y1 = node.y;
417
+ const x2 = child.x;
418
+ const y2 = child.y;
419
+ const dx = (x2 - x1) * 0.48;
342
420
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
343
- const x1 = node.x + 130, y1 = node.y;
344
- const x2 = c.x, y2 = c.y;
345
- const dx = (x2 - x1) * 0.5;
346
421
  path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' C ' + (x1 + dx) + ' ' + y1 + ', ' + (x2 - dx) + ' ' + y2 + ', ' + x2 + ' ' + y2);
347
- path.setAttribute('class', 'markmap-link');
422
+ path.setAttribute('class', 'mindmap-link');
348
423
  g.appendChild(path);
349
- draw(c);
424
+ draw(child);
350
425
  });
351
426
  }
352
- const foreign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
353
- foreign.setAttribute('x', node.x - 10);
354
- foreign.setAttribute('y', node.y - 18);
355
- foreign.setAttribute('width', '200');
356
- foreign.setAttribute('height', '40');
357
- foreign.innerHTML = '<div class="markmap-node" data-depth="' + node.depth + '"><div class="markmap-foreign">' + node.name + '</div></div>';
358
- g.appendChild(foreign);
427
+
428
+ const nodeG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
429
+ nodeG.setAttribute('class', 'mindmap-node depth-' + node.depth);
430
+
431
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
432
+ rect.setAttribute('x', node.x);
433
+ rect.setAttribute('y', node.y - node.h / 2);
434
+ rect.setAttribute('width', node.w);
435
+ rect.setAttribute('height', node.h);
436
+ rect.setAttribute('rx', node.depth === 0 ? '12' : '8');
437
+ rect.setAttribute('ry', node.depth === 0 ? '12' : '8');
438
+ rect.setAttribute('class', 'node-rect ' + (node.depth === 0 ? 'root' : node.depth === 1 ? 'pillar' : 'leaf'));
439
+ nodeG.appendChild(rect);
440
+
441
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
442
+ text.setAttribute('x', node.x + node.w / 2);
443
+ text.setAttribute('y', node.y + (node.depth === 0 ? 5.5 : 4.5));
444
+ text.setAttribute('text-anchor', 'middle');
445
+ text.setAttribute('class', 'node-text');
446
+ text.textContent = node.name;
447
+ nodeG.appendChild(text);
448
+
449
+ // Collapsible Badge Button
450
+ if (node.children && node.children.length > 0) {
451
+ const badgeG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
452
+ badgeG.setAttribute('transform', 'translate(' + (node.x + node.w) + ', ' + node.y + ')');
453
+ badgeG.style.cursor = 'pointer';
454
+ badgeG.onclick = (e) => {
455
+ e.stopPropagation();
456
+ node.collapsed = !node.collapsed;
457
+ renderTree();
458
+ };
459
+
460
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
461
+ circle.setAttribute('r', '7.5');
462
+ circle.setAttribute('class', 'badge-circle');
463
+ badgeG.appendChild(circle);
464
+
465
+ const sign = document.createElementNS('http://www.w3.org/2000/svg', 'text');
466
+ sign.setAttribute('y', '3.5');
467
+ sign.setAttribute('text-anchor', 'middle');
468
+ sign.setAttribute('class', 'badge-text');
469
+ sign.textContent = node.collapsed ? '+' : '−';
470
+ badgeG.appendChild(sign);
471
+
472
+ nodeG.appendChild(badgeG);
473
+ }
474
+
475
+ g.appendChild(nodeG);
359
476
  }
360
- draw(rootNode);
361
- svg.setAttribute('viewBox', '0 0 ' + Math.max(800, curY * 1.8) + ' ' + Math.max(500, curY + 60));
477
+ draw(rootTree);
478
+
479
+ applyTransform();
480
+ }
481
+
482
+ function applyTransform() {
483
+ const g = document.getElementById('viewportGroup');
484
+ if (g) {
485
+ g.setAttribute('transform', 'translate(' + currentX + ', ' + currentY + ') scale(' + currentScale + ')');
486
+ }
487
+ }
488
+
489
+ function fitView() {
490
+ const cw = viewerCard.clientWidth || 1000;
491
+ const ch = viewerCard.clientHeight || 550;
492
+ const tw = (bbox.maxX - bbox.minX) + 80;
493
+ const th = (bbox.maxY - bbox.minY) + 80;
494
+
495
+ currentScale = Math.min(1.15, Math.max(0.35, Math.min((cw - 60) / tw, (ch - 60) / th)));
496
+ currentX = (cw - tw * currentScale) / 2 + 20 * currentScale;
497
+ currentY = (ch - th * currentScale) / 2 + 20 * currentScale;
498
+ applyTransform();
362
499
  }
363
500
 
364
501
  function zoomIn() {
365
- if (mmInstance) mmInstance.rescale(1.25);
502
+ currentScale = Math.min(currentScale * 1.25, 3.5);
503
+ applyTransform();
366
504
  }
505
+
367
506
  function zoomOut() {
368
- if (mmInstance) mmInstance.rescale(0.8);
369
- }
370
- function fitView() {
371
- if (mmInstance) mmInstance.fit();
507
+ currentScale = Math.max(currentScale * 0.8, 0.25);
508
+ applyTransform();
372
509
  }
510
+
373
511
  function toggleTheme() {
374
512
  document.body.classList.toggle('theme-dark');
375
513
  const isDark = document.body.classList.contains('theme-dark');
376
514
  const btn = document.getElementById('themeToggleBtn');
377
515
  if (btn) btn.textContent = isDark ? '☀️ Light Mode' : '🌙 Poster Dark';
378
516
  }
517
+
379
518
  function copySource() {
380
519
  navigator.clipboard.writeText(rawContent).then(() => {
381
- alert('Mind map source copied to clipboard!');
520
+ alert('Mind map source code copied to clipboard!');
382
521
  });
383
522
  }
384
523
 
524
+ // Pointer Interaction (Pan & Zoom)
525
+ viewerCard.addEventListener('pointerdown', (e) => {
526
+ isPanning = true;
527
+ startPointerX = e.clientX;
528
+ startPointerY = e.clientY;
529
+ startPosX = currentX;
530
+ startPosY = currentY;
531
+ viewerCard.setPointerCapture(e.pointerId);
532
+ });
533
+
534
+ viewerCard.addEventListener('pointermove', (e) => {
535
+ if (!isPanning) return;
536
+ currentX = startPosX + (e.clientX - startPointerX);
537
+ currentY = startPosY + (e.clientY - startPointerY);
538
+ applyTransform();
539
+ });
540
+
541
+ const endPan = (e) => {
542
+ isPanning = false;
543
+ try { viewerCard.releasePointerCapture(e.pointerId); } catch (_) {}
544
+ };
545
+ viewerCard.addEventListener('pointerup', endPan);
546
+ viewerCard.addEventListener('pointercancel', endPan);
547
+
548
+ viewerCard.addEventListener('wheel', (e) => {
549
+ e.preventDefault();
550
+ const delta = e.deltaY < 0 ? 1.12 : 0.89;
551
+ const newScale = Math.min(Math.max(currentScale * delta, 0.25), 3.5);
552
+
553
+ const rect = viewerCard.getBoundingClientRect();
554
+ const mouseX = e.clientX - rect.left;
555
+ const mouseY = e.clientY - rect.top;
556
+
557
+ currentX = mouseX - (mouseX - currentX) * (newScale / currentScale);
558
+ currentY = mouseY - (mouseY - currentY) * (newScale / currentScale);
559
+ currentScale = newScale;
560
+ applyTransform();
561
+ }, { passive: false });
562
+
563
+ window.addEventListener('resize', fitView);
385
564
  window.addEventListener('load', () => {
386
- setTimeout(renderMarkmap, 60);
565
+ renderTree();
566
+ setTimeout(fitView, 50);
387
567
  });
388
- if (document.readyState === 'complete') renderMarkmap();
568
+ if (document.readyState === 'complete') {
569
+ renderTree();
570
+ setTimeout(fitView, 50);
571
+ }
389
572
  </script>
390
573
  </body>
391
574
  </html>`;
392
- fs.writeFileSync(path.join(this.baseDir, `${artifact.id}.html`), htmlMindmap, 'utf-8');
393
- }
394
- else if (artifact.type === 'graph') {
395
- const badgeLabel = '📊 Architecture Graph';
396
- const htmlGraph = `<!DOCTYPE html>
575
+ }
576
+ if (artifact.type === 'graph') {
577
+ const badgeLabel = '📊 Architecture Graph';
578
+ return `<!DOCTYPE html>
397
579
  <html lang="en">
398
580
  <head>
399
581
  <meta charset="UTF-8">
@@ -442,7 +624,7 @@ export class ArtifactManager {
442
624
  transition: background 0.3s ease;
443
625
  }
444
626
  .header { text-align: center; margin-bottom: 14px; }
445
- h1 { font-size: 19px; color: var(--text-main); margin: 0 0 6px 0; font-weight: 800; }
627
+ h1 { font-size: 19px; color: var(--text-main); margin: 0 0 6px 0; font-weight: 800; letter-spacing: -0.3px; }
446
628
  .badge {
447
629
  font-size: 11px;
448
630
  background: var(--badge-bg);
@@ -464,35 +646,22 @@ export class ArtifactManager {
464
646
  .btn:hover { background: var(--btn-hover); transform: translateY(-1px); }
465
647
  .btn:active { transform: translateY(0); }
466
648
  .viewer-card {
467
- width: 100%; max-width: 1150px; height: 72vh; min-height: 520px;
649
+ width: 100%; max-width: 1300px; height: 76vh; min-height: 520px;
468
650
  position: relative; overflow: hidden; border-radius: 18px;
469
651
  background: var(--bg-viewport); backdrop-filter: blur(20px);
470
652
  border: 1px solid var(--border-viewport);
471
653
  box-shadow: var(--shadow-viewport);
472
- cursor: grab; user-select: none; touch-action: none;
654
+ display: flex; align-items: center; justify-content: center;
655
+ touch-action: none; cursor: grab;
473
656
  }
474
- .viewer-card.grabbing { cursor: grabbing; }
475
- .canvas-wrap {
657
+ .viewer-card:active { cursor: grabbing; }
658
+ .diagram-container {
476
659
  width: 100%; height: 100%;
477
660
  display: flex; align-items: center; justify-content: center;
478
661
  transform-origin: center center;
479
- will-change: transform;
480
- }
481
- .canvas-wrap.animating {
482
- transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
483
- }
484
- .mermaid { width: 100%; text-align: center; }
485
- .mermaid svg { max-width: 100%; height: auto; }
486
- .help-hint {
487
- position: absolute; bottom: 12px; right: 14px;
488
- font-size: 11px; color: var(--text-muted); pointer-events: none;
489
- background: rgba(255, 255, 255, 0.75); padding: 3px 8px; border-radius: 6px;
490
- border: 1px solid rgba(0,0,0,0.05);
491
- }
492
- body.theme-dark .help-hint {
493
- background: rgba(18, 24, 38, 0.7);
494
- border-color: rgba(255, 255, 255, 0.08);
662
+ transition: transform 0.05s ease-out;
495
663
  }
664
+ .mermaid { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
496
665
  </style>
497
666
  </head>
498
667
  <body>
@@ -503,133 +672,64 @@ export class ArtifactManager {
503
672
  <div class="controls">
504
673
  <button class="btn" onclick="zoomIn()">🔍 Zoom +</button>
505
674
  <button class="btn" onclick="zoomOut()">🔍 Zoom -</button>
506
- <button class="btn" onclick="resetZoom()">⛶ Reset</button>
675
+ <button class="btn" onclick="resetZoom()">⛶ Fit / Reset</button>
507
676
  <button class="btn" onclick="toggleTheme()" id="themeToggleBtn">🌙 Poster Dark</button>
508
677
  </div>
509
678
  <div class="viewer-card" id="viewerCard">
510
- <div class="canvas-wrap" id="canvasWrap">
511
- <pre class="mermaid" id="mermaidDiagram">
512
- ${artifact.content.trim()}
513
- </pre>
679
+ <div class="diagram-container" id="diagramContainer">
680
+ <pre class="mermaid">${artifact.content.replace(/<antri_artifact[\s\S]*?>|<\/antri_artifact>/gi, '').trim()}</pre>
514
681
  </div>
515
- <div class="help-hint">🖱️ Drag to pan · Scroll to zoom</div>
516
682
  </div>
517
683
  <script>
518
684
  let scale = 1.0;
519
685
  let posX = 0;
520
686
  let posY = 0;
521
687
  let isDragging = false;
522
- let startPointerX = 0;
523
- let startPointerY = 0;
524
- let startPosX = 0;
525
- let startPosY = 0;
526
-
527
- const activeTouches = new Map();
528
- let initialPinchDist = 0;
529
- let initialPinchScale = 1.0;
530
-
688
+ let startX = 0;
689
+ let startY = 0;
690
+ const container = document.getElementById('diagramContainer');
531
691
  const viewerCard = document.getElementById('viewerCard');
532
- const canvasWrap = document.getElementById('canvasWrap');
533
692
 
534
693
  function updateTransform(animate = false) {
535
694
  if (animate) {
536
- canvasWrap.classList.add('animating');
537
- setTimeout(() => canvasWrap.classList.remove('animating'), 320);
695
+ container.style.transition = 'transform 0.25s cubic-bezier(0.2, 0, 0, 1)';
538
696
  } else {
539
- canvasWrap.classList.remove('animating');
697
+ container.style.transition = 'none';
540
698
  }
541
- canvasWrap.style.transform = 'translate3d(' + posX + 'px, ' + posY + 'px, 0) scale(' + scale + ')';
699
+ container.style.transform = 'translate(' + posX + 'px, ' + posY + 'px) scale(' + scale + ')';
542
700
  }
543
701
 
544
702
  viewerCard.addEventListener('pointerdown', (e) => {
545
- activeTouches.set(e.pointerId, { x: e.clientX, y: e.clientY });
546
- try { viewerCard.setPointerCapture(e.pointerId); } catch {}
547
-
548
- if (activeTouches.size === 1) {
549
- isDragging = true;
550
- viewerCard.classList.add('grabbing');
551
- startPointerX = e.clientX;
552
- startPointerY = e.clientY;
553
- startPosX = posX;
554
- startPosY = posY;
555
- } else if (activeTouches.size === 2) {
556
- isDragging = false;
557
- viewerCard.classList.remove('grabbing');
558
- const pts = Array.from(activeTouches.values());
559
- initialPinchDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
560
- initialPinchScale = scale;
561
- }
703
+ isDragging = true;
704
+ startX = e.clientX - posX;
705
+ startY = e.clientY - posY;
706
+ viewerCard.setPointerCapture(e.pointerId);
562
707
  });
563
708
 
564
709
  viewerCard.addEventListener('pointermove', (e) => {
565
- if (!activeTouches.has(e.pointerId)) return;
566
- activeTouches.set(e.pointerId, { x: e.clientX, y: e.clientY });
567
-
568
- if (activeTouches.size === 1 && isDragging) {
569
- const dx = e.clientX - startPointerX;
570
- const dy = e.clientY - startPointerY;
571
- posX = startPosX + dx;
572
- posY = startPosY + dy;
573
- updateTransform(false);
574
- } else if (activeTouches.size === 2) {
575
- const pts = Array.from(activeTouches.values());
576
- const currentDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
577
- if (initialPinchDist > 0) {
578
- scale = Math.min(Math.max(initialPinchScale * (currentDist / initialPinchDist), 0.2), 4.0);
579
- updateTransform(false);
580
- }
581
- }
710
+ if (!isDragging) return;
711
+ posX = e.clientX - startX;
712
+ posY = e.clientY - startY;
713
+ updateTransform(false);
582
714
  });
583
715
 
584
- function endPointer(e) {
585
- activeTouches.delete(e.pointerId);
586
- try { viewerCard.releasePointerCapture(e.pointerId); } catch {}
587
- if (activeTouches.size === 0) {
588
- isDragging = false;
589
- viewerCard.classList.remove('grabbing');
590
- } else if (activeTouches.size === 1) {
591
- isDragging = true;
592
- viewerCard.classList.add('grabbing');
593
- const remaining = Array.from(activeTouches.values())[0];
594
- startPointerX = remaining.x;
595
- startPointerY = remaining.y;
596
- startPosX = posX;
597
- startPosY = posY;
598
- }
599
- }
600
-
601
- viewerCard.addEventListener('pointerup', endPointer);
602
- viewerCard.addEventListener('pointercancel', endPointer);
716
+ const endDrag = (e) => {
717
+ isDragging = false;
718
+ try { viewerCard.releasePointerCapture(e.pointerId); } catch (_) {}
719
+ };
720
+ viewerCard.addEventListener('pointerup', endDrag);
721
+ viewerCard.addEventListener('pointercancel', endDrag);
603
722
 
604
723
  viewerCard.addEventListener('wheel', (e) => {
605
724
  e.preventDefault();
606
- const delta = e.deltaY < 0 ? 1.15 : 0.87;
607
- const newScale = Math.min(Math.max(scale * delta, 0.2), 4.0);
608
-
609
- const rect = viewerCard.getBoundingClientRect();
610
- const mouseX = e.clientX - rect.left - rect.width / 2;
611
- const mouseY = e.clientY - rect.top - rect.height / 2;
612
-
613
- posX = mouseX - (mouseX - posX) * (newScale / scale);
614
- posY = mouseY - (mouseY - posY) * (newScale / scale);
615
- scale = newScale;
725
+ const delta = e.deltaY < 0 ? 1.12 : 0.89;
726
+ scale = Math.min(Math.max(scale * delta, 0.25), 4.0);
616
727
  updateTransform(false);
617
728
  }, { passive: false });
618
729
 
619
- function zoomIn() {
620
- scale = Math.min(scale * 1.25, 4.0);
621
- updateTransform(true);
622
- }
623
- function zoomOut() {
624
- scale = Math.max(scale * 0.8, 0.2);
625
- updateTransform(true);
626
- }
627
- function resetZoom() {
628
- scale = 1.0;
629
- posX = 0;
630
- posY = 0;
631
- updateTransform(true);
632
- }
730
+ function zoomIn() { scale = Math.min(scale * 1.25, 4.0); updateTransform(true); }
731
+ function zoomOut() { scale = Math.max(scale * 0.8, 0.25); updateTransform(true); }
732
+ function resetZoom() { scale = 1.0; posX = 0; posY = 0; updateTransform(true); }
633
733
  function toggleTheme() {
634
734
  document.body.classList.toggle('theme-dark');
635
735
  const isDark = document.body.classList.contains('theme-dark');
@@ -637,32 +737,12 @@ ${artifact.content.trim()}
637
737
  if (btn) btn.textContent = isDark ? '☀️ Light Mode' : '🌙 Poster Dark';
638
738
  }
639
739
 
640
- mermaid.initialize({
641
- startOnLoad: true,
642
- theme: 'base',
643
- securityLevel: 'loose',
644
- themeVariables: {
645
- darkMode: false,
646
- background: '#ffffff',
647
- primaryColor: '#6366f1',
648
- primaryTextColor: '#0f172a',
649
- primaryBorderColor: '#4f46e5',
650
- lineColor: '#6366f1',
651
- secondaryColor: '#0ea5e9',
652
- tertiaryColor: '#10b981',
653
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
654
- fontSize: '14px'
655
- }
656
- });
740
+ mermaid.initialize({ startOnLoad: true, theme: 'base', securityLevel: 'loose' });
657
741
  </script>
658
742
  </body>
659
743
  </html>`;
660
- fs.writeFileSync(path.join(this.baseDir, `${artifact.id}.html`), htmlGraph, 'utf-8');
661
- }
662
744
  }
663
- catch { }
664
- this.persistIndex();
665
- return artifact;
745
+ return artifact.content;
666
746
  }
667
747
  getArtifact(id) {
668
748
  return this.artifacts.get(id) || null;