@shumoku/renderer 0.2.6 → 0.2.15

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.
@@ -88,7 +88,10 @@ export function renderHierarchical(sheets, options) {
88
88
  breadcrumb: ['root'],
89
89
  sheets: sheetInfos,
90
90
  };
91
- return generateHierarchicalHtml(sheetSvgs, title, { ...opts, navigation });
91
+ return generateHierarchicalHtml(sheetSvgs, title, {
92
+ ...opts,
93
+ navigation,
94
+ });
92
95
  }
93
96
  /**
94
97
  * Check if graph is a hierarchical graph
@@ -153,21 +156,21 @@ function buildNavigationState(graph, currentSheet) {
153
156
  }
154
157
  function generateHtml(svg, title, options) {
155
158
  const brandingHtml = options.branding
156
- ? `<a class="branding" href="https://shumoku.packof.me" target="_blank" rel="noopener">
157
- <svg class="branding-icon" viewBox="0 0 1024 1024" fill="none"><rect x="64" y="64" width="896" height="896" rx="200" fill="#7FE4C1"/><g transform="translate(90,40) scale(1.25)"><path fill="#1F2328" d="M380 340H450V505H700V555H510V645H450V645H380Z"/></g></svg>
158
- <span>Made with Shumoku</span>
159
+ ? `<a class="branding" href="https://shumoku.packof.me" target="_blank" rel="noopener">
160
+ <svg class="branding-icon" viewBox="0 0 1024 1024" fill="none"><rect x="64" y="64" width="896" height="896" rx="200" fill="#7FE4C1"/><g transform="translate(90,40) scale(1.25)"><path fill="#1F2328" d="M380 340H450V505H700V555H510V645H450V645H380Z"/></g></svg>
161
+ <span>Made with Shumoku</span>
159
162
  </a>`
160
163
  : '';
161
164
  const toolbarHtml = options.toolbar
162
- ? `<div class="toolbar">
163
- <span class="toolbar-title">${escapeHtml(title)}</span>
164
- <div class="toolbar-buttons">
165
- <button id="btn-out" title="Zoom Out"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M20 12H4"/></svg></button>
166
- <span class="zoom-text" id="zoom">100%</span>
167
- <button id="btn-in" title="Zoom In"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg></button>
168
- <button id="btn-fit" title="Fit"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg></button>
169
- <button id="btn-reset" title="Reset"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg></button>
170
- </div>
165
+ ? `<div class="toolbar">
166
+ <span class="toolbar-title">${escapeHtml(title)}</span>
167
+ <div class="toolbar-buttons">
168
+ <button id="btn-out" title="Zoom Out"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M20 12H4"/></svg></button>
169
+ <span class="zoom-text" id="zoom">100%</span>
170
+ <button id="btn-in" title="Zoom In"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg></button>
171
+ <button id="btn-fit" title="Fit"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg></button>
172
+ <button id="btn-reset" title="Reset"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg></button>
173
+ </div>
171
174
  </div>`
172
175
  : '';
173
176
  // Navigation toolbar for hierarchical diagrams
@@ -181,274 +184,274 @@ function generateHtml(svg, title, options) {
181
184
  const navHeight = options.hierarchical && options.navigation ? 60 : 0;
182
185
  const totalHeaderHeight = headerHeight + navHeight;
183
186
  const containerHeight = totalHeaderHeight > 0 ? `calc(100vh - ${totalHeaderHeight}px)` : '100vh';
184
- return `<!DOCTYPE html>
185
- <html>
186
- <head>
187
- <meta charset="UTF-8">
188
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
189
- <title>${escapeHtml(title)}</title>
190
- <style>
191
- * { margin: 0; padding: 0; box-sizing: border-box; }
192
- body { background: #f5f5f5; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; }
193
- .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; background: white; border-bottom: 1px solid #e5e5e5; }
194
- .toolbar-title { font-size: 14px; color: #666; }
195
- .toolbar-buttons { display: flex; gap: 4px; align-items: center; }
196
- .toolbar button { padding: 6px; border: none; background: none; cursor: pointer; border-radius: 4px; color: #666; }
197
- .toolbar button:hover { background: #f0f0f0; }
198
- .zoom-text { min-width: 50px; text-align: center; font-size: 13px; color: #666; }
199
- .container { position: relative; width: 100%; height: ${containerHeight}; overflow: hidden; cursor: grab; background: repeating-conic-gradient(#f8f8f8 0% 25%, transparent 0% 50%) 50% / 20px 20px; }
200
- .container.dragging { cursor: grabbing; }
201
- .container > svg { width: 100%; height: 100%; }
202
- .branding { position: absolute; bottom: 16px; right: 16px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(255,255,255,0.95); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; font-size: 13px; font-family: system-ui, sans-serif; color: #555; text-decoration: none; transition: all 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 100; }
203
- .branding:hover { color: #222; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
204
- .branding-icon { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
205
- /* SVG interactive styles */
206
- .node { cursor: pointer; }
207
- .node:hover rect, .node:hover circle, .node:hover polygon { filter: brightness(0.95); }
208
- .port { cursor: pointer; }
209
- .link-hit-area { cursor: pointer; }
210
- /* Subgraph click for hierarchical navigation */
211
- .subgraph[data-has-sheet] { cursor: pointer; }
212
- .subgraph[data-has-sheet]:hover > rect { filter: brightness(0.95); }
213
- ${navStyles}
214
- </style>
215
- </head>
216
- <body>
217
- ${toolbarHtml}
218
- ${navToolbarHtml}
219
- <div class="container" id="container">
220
- ${svg}
221
- ${brandingHtml}
222
- </div>
223
- <script>${INTERACTIVE_IIFE}</script>
224
- <script>
225
- (function() {
226
- var svg = document.querySelector('#container > svg');
227
- var container = document.getElementById('container');
228
- if (!svg || !container) { console.error('SVG or container not found'); return; }
229
-
230
- var vb = { x: 0, y: 0, w: 0, h: 0 };
231
- var origVb = { x: 0, y: 0, w: 0, h: 0 };
232
- var drag = { active: false, x: 0, y: 0, vx: 0, vy: 0 };
233
-
234
- function init() {
235
- var w = parseFloat(svg.getAttribute('width')) || 800;
236
- var h = parseFloat(svg.getAttribute('height')) || 600;
237
- var existing = svg.getAttribute('viewBox');
238
- if (existing) {
239
- var p = existing.split(/\\s+|,/).map(Number);
240
- origVb = { x: p[0] || 0, y: p[1] || 0, w: p[2] || w, h: p[3] || h };
241
- } else {
242
- origVb = { x: 0, y: 0, w: w, h: h };
243
- }
244
- svg.removeAttribute('width');
245
- svg.removeAttribute('height');
246
- svg.style.width = '100%';
247
- svg.style.height = '100%';
248
- fitView();
249
-
250
- if (window.ShumokuInteractive) {
251
- window.ShumokuInteractive.initInteractive({ target: svg, modal: { enabled: true }, tooltip: { enabled: true }, panZoom: { enabled: false } });
252
- }
253
- }
254
-
255
- function updateViewBox() {
256
- svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
257
- var zoomEl = document.getElementById('zoom');
258
- if (zoomEl) zoomEl.textContent = Math.round(origVb.w / vb.w * 100) + '%';
259
- }
260
-
261
- function fitView() {
262
- var cw = container.clientWidth || 800;
263
- var ch = container.clientHeight || 600;
264
- var scale = Math.min(cw / origVb.w, ch / origVb.h) * 0.9;
265
- vb.w = cw / scale;
266
- vb.h = ch / scale;
267
- vb.x = origVb.x + (origVb.w - vb.w) / 2;
268
- vb.y = origVb.y + (origVb.h - vb.h) / 2;
269
- updateViewBox();
270
- }
271
-
272
- function resetView() {
273
- vb.x = origVb.x; vb.y = origVb.y; vb.w = origVb.w; vb.h = origVb.h;
274
- updateViewBox();
275
- }
276
-
277
- function zoom(f) {
278
- var cx = vb.x + vb.w / 2, cy = vb.y + vb.h / 2;
279
- var nw = vb.w / f, nh = vb.h / f;
280
- var scale = origVb.w / nw;
281
- if (scale < 0.1 || scale > 10) return;
282
- vb.w = nw; vb.h = nh; vb.x = cx - nw / 2; vb.y = cy - nh / 2;
283
- updateViewBox();
284
- }
285
-
286
- var btnIn = document.getElementById('btn-in');
287
- var btnOut = document.getElementById('btn-out');
288
- var btnFit = document.getElementById('btn-fit');
289
- var btnReset = document.getElementById('btn-reset');
290
- if (btnIn) btnIn.addEventListener('click', function() { zoom(1.2); });
291
- if (btnOut) btnOut.addEventListener('click', function() { zoom(1/1.2); });
292
- if (btnFit) btnFit.addEventListener('click', fitView);
293
- if (btnReset) btnReset.addEventListener('click', resetView);
294
-
295
- container.addEventListener('wheel', function(e) {
296
- e.preventDefault();
297
- var rect = container.getBoundingClientRect();
298
- var mx = (e.clientX - rect.left) / rect.width;
299
- var my = (e.clientY - rect.top) / rect.height;
300
- var px = vb.x + vb.w * mx, py = vb.y + vb.h * my;
301
- var f = e.deltaY > 0 ? 1/1.2 : 1.2;
302
- var nw = vb.w / f, nh = vb.h / f;
303
- var scale = origVb.w / nw;
304
- if (scale < 0.1 || scale > 10) return;
305
- vb.w = nw; vb.h = nh; vb.x = px - nw * mx; vb.y = py - nh * my;
306
- updateViewBox();
307
- }, { passive: false });
308
-
309
- container.addEventListener('mousedown', function(e) {
310
- if (e.button === 0) {
311
- drag = { active: true, x: e.clientX, y: e.clientY, vx: vb.x, vy: vb.y };
312
- container.classList.add('dragging');
313
- }
314
- });
315
-
316
- document.addEventListener('mousemove', function(e) {
317
- if (!drag.active) return;
318
- var sx = vb.w / container.clientWidth;
319
- var sy = vb.h / container.clientHeight;
320
- vb.x = drag.vx - (e.clientX - drag.x) * sx;
321
- vb.y = drag.vy - (e.clientY - drag.y) * sy;
322
- updateViewBox();
323
- });
324
-
325
- document.addEventListener('mouseup', function() {
326
- drag.active = false;
327
- container.classList.remove('dragging');
328
- });
329
-
330
- // Touch events for pan/zoom
331
- var pinch = null;
332
- var touch1 = null;
333
- var hasMoved = false;
334
- var DRAG_THRESHOLD = 8;
335
-
336
- function getTouchDist(t) {
337
- if (t.length < 2) return 0;
338
- return Math.hypot(t[1].clientX - t[0].clientX, t[1].clientY - t[0].clientY);
339
- }
340
-
341
- function getTouchCenter(t) {
342
- return { x: (t[0].clientX + t[1].clientX) / 2, y: (t[0].clientY + t[1].clientY) / 2 };
343
- }
344
-
345
- container.addEventListener('touchstart', function(e) {
346
- // Skip if touching branding link
347
- if (e.target.closest && e.target.closest('.branding')) return;
348
-
349
- if (e.touches.length === 1) {
350
- // Single finger - potential pan or tap
351
- touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY, vx: vb.x, vy: vb.y };
352
- hasMoved = false;
353
- } else if (e.touches.length >= 2) {
354
- // Two fingers - pinch zoom
355
- e.preventDefault();
356
- touch1 = null;
357
- hasMoved = true;
358
- var dist = getTouchDist(e.touches);
359
- var center = getTouchCenter(e.touches);
360
- var rect = container.getBoundingClientRect();
361
- pinch = {
362
- dist: dist,
363
- vb: { x: vb.x, y: vb.y, w: vb.w, h: vb.h },
364
- cx: vb.x + vb.w * ((center.x - rect.left) / rect.width),
365
- cy: vb.y + vb.h * ((center.y - rect.top) / rect.height),
366
- lastCenter: center
367
- };
368
- }
369
- }, { passive: false });
370
-
371
- container.addEventListener('touchmove', function(e) {
372
- // Skip if touching branding link
373
- if (e.target.closest && e.target.closest('.branding')) return;
374
-
375
- if (e.touches.length === 1 && touch1) {
376
- var dx = e.touches[0].clientX - touch1.x;
377
- var dy = e.touches[0].clientY - touch1.y;
378
-
379
- // Check if moved beyond threshold
380
- if (!hasMoved && Math.hypot(dx, dy) > DRAG_THRESHOLD) {
381
- hasMoved = true;
382
- }
383
-
384
- if (hasMoved) {
385
- e.preventDefault();
386
- var sx = vb.w / container.clientWidth;
387
- var sy = vb.h / container.clientHeight;
388
- vb.x = touch1.vx - dx * sx;
389
- vb.y = touch1.vy - dy * sy;
390
- updateViewBox();
391
- }
392
- } else if (e.touches.length >= 2 && pinch) {
393
- e.preventDefault();
394
- var dist = getTouchDist(e.touches);
395
- var center = getTouchCenter(e.touches);
396
- if (dist === 0 || pinch.dist === 0) return;
397
-
398
- var scale = dist / pinch.dist;
399
- var nw = pinch.vb.w / scale;
400
- var nh = pinch.vb.h / scale;
401
- var newScale = origVb.w / nw;
402
- if (newScale < 0.1 || newScale > 10) return;
403
-
404
- var rect = container.getBoundingClientRect();
405
- var sx = nw / rect.width;
406
- var sy = nh / rect.height;
407
- var panX = (center.x - pinch.lastCenter.x) * sx;
408
- var panY = (center.y - pinch.lastCenter.y) * sy;
409
-
410
- var mx = (center.x - rect.left) / rect.width;
411
- var my = (center.y - rect.top) / rect.height;
412
- vb.x = pinch.cx - nw * mx - panX;
413
- vb.y = pinch.cy - nh * my - panY;
414
- vb.w = nw;
415
- vb.h = nh;
416
- updateViewBox();
417
- }
418
- }, { passive: false });
419
-
420
- container.addEventListener('touchend', function(e) {
421
- if (e.touches.length === 0) {
422
- touch1 = null;
423
- pinch = null;
424
- hasMoved = false;
425
- } else if (e.touches.length === 1) {
426
- pinch = null;
427
- touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY, vx: vb.x, vy: vb.y };
428
- hasMoved = true; // Already moving
429
- }
430
- });
431
-
432
- container.addEventListener('touchcancel', function() {
433
- touch1 = null;
434
- pinch = null;
435
- hasMoved = false;
436
- });
437
-
438
- // Listen for hierarchical navigation events
439
- document.addEventListener('shumoku:navigate', function(e) {
440
- var sheetId = e.detail && e.detail.sheetId;
441
- if (sheetId) {
442
- console.log('[Shumoku] Navigate to sheet:', sheetId);
443
- alert('Navigate to: ' + sheetId + '\\n\\nThis sheet would show the detailed view of this subgraph.');
444
- }
445
- });
446
-
447
- init();
448
- })();
449
- </script>
450
- <script>${navScript}</script>
451
- </body>
187
+ return `<!DOCTYPE html>
188
+ <html>
189
+ <head>
190
+ <meta charset="UTF-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
+ <title>${escapeHtml(title)}</title>
193
+ <style>
194
+ * { margin: 0; padding: 0; box-sizing: border-box; }
195
+ body { background: #f5f5f5; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; }
196
+ .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; background: white; border-bottom: 1px solid #e5e5e5; }
197
+ .toolbar-title { font-size: 14px; color: #666; }
198
+ .toolbar-buttons { display: flex; gap: 4px; align-items: center; }
199
+ .toolbar button { padding: 6px; border: none; background: none; cursor: pointer; border-radius: 4px; color: #666; }
200
+ .toolbar button:hover { background: #f0f0f0; }
201
+ .zoom-text { min-width: 50px; text-align: center; font-size: 13px; color: #666; }
202
+ .container { position: relative; width: 100%; height: ${containerHeight}; overflow: hidden; cursor: grab; background: repeating-conic-gradient(#f8f8f8 0% 25%, transparent 0% 50%) 50% / 20px 20px; }
203
+ .container.dragging { cursor: grabbing; }
204
+ .container > svg { width: 100%; height: 100%; }
205
+ .branding { position: absolute; bottom: 16px; right: 16px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(255,255,255,0.95); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; font-size: 13px; font-family: system-ui, sans-serif; color: #555; text-decoration: none; transition: all 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 100; }
206
+ .branding:hover { color: #222; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
207
+ .branding-icon { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
208
+ /* SVG interactive styles */
209
+ .node { cursor: pointer; }
210
+ .node:hover rect, .node:hover circle, .node:hover polygon { filter: brightness(0.95); }
211
+ .port { cursor: pointer; }
212
+ .link-hit-area { cursor: pointer; }
213
+ /* Subgraph click for hierarchical navigation */
214
+ .subgraph[data-has-sheet] { cursor: pointer; }
215
+ .subgraph[data-has-sheet]:hover > rect { filter: brightness(0.95); }
216
+ ${navStyles}
217
+ </style>
218
+ </head>
219
+ <body>
220
+ ${toolbarHtml}
221
+ ${navToolbarHtml}
222
+ <div class="container" id="container">
223
+ ${svg}
224
+ ${brandingHtml}
225
+ </div>
226
+ <script>${INTERACTIVE_IIFE}</script>
227
+ <script>
228
+ (function() {
229
+ var svg = document.querySelector('#container > svg');
230
+ var container = document.getElementById('container');
231
+ if (!svg || !container) { console.error('SVG or container not found'); return; }
232
+
233
+ var vb = { x: 0, y: 0, w: 0, h: 0 };
234
+ var origVb = { x: 0, y: 0, w: 0, h: 0 };
235
+ var drag = { active: false, x: 0, y: 0, vx: 0, vy: 0 };
236
+
237
+ function init() {
238
+ var w = parseFloat(svg.getAttribute('width')) || 800;
239
+ var h = parseFloat(svg.getAttribute('height')) || 600;
240
+ var existing = svg.getAttribute('viewBox');
241
+ if (existing) {
242
+ var p = existing.split(/\\s+|,/).map(Number);
243
+ origVb = { x: p[0] || 0, y: p[1] || 0, w: p[2] || w, h: p[3] || h };
244
+ } else {
245
+ origVb = { x: 0, y: 0, w: w, h: h };
246
+ }
247
+ svg.removeAttribute('width');
248
+ svg.removeAttribute('height');
249
+ svg.style.width = '100%';
250
+ svg.style.height = '100%';
251
+ fitView();
252
+
253
+ if (window.ShumokuInteractive) {
254
+ window.ShumokuInteractive.initInteractive({ target: svg, modal: { enabled: true }, tooltip: { enabled: true }, panZoom: { enabled: false } });
255
+ }
256
+ }
257
+
258
+ function updateViewBox() {
259
+ svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
260
+ var zoomEl = document.getElementById('zoom');
261
+ if (zoomEl) zoomEl.textContent = Math.round(origVb.w / vb.w * 100) + '%';
262
+ }
263
+
264
+ function fitView() {
265
+ var cw = container.clientWidth || 800;
266
+ var ch = container.clientHeight || 600;
267
+ var scale = Math.min(cw / origVb.w, ch / origVb.h) * 0.9;
268
+ vb.w = cw / scale;
269
+ vb.h = ch / scale;
270
+ vb.x = origVb.x + (origVb.w - vb.w) / 2;
271
+ vb.y = origVb.y + (origVb.h - vb.h) / 2;
272
+ updateViewBox();
273
+ }
274
+
275
+ function resetView() {
276
+ vb.x = origVb.x; vb.y = origVb.y; vb.w = origVb.w; vb.h = origVb.h;
277
+ updateViewBox();
278
+ }
279
+
280
+ function zoom(f) {
281
+ var cx = vb.x + vb.w / 2, cy = vb.y + vb.h / 2;
282
+ var nw = vb.w / f, nh = vb.h / f;
283
+ var scale = origVb.w / nw;
284
+ if (scale < 0.1 || scale > 10) return;
285
+ vb.w = nw; vb.h = nh; vb.x = cx - nw / 2; vb.y = cy - nh / 2;
286
+ updateViewBox();
287
+ }
288
+
289
+ var btnIn = document.getElementById('btn-in');
290
+ var btnOut = document.getElementById('btn-out');
291
+ var btnFit = document.getElementById('btn-fit');
292
+ var btnReset = document.getElementById('btn-reset');
293
+ if (btnIn) btnIn.addEventListener('click', function() { zoom(1.2); });
294
+ if (btnOut) btnOut.addEventListener('click', function() { zoom(1/1.2); });
295
+ if (btnFit) btnFit.addEventListener('click', fitView);
296
+ if (btnReset) btnReset.addEventListener('click', resetView);
297
+
298
+ container.addEventListener('wheel', function(e) {
299
+ e.preventDefault();
300
+ var rect = container.getBoundingClientRect();
301
+ var mx = (e.clientX - rect.left) / rect.width;
302
+ var my = (e.clientY - rect.top) / rect.height;
303
+ var px = vb.x + vb.w * mx, py = vb.y + vb.h * my;
304
+ var f = e.deltaY > 0 ? 1/1.2 : 1.2;
305
+ var nw = vb.w / f, nh = vb.h / f;
306
+ var scale = origVb.w / nw;
307
+ if (scale < 0.1 || scale > 10) return;
308
+ vb.w = nw; vb.h = nh; vb.x = px - nw * mx; vb.y = py - nh * my;
309
+ updateViewBox();
310
+ }, { passive: false });
311
+
312
+ container.addEventListener('mousedown', function(e) {
313
+ if (e.button === 0) {
314
+ drag = { active: true, x: e.clientX, y: e.clientY, vx: vb.x, vy: vb.y };
315
+ container.classList.add('dragging');
316
+ }
317
+ });
318
+
319
+ document.addEventListener('mousemove', function(e) {
320
+ if (!drag.active) return;
321
+ var sx = vb.w / container.clientWidth;
322
+ var sy = vb.h / container.clientHeight;
323
+ vb.x = drag.vx - (e.clientX - drag.x) * sx;
324
+ vb.y = drag.vy - (e.clientY - drag.y) * sy;
325
+ updateViewBox();
326
+ });
327
+
328
+ document.addEventListener('mouseup', function() {
329
+ drag.active = false;
330
+ container.classList.remove('dragging');
331
+ });
332
+
333
+ // Touch events for pan/zoom
334
+ var pinch = null;
335
+ var touch1 = null;
336
+ var hasMoved = false;
337
+ var DRAG_THRESHOLD = 8;
338
+
339
+ function getTouchDist(t) {
340
+ if (t.length < 2) return 0;
341
+ return Math.hypot(t[1].clientX - t[0].clientX, t[1].clientY - t[0].clientY);
342
+ }
343
+
344
+ function getTouchCenter(t) {
345
+ return { x: (t[0].clientX + t[1].clientX) / 2, y: (t[0].clientY + t[1].clientY) / 2 };
346
+ }
347
+
348
+ container.addEventListener('touchstart', function(e) {
349
+ // Skip if touching branding link
350
+ if (e.target.closest && e.target.closest('.branding')) return;
351
+
352
+ if (e.touches.length === 1) {
353
+ // Single finger - potential pan or tap
354
+ touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY, vx: vb.x, vy: vb.y };
355
+ hasMoved = false;
356
+ } else if (e.touches.length >= 2) {
357
+ // Two fingers - pinch zoom
358
+ e.preventDefault();
359
+ touch1 = null;
360
+ hasMoved = true;
361
+ var dist = getTouchDist(e.touches);
362
+ var center = getTouchCenter(e.touches);
363
+ var rect = container.getBoundingClientRect();
364
+ pinch = {
365
+ dist: dist,
366
+ vb: { x: vb.x, y: vb.y, w: vb.w, h: vb.h },
367
+ cx: vb.x + vb.w * ((center.x - rect.left) / rect.width),
368
+ cy: vb.y + vb.h * ((center.y - rect.top) / rect.height),
369
+ lastCenter: center
370
+ };
371
+ }
372
+ }, { passive: false });
373
+
374
+ container.addEventListener('touchmove', function(e) {
375
+ // Skip if touching branding link
376
+ if (e.target.closest && e.target.closest('.branding')) return;
377
+
378
+ if (e.touches.length === 1 && touch1) {
379
+ var dx = e.touches[0].clientX - touch1.x;
380
+ var dy = e.touches[0].clientY - touch1.y;
381
+
382
+ // Check if moved beyond threshold
383
+ if (!hasMoved && Math.hypot(dx, dy) > DRAG_THRESHOLD) {
384
+ hasMoved = true;
385
+ }
386
+
387
+ if (hasMoved) {
388
+ e.preventDefault();
389
+ var sx = vb.w / container.clientWidth;
390
+ var sy = vb.h / container.clientHeight;
391
+ vb.x = touch1.vx - dx * sx;
392
+ vb.y = touch1.vy - dy * sy;
393
+ updateViewBox();
394
+ }
395
+ } else if (e.touches.length >= 2 && pinch) {
396
+ e.preventDefault();
397
+ var dist = getTouchDist(e.touches);
398
+ var center = getTouchCenter(e.touches);
399
+ if (dist === 0 || pinch.dist === 0) return;
400
+
401
+ var scale = dist / pinch.dist;
402
+ var nw = pinch.vb.w / scale;
403
+ var nh = pinch.vb.h / scale;
404
+ var newScale = origVb.w / nw;
405
+ if (newScale < 0.1 || newScale > 10) return;
406
+
407
+ var rect = container.getBoundingClientRect();
408
+ var sx = nw / rect.width;
409
+ var sy = nh / rect.height;
410
+ var panX = (center.x - pinch.lastCenter.x) * sx;
411
+ var panY = (center.y - pinch.lastCenter.y) * sy;
412
+
413
+ var mx = (center.x - rect.left) / rect.width;
414
+ var my = (center.y - rect.top) / rect.height;
415
+ vb.x = pinch.cx - nw * mx - panX;
416
+ vb.y = pinch.cy - nh * my - panY;
417
+ vb.w = nw;
418
+ vb.h = nh;
419
+ updateViewBox();
420
+ }
421
+ }, { passive: false });
422
+
423
+ container.addEventListener('touchend', function(e) {
424
+ if (e.touches.length === 0) {
425
+ touch1 = null;
426
+ pinch = null;
427
+ hasMoved = false;
428
+ } else if (e.touches.length === 1) {
429
+ pinch = null;
430
+ touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY, vx: vb.x, vy: vb.y };
431
+ hasMoved = true; // Already moving
432
+ }
433
+ });
434
+
435
+ container.addEventListener('touchcancel', function() {
436
+ touch1 = null;
437
+ pinch = null;
438
+ hasMoved = false;
439
+ });
440
+
441
+ // Listen for hierarchical navigation events
442
+ document.addEventListener('shumoku:navigate', function(e) {
443
+ var sheetId = e.detail && e.detail.sheetId;
444
+ if (sheetId) {
445
+ console.log('[Shumoku] Navigate to sheet:', sheetId);
446
+ alert('Navigate to: ' + sheetId + '\\n\\nThis sheet would show the detailed view of this subgraph.');
447
+ }
448
+ });
449
+
450
+ init();
451
+ })();
452
+ </script>
453
+ <script>${navScript}</script>
454
+ </body>
452
455
  </html>`;
453
456
  }
454
457
  function escapeHtml(str) {
@@ -464,21 +467,21 @@ function escapeHtml(str) {
464
467
  */
465
468
  function generateHierarchicalHtml(sheetSvgs, title, options) {
466
469
  const brandingHtml = options.branding
467
- ? `<a class="branding" href="https://shumoku.packof.me" target="_blank" rel="noopener">
468
- <svg class="branding-icon" viewBox="0 0 1024 1024" fill="none"><rect x="64" y="64" width="896" height="896" rx="200" fill="#7FE4C1"/><g transform="translate(90,40) scale(1.25)"><path fill="#1F2328" d="M380 340H450V505H700V555H510V645H450V645H380Z"/></g></svg>
469
- <span>Made with Shumoku</span>
470
+ ? `<a class="branding" href="https://shumoku.packof.me" target="_blank" rel="noopener">
471
+ <svg class="branding-icon" viewBox="0 0 1024 1024" fill="none"><rect x="64" y="64" width="896" height="896" rx="200" fill="#7FE4C1"/><g transform="translate(90,40) scale(1.25)"><path fill="#1F2328" d="M380 340H450V505H700V555H510V645H450V645H380Z"/></g></svg>
472
+ <span>Made with Shumoku</span>
470
473
  </a>`
471
474
  : '';
472
475
  const toolbarHtml = options.toolbar
473
- ? `<div class="toolbar">
474
- <span class="toolbar-title" id="sheet-title">${escapeHtml(title)}</span>
475
- <div class="toolbar-buttons">
476
- <button id="btn-out" title="Zoom Out"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M20 12H4"/></svg></button>
477
- <span class="zoom-text" id="zoom">100%</span>
478
- <button id="btn-in" title="Zoom In"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg></button>
479
- <button id="btn-fit" title="Fit"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg></button>
480
- <button id="btn-reset" title="Reset"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg></button>
481
- </div>
476
+ ? `<div class="toolbar">
477
+ <span class="toolbar-title" id="sheet-title">${escapeHtml(title)}</span>
478
+ <div class="toolbar-buttons">
479
+ <button id="btn-out" title="Zoom Out"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M20 12H4"/></svg></button>
480
+ <span class="zoom-text" id="zoom">100%</span>
481
+ <button id="btn-in" title="Zoom In"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg></button>
482
+ <button id="btn-fit" title="Fit"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg></button>
483
+ <button id="btn-reset" title="Reset"><svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg></button>
484
+ </div>
482
485
  </div>`
483
486
  : '';
484
487
  // Build sheet containers
@@ -486,8 +489,8 @@ function generateHierarchicalHtml(sheetSvgs, title, options) {
486
489
  for (const [sheetId, svg] of sheetSvgs) {
487
490
  const isRoot = sheetId === 'root';
488
491
  const display = isRoot ? 'block' : 'none';
489
- sheetContainers.push(`<div class="sheet-container" data-sheet-id="${escapeHtml(sheetId)}" style="display: ${display};">
490
- ${svg}
492
+ sheetContainers.push(`<div class="sheet-container" data-sheet-id="${escapeHtml(sheetId)}" style="display: ${display};">
493
+ ${svg}
491
494
  </div>`);
492
495
  }
493
496
  // Build sheet info JSON for JavaScript
@@ -497,298 +500,298 @@ function generateHierarchicalHtml(sheetSvgs, title, options) {
497
500
  }
498
501
  const headerHeight = options.toolbar ? 45 : 0;
499
502
  const containerHeight = headerHeight > 0 ? `calc(100vh - ${headerHeight}px)` : '100vh';
500
- return `<!DOCTYPE html>
501
- <html>
502
- <head>
503
- <meta charset="UTF-8">
504
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
505
- <title>${escapeHtml(title)}</title>
506
- <style>
507
- * { margin: 0; padding: 0; box-sizing: border-box; }
508
- body { background: #f5f5f5; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; }
509
- .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; background: white; border-bottom: 1px solid #e5e5e5; }
510
- .toolbar-title { font-size: 14px; color: #666; display: flex; align-items: center; gap: 8px; }
511
- .toolbar-buttons { display: flex; gap: 4px; align-items: center; }
512
- .toolbar button { padding: 6px; border: none; background: none; cursor: pointer; border-radius: 4px; color: #666; }
513
- .toolbar button:hover { background: #f0f0f0; }
514
- .zoom-text { min-width: 50px; text-align: center; font-size: 13px; color: #666; }
515
- .back-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; font-size: 13px; color: #555; }
516
- .back-btn:hover { background: #f5f5f5; }
517
- .back-btn svg { width: 14px; height: 14px; }
518
- .container { position: relative; width: 100%; height: ${containerHeight}; overflow: hidden; cursor: grab; background: repeating-conic-gradient(#f8f8f8 0% 25%, transparent 0% 50%) 50% / 20px 20px; }
519
- .container.dragging { cursor: grabbing; }
520
- .sheet-container { width: 100%; height: 100%; }
521
- .sheet-container > svg { width: 100%; height: 100%; }
522
- .branding { position: absolute; bottom: 16px; right: 16px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(255,255,255,0.95); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; font-size: 13px; font-family: system-ui, sans-serif; color: #555; text-decoration: none; transition: all 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 100; }
523
- .branding:hover { color: #222; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
524
- .branding-icon { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
525
- .node { cursor: pointer; }
526
- .node:hover rect, .node:hover circle, .node:hover polygon { filter: brightness(0.95); }
527
- .port { cursor: pointer; }
528
- .link-hit-area { cursor: pointer; }
529
- .subgraph[data-has-sheet] { cursor: pointer; }
530
- .subgraph[data-has-sheet]:hover > rect { filter: brightness(0.95); }
531
- </style>
532
- </head>
533
- <body>
534
- ${toolbarHtml}
535
- <div class="container" id="container">
536
- ${sheetContainers.join('\n ')}
537
- ${brandingHtml}
538
- </div>
539
- <script>${INTERACTIVE_IIFE}</script>
540
- <script>
541
- (function() {
542
- var sheetInfo = ${JSON.stringify(sheetInfoJson)};
543
- var currentSheet = 'root';
544
- var breadcrumb = ['root'];
545
- var sheetViewBoxes = {};
546
- var container = document.getElementById('container');
547
-
548
- function getActiveSheet() {
549
- return container.querySelector('.sheet-container[data-sheet-id="' + currentSheet + '"]');
550
- }
551
-
552
- function getActiveSvg() {
553
- var sheet = getActiveSheet();
554
- return sheet ? sheet.querySelector('svg') : null;
555
- }
556
-
557
- function initSheet(sheetId) {
558
- var sheet = container.querySelector('.sheet-container[data-sheet-id="' + sheetId + '"]');
559
- if (!sheet) return;
560
- var svg = sheet.querySelector('svg');
561
- if (!svg) return;
562
-
563
- var w = parseFloat(svg.getAttribute('width')) || 800;
564
- var h = parseFloat(svg.getAttribute('height')) || 600;
565
- var existing = svg.getAttribute('viewBox');
566
- var vb;
567
- if (existing) {
568
- var p = existing.split(/\\s+|,/).map(Number);
569
- vb = { x: p[0] || 0, y: p[1] || 0, w: p[2] || w, h: p[3] || h, origX: p[0] || 0, origY: p[1] || 0, origW: p[2] || w, origH: p[3] || h };
570
- } else {
571
- vb = { x: 0, y: 0, w: w, h: h, origX: 0, origY: 0, origW: w, origH: h };
572
- }
573
- sheetViewBoxes[sheetId] = vb;
574
-
575
- svg.removeAttribute('width');
576
- svg.removeAttribute('height');
577
- svg.style.width = '100%';
578
- svg.style.height = '100%';
579
-
580
- // Fit view
581
- var cw = container.clientWidth || 800;
582
- var ch = container.clientHeight || 600;
583
- var scale = Math.min(cw / vb.origW, ch / vb.origH) * 0.9;
584
- vb.w = cw / scale;
585
- vb.h = ch / scale;
586
- vb.x = vb.origX + (vb.origW - vb.w) / 2;
587
- vb.y = vb.origY + (vb.origH - vb.h) / 2;
588
- svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
589
-
590
- if (window.ShumokuInteractive) {
591
- window.ShumokuInteractive.initInteractive({ target: svg, modal: { enabled: true }, tooltip: { enabled: true }, panZoom: { enabled: false } });
592
- }
593
- }
594
-
595
- function updateViewBox() {
596
- var svg = getActiveSvg();
597
- var vb = sheetViewBoxes[currentSheet];
598
- if (!svg || !vb) return;
599
- svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
600
- var zoomEl = document.getElementById('zoom');
601
- if (zoomEl) zoomEl.textContent = Math.round(vb.origW / vb.w * 100) + '%';
602
- }
603
-
604
- function updateTitle() {
605
- var titleEl = document.getElementById('sheet-title');
606
- if (!titleEl) return;
607
- var info = sheetInfo[currentSheet];
608
- var label = info ? info.label : currentSheet;
609
-
610
- if (currentSheet === 'root') {
611
- titleEl.innerHTML = label;
612
- } else {
613
- titleEl.innerHTML = '<button class="back-btn" id="back-btn"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg></button>' + label;
614
- document.getElementById('back-btn').addEventListener('click', function() {
615
- navigateToSheet('root');
616
- });
617
- }
618
- }
619
-
620
- function navigateToSheet(sheetId) {
621
- if (sheetId === currentSheet) return;
622
- if (!container.querySelector('.sheet-container[data-sheet-id="' + sheetId + '"]')) {
623
- console.warn('[Shumoku] Sheet not found:', sheetId);
624
- return;
625
- }
626
-
627
- // Hide current
628
- var current = getActiveSheet();
629
- if (current) current.style.display = 'none';
630
-
631
- // Show new
632
- currentSheet = sheetId;
633
- var newSheet = getActiveSheet();
634
- if (newSheet) {
635
- newSheet.style.display = 'block';
636
- if (!sheetViewBoxes[sheetId]) {
637
- initSheet(sheetId);
638
- }
639
- }
640
-
641
- // Update breadcrumb
642
- if (sheetId === 'root') {
643
- breadcrumb = ['root'];
644
- } else {
645
- breadcrumb = ['root', sheetId];
646
- }
647
-
648
- updateTitle();
649
- updateViewBox();
650
- }
651
-
652
- // Zoom functions
653
- function zoom(f) {
654
- var vb = sheetViewBoxes[currentSheet];
655
- if (!vb) return;
656
- var cx = vb.x + vb.w / 2, cy = vb.y + vb.h / 2;
657
- var nw = vb.w / f, nh = vb.h / f;
658
- var scale = vb.origW / nw;
659
- if (scale < 0.1 || scale > 10) return;
660
- vb.w = nw; vb.h = nh; vb.x = cx - nw / 2; vb.y = cy - nh / 2;
661
- updateViewBox();
662
- }
663
-
664
- function fitView() {
665
- var vb = sheetViewBoxes[currentSheet];
666
- if (!vb) return;
667
- var cw = container.clientWidth || 800;
668
- var ch = container.clientHeight || 600;
669
- var scale = Math.min(cw / vb.origW, ch / vb.origH) * 0.9;
670
- vb.w = cw / scale;
671
- vb.h = ch / scale;
672
- vb.x = vb.origX + (vb.origW - vb.w) / 2;
673
- vb.y = vb.origY + (vb.origH - vb.h) / 2;
674
- updateViewBox();
675
- }
676
-
677
- function resetView() {
678
- var vb = sheetViewBoxes[currentSheet];
679
- if (!vb) return;
680
- vb.x = vb.origX; vb.y = vb.origY; vb.w = vb.origW; vb.h = vb.origH;
681
- updateViewBox();
682
- }
683
-
684
- // Toolbar buttons
685
- var btnIn = document.getElementById('btn-in');
686
- var btnOut = document.getElementById('btn-out');
687
- var btnFit = document.getElementById('btn-fit');
688
- var btnReset = document.getElementById('btn-reset');
689
- if (btnIn) btnIn.addEventListener('click', function() { zoom(1.2); });
690
- if (btnOut) btnOut.addEventListener('click', function() { zoom(1/1.2); });
691
- if (btnFit) btnFit.addEventListener('click', fitView);
692
- if (btnReset) btnReset.addEventListener('click', resetView);
693
-
694
- // Mouse drag
695
- var drag = { active: false, x: 0, y: 0 };
696
-
697
- container.addEventListener('mousedown', function(e) {
698
- if (e.button === 0) {
699
- var vb = sheetViewBoxes[currentSheet];
700
- if (!vb) return;
701
- drag = { active: true, x: e.clientX, y: e.clientY, vx: vb.x, vy: vb.y };
702
- container.classList.add('dragging');
703
- }
704
- });
705
-
706
- document.addEventListener('mousemove', function(e) {
707
- if (!drag.active) return;
708
- var vb = sheetViewBoxes[currentSheet];
709
- if (!vb) return;
710
- var sx = vb.w / container.clientWidth;
711
- var sy = vb.h / container.clientHeight;
712
- vb.x = drag.vx - (e.clientX - drag.x) * sx;
713
- vb.y = drag.vy - (e.clientY - drag.y) * sy;
714
- updateViewBox();
715
- });
716
-
717
- document.addEventListener('mouseup', function() {
718
- drag.active = false;
719
- container.classList.remove('dragging');
720
- });
721
-
722
- // Wheel zoom
723
- container.addEventListener('wheel', function(e) {
724
- e.preventDefault();
725
- var vb = sheetViewBoxes[currentSheet];
726
- if (!vb) return;
727
- var rect = container.getBoundingClientRect();
728
- var mx = (e.clientX - rect.left) / rect.width;
729
- var my = (e.clientY - rect.top) / rect.height;
730
- var px = vb.x + vb.w * mx, py = vb.y + vb.h * my;
731
- var f = e.deltaY > 0 ? 1/1.2 : 1.2;
732
- var nw = vb.w / f, nh = vb.h / f;
733
- var scale = vb.origW / nw;
734
- if (scale < 0.1 || scale > 10) return;
735
- vb.w = nw; vb.h = nh; vb.x = px - nw * mx; vb.y = py - nh * my;
736
- updateViewBox();
737
- }, { passive: false });
738
-
739
- // Navigation event listener
740
- document.addEventListener('shumoku:navigate', function(e) {
741
- var sheetId = e.detail && e.detail.sheetId;
742
- if (sheetId) {
743
- navigateToSheet(sheetId);
744
- }
745
- });
746
-
747
- // Touch support (simplified)
748
- var touch1 = null;
749
- var hasMoved = false;
750
-
751
- container.addEventListener('touchstart', function(e) {
752
- if (e.target.closest && e.target.closest('.branding')) return;
753
- if (e.touches.length === 1) {
754
- var vb = sheetViewBoxes[currentSheet];
755
- if (vb) {
756
- touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY, vx: vb.x, vy: vb.y };
757
- hasMoved = false;
758
- }
759
- }
760
- }, { passive: false });
761
-
762
- container.addEventListener('touchmove', function(e) {
763
- if (e.target.closest && e.target.closest('.branding')) return;
764
- if (e.touches.length === 1 && touch1) {
765
- var vb = sheetViewBoxes[currentSheet];
766
- if (!vb) return;
767
- var dx = e.touches[0].clientX - touch1.x;
768
- var dy = e.touches[0].clientY - touch1.y;
769
- if (!hasMoved && Math.hypot(dx, dy) > 8) hasMoved = true;
770
- if (hasMoved) {
771
- e.preventDefault();
772
- var sx = vb.w / container.clientWidth;
773
- var sy = vb.h / container.clientHeight;
774
- vb.x = touch1.vx - dx * sx;
775
- vb.y = touch1.vy - dy * sy;
776
- updateViewBox();
777
- }
778
- }
779
- }, { passive: false });
780
-
781
- container.addEventListener('touchend', function() {
782
- touch1 = null;
783
- hasMoved = false;
784
- });
785
-
786
- // Initialize root sheet
787
- initSheet('root');
788
- updateTitle();
789
- })();
790
- </script>
791
- </body>
503
+ return `<!DOCTYPE html>
504
+ <html>
505
+ <head>
506
+ <meta charset="UTF-8">
507
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
508
+ <title>${escapeHtml(title)}</title>
509
+ <style>
510
+ * { margin: 0; padding: 0; box-sizing: border-box; }
511
+ body { background: #f5f5f5; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; }
512
+ .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; background: white; border-bottom: 1px solid #e5e5e5; }
513
+ .toolbar-title { font-size: 14px; color: #666; display: flex; align-items: center; gap: 8px; }
514
+ .toolbar-buttons { display: flex; gap: 4px; align-items: center; }
515
+ .toolbar button { padding: 6px; border: none; background: none; cursor: pointer; border-radius: 4px; color: #666; }
516
+ .toolbar button:hover { background: #f0f0f0; }
517
+ .zoom-text { min-width: 50px; text-align: center; font-size: 13px; color: #666; }
518
+ .back-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; font-size: 13px; color: #555; }
519
+ .back-btn:hover { background: #f5f5f5; }
520
+ .back-btn svg { width: 14px; height: 14px; }
521
+ .container { position: relative; width: 100%; height: ${containerHeight}; overflow: hidden; cursor: grab; background: repeating-conic-gradient(#f8f8f8 0% 25%, transparent 0% 50%) 50% / 20px 20px; }
522
+ .container.dragging { cursor: grabbing; }
523
+ .sheet-container { width: 100%; height: 100%; }
524
+ .sheet-container > svg { width: 100%; height: 100%; }
525
+ .branding { position: absolute; bottom: 16px; right: 16px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(255,255,255,0.95); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; font-size: 13px; font-family: system-ui, sans-serif; color: #555; text-decoration: none; transition: all 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 100; }
526
+ .branding:hover { color: #222; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
527
+ .branding-icon { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
528
+ .node { cursor: pointer; }
529
+ .node:hover rect, .node:hover circle, .node:hover polygon { filter: brightness(0.95); }
530
+ .port { cursor: pointer; }
531
+ .link-hit-area { cursor: pointer; }
532
+ .subgraph[data-has-sheet] { cursor: pointer; }
533
+ .subgraph[data-has-sheet]:hover > rect { filter: brightness(0.95); }
534
+ </style>
535
+ </head>
536
+ <body>
537
+ ${toolbarHtml}
538
+ <div class="container" id="container">
539
+ ${sheetContainers.join('\n ')}
540
+ ${brandingHtml}
541
+ </div>
542
+ <script>${INTERACTIVE_IIFE}</script>
543
+ <script>
544
+ (function() {
545
+ var sheetInfo = ${JSON.stringify(sheetInfoJson)};
546
+ var currentSheet = 'root';
547
+ var breadcrumb = ['root'];
548
+ var sheetViewBoxes = {};
549
+ var container = document.getElementById('container');
550
+
551
+ function getActiveSheet() {
552
+ return container.querySelector('.sheet-container[data-sheet-id="' + currentSheet + '"]');
553
+ }
554
+
555
+ function getActiveSvg() {
556
+ var sheet = getActiveSheet();
557
+ return sheet ? sheet.querySelector('svg') : null;
558
+ }
559
+
560
+ function initSheet(sheetId) {
561
+ var sheet = container.querySelector('.sheet-container[data-sheet-id="' + sheetId + '"]');
562
+ if (!sheet) return;
563
+ var svg = sheet.querySelector('svg');
564
+ if (!svg) return;
565
+
566
+ var w = parseFloat(svg.getAttribute('width')) || 800;
567
+ var h = parseFloat(svg.getAttribute('height')) || 600;
568
+ var existing = svg.getAttribute('viewBox');
569
+ var vb;
570
+ if (existing) {
571
+ var p = existing.split(/\\s+|,/).map(Number);
572
+ vb = { x: p[0] || 0, y: p[1] || 0, w: p[2] || w, h: p[3] || h, origX: p[0] || 0, origY: p[1] || 0, origW: p[2] || w, origH: p[3] || h };
573
+ } else {
574
+ vb = { x: 0, y: 0, w: w, h: h, origX: 0, origY: 0, origW: w, origH: h };
575
+ }
576
+ sheetViewBoxes[sheetId] = vb;
577
+
578
+ svg.removeAttribute('width');
579
+ svg.removeAttribute('height');
580
+ svg.style.width = '100%';
581
+ svg.style.height = '100%';
582
+
583
+ // Fit view
584
+ var cw = container.clientWidth || 800;
585
+ var ch = container.clientHeight || 600;
586
+ var scale = Math.min(cw / vb.origW, ch / vb.origH) * 0.9;
587
+ vb.w = cw / scale;
588
+ vb.h = ch / scale;
589
+ vb.x = vb.origX + (vb.origW - vb.w) / 2;
590
+ vb.y = vb.origY + (vb.origH - vb.h) / 2;
591
+ svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
592
+
593
+ if (window.ShumokuInteractive) {
594
+ window.ShumokuInteractive.initInteractive({ target: svg, modal: { enabled: true }, tooltip: { enabled: true }, panZoom: { enabled: false } });
595
+ }
596
+ }
597
+
598
+ function updateViewBox() {
599
+ var svg = getActiveSvg();
600
+ var vb = sheetViewBoxes[currentSheet];
601
+ if (!svg || !vb) return;
602
+ svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
603
+ var zoomEl = document.getElementById('zoom');
604
+ if (zoomEl) zoomEl.textContent = Math.round(vb.origW / vb.w * 100) + '%';
605
+ }
606
+
607
+ function updateTitle() {
608
+ var titleEl = document.getElementById('sheet-title');
609
+ if (!titleEl) return;
610
+ var info = sheetInfo[currentSheet];
611
+ var label = info ? info.label : currentSheet;
612
+
613
+ if (currentSheet === 'root') {
614
+ titleEl.innerHTML = label;
615
+ } else {
616
+ titleEl.innerHTML = '<button class="back-btn" id="back-btn"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg></button>' + label;
617
+ document.getElementById('back-btn').addEventListener('click', function() {
618
+ navigateToSheet('root');
619
+ });
620
+ }
621
+ }
622
+
623
+ function navigateToSheet(sheetId) {
624
+ if (sheetId === currentSheet) return;
625
+ if (!container.querySelector('.sheet-container[data-sheet-id="' + sheetId + '"]')) {
626
+ console.warn('[Shumoku] Sheet not found:', sheetId);
627
+ return;
628
+ }
629
+
630
+ // Hide current
631
+ var current = getActiveSheet();
632
+ if (current) current.style.display = 'none';
633
+
634
+ // Show new
635
+ currentSheet = sheetId;
636
+ var newSheet = getActiveSheet();
637
+ if (newSheet) {
638
+ newSheet.style.display = 'block';
639
+ if (!sheetViewBoxes[sheetId]) {
640
+ initSheet(sheetId);
641
+ }
642
+ }
643
+
644
+ // Update breadcrumb
645
+ if (sheetId === 'root') {
646
+ breadcrumb = ['root'];
647
+ } else {
648
+ breadcrumb = ['root', sheetId];
649
+ }
650
+
651
+ updateTitle();
652
+ updateViewBox();
653
+ }
654
+
655
+ // Zoom functions
656
+ function zoom(f) {
657
+ var vb = sheetViewBoxes[currentSheet];
658
+ if (!vb) return;
659
+ var cx = vb.x + vb.w / 2, cy = vb.y + vb.h / 2;
660
+ var nw = vb.w / f, nh = vb.h / f;
661
+ var scale = vb.origW / nw;
662
+ if (scale < 0.1 || scale > 10) return;
663
+ vb.w = nw; vb.h = nh; vb.x = cx - nw / 2; vb.y = cy - nh / 2;
664
+ updateViewBox();
665
+ }
666
+
667
+ function fitView() {
668
+ var vb = sheetViewBoxes[currentSheet];
669
+ if (!vb) return;
670
+ var cw = container.clientWidth || 800;
671
+ var ch = container.clientHeight || 600;
672
+ var scale = Math.min(cw / vb.origW, ch / vb.origH) * 0.9;
673
+ vb.w = cw / scale;
674
+ vb.h = ch / scale;
675
+ vb.x = vb.origX + (vb.origW - vb.w) / 2;
676
+ vb.y = vb.origY + (vb.origH - vb.h) / 2;
677
+ updateViewBox();
678
+ }
679
+
680
+ function resetView() {
681
+ var vb = sheetViewBoxes[currentSheet];
682
+ if (!vb) return;
683
+ vb.x = vb.origX; vb.y = vb.origY; vb.w = vb.origW; vb.h = vb.origH;
684
+ updateViewBox();
685
+ }
686
+
687
+ // Toolbar buttons
688
+ var btnIn = document.getElementById('btn-in');
689
+ var btnOut = document.getElementById('btn-out');
690
+ var btnFit = document.getElementById('btn-fit');
691
+ var btnReset = document.getElementById('btn-reset');
692
+ if (btnIn) btnIn.addEventListener('click', function() { zoom(1.2); });
693
+ if (btnOut) btnOut.addEventListener('click', function() { zoom(1/1.2); });
694
+ if (btnFit) btnFit.addEventListener('click', fitView);
695
+ if (btnReset) btnReset.addEventListener('click', resetView);
696
+
697
+ // Mouse drag
698
+ var drag = { active: false, x: 0, y: 0 };
699
+
700
+ container.addEventListener('mousedown', function(e) {
701
+ if (e.button === 0) {
702
+ var vb = sheetViewBoxes[currentSheet];
703
+ if (!vb) return;
704
+ drag = { active: true, x: e.clientX, y: e.clientY, vx: vb.x, vy: vb.y };
705
+ container.classList.add('dragging');
706
+ }
707
+ });
708
+
709
+ document.addEventListener('mousemove', function(e) {
710
+ if (!drag.active) return;
711
+ var vb = sheetViewBoxes[currentSheet];
712
+ if (!vb) return;
713
+ var sx = vb.w / container.clientWidth;
714
+ var sy = vb.h / container.clientHeight;
715
+ vb.x = drag.vx - (e.clientX - drag.x) * sx;
716
+ vb.y = drag.vy - (e.clientY - drag.y) * sy;
717
+ updateViewBox();
718
+ });
719
+
720
+ document.addEventListener('mouseup', function() {
721
+ drag.active = false;
722
+ container.classList.remove('dragging');
723
+ });
724
+
725
+ // Wheel zoom
726
+ container.addEventListener('wheel', function(e) {
727
+ e.preventDefault();
728
+ var vb = sheetViewBoxes[currentSheet];
729
+ if (!vb) return;
730
+ var rect = container.getBoundingClientRect();
731
+ var mx = (e.clientX - rect.left) / rect.width;
732
+ var my = (e.clientY - rect.top) / rect.height;
733
+ var px = vb.x + vb.w * mx, py = vb.y + vb.h * my;
734
+ var f = e.deltaY > 0 ? 1/1.2 : 1.2;
735
+ var nw = vb.w / f, nh = vb.h / f;
736
+ var scale = vb.origW / nw;
737
+ if (scale < 0.1 || scale > 10) return;
738
+ vb.w = nw; vb.h = nh; vb.x = px - nw * mx; vb.y = py - nh * my;
739
+ updateViewBox();
740
+ }, { passive: false });
741
+
742
+ // Navigation event listener
743
+ document.addEventListener('shumoku:navigate', function(e) {
744
+ var sheetId = e.detail && e.detail.sheetId;
745
+ if (sheetId) {
746
+ navigateToSheet(sheetId);
747
+ }
748
+ });
749
+
750
+ // Touch support (simplified)
751
+ var touch1 = null;
752
+ var hasMoved = false;
753
+
754
+ container.addEventListener('touchstart', function(e) {
755
+ if (e.target.closest && e.target.closest('.branding')) return;
756
+ if (e.touches.length === 1) {
757
+ var vb = sheetViewBoxes[currentSheet];
758
+ if (vb) {
759
+ touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY, vx: vb.x, vy: vb.y };
760
+ hasMoved = false;
761
+ }
762
+ }
763
+ }, { passive: false });
764
+
765
+ container.addEventListener('touchmove', function(e) {
766
+ if (e.target.closest && e.target.closest('.branding')) return;
767
+ if (e.touches.length === 1 && touch1) {
768
+ var vb = sheetViewBoxes[currentSheet];
769
+ if (!vb) return;
770
+ var dx = e.touches[0].clientX - touch1.x;
771
+ var dy = e.touches[0].clientY - touch1.y;
772
+ if (!hasMoved && Math.hypot(dx, dy) > 8) hasMoved = true;
773
+ if (hasMoved) {
774
+ e.preventDefault();
775
+ var sx = vb.w / container.clientWidth;
776
+ var sy = vb.h / container.clientHeight;
777
+ vb.x = touch1.vx - dx * sx;
778
+ vb.y = touch1.vy - dy * sy;
779
+ updateViewBox();
780
+ }
781
+ }
782
+ }, { passive: false });
783
+
784
+ container.addEventListener('touchend', function() {
785
+ touch1 = null;
786
+ hasMoved = false;
787
+ });
788
+
789
+ // Initialize root sheet
790
+ initSheet('root');
791
+ updateTitle();
792
+ })();
793
+ </script>
794
+ </body>
792
795
  </html>`;
793
796
  }
794
797
  //# sourceMappingURL=index.js.map