@shumoku/renderer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Interactive Runtime - Hover tooltip with touch support and pan/zoom
3
+ */
4
+
5
+ import type { InteractiveInstance, InteractiveOptions } from '../types.js'
6
+
7
+ let tooltip: HTMLDivElement | null = null
8
+ let overlay: HTMLDivElement | null = null
9
+ let currentHighlight: Element | null = null
10
+ let isTouchDevice = false
11
+
12
+ function getTooltip(): HTMLDivElement {
13
+ if (!tooltip) {
14
+ tooltip = document.createElement('div')
15
+ tooltip.style.cssText = `
16
+ position: fixed;
17
+ z-index: 10000;
18
+ padding: 6px 10px;
19
+ background: #1e293b;
20
+ color: #fff;
21
+ font-size: 12px;
22
+ border-radius: 4px;
23
+ pointer-events: none;
24
+ opacity: 0;
25
+ transition: opacity 0.15s;
26
+ font-family: system-ui, sans-serif;
27
+ max-width: 300px;
28
+ white-space: pre-line;
29
+ `
30
+ document.body.appendChild(tooltip)
31
+ }
32
+ return tooltip
33
+ }
34
+
35
+ function showTooltip(text: string, x: number, y: number): void {
36
+ const t = getTooltip()
37
+ t.textContent = text
38
+
39
+ // Position tooltip, keeping it within viewport
40
+ const pad = 12
41
+ let left = x + pad
42
+ let top = y + pad
43
+
44
+ // Adjust if tooltip would go off-screen
45
+ requestAnimationFrame(() => {
46
+ const rect = t.getBoundingClientRect()
47
+ if (left + rect.width > window.innerWidth - pad) {
48
+ left = x - rect.width - pad
49
+ }
50
+ if (top + rect.height > window.innerHeight - pad) {
51
+ top = y - rect.height - pad
52
+ }
53
+ t.style.left = `${Math.max(pad, left)}px`
54
+ t.style.top = `${Math.max(pad, top)}px`
55
+ })
56
+
57
+ t.style.left = `${left}px`
58
+ t.style.top = `${top}px`
59
+ t.style.opacity = '1'
60
+ }
61
+
62
+ function hideTooltip(): void {
63
+ if (tooltip) {
64
+ tooltip.style.opacity = '0'
65
+ }
66
+ }
67
+
68
+ function getOverlay(): HTMLDivElement {
69
+ if (!overlay) {
70
+ overlay = document.createElement('div')
71
+ overlay.style.cssText = `
72
+ position: fixed;
73
+ top: 0;
74
+ left: 0;
75
+ right: 0;
76
+ bottom: 0;
77
+ background: rgba(0, 0, 0, 0.5);
78
+ pointer-events: none;
79
+ opacity: 0;
80
+ transition: opacity 0.15s ease;
81
+ z-index: 9998;
82
+ `
83
+ document.body.appendChild(overlay)
84
+ }
85
+ return overlay
86
+ }
87
+
88
+ let highlightContainer: HTMLDivElement | null = null
89
+ let currentMiniSvg: SVGSVGElement | null = null
90
+
91
+ function getHighlightContainer(): HTMLDivElement {
92
+ if (!highlightContainer) {
93
+ highlightContainer = document.createElement('div')
94
+ highlightContainer.style.cssText = `
95
+ position: fixed;
96
+ top: 0;
97
+ left: 0;
98
+ pointer-events: none;
99
+ z-index: 9999;
100
+ `
101
+ document.body.appendChild(highlightContainer)
102
+ }
103
+ return highlightContainer
104
+ }
105
+
106
+ function updateHighlightPosition(): void {
107
+ if (!currentHighlight || !currentMiniSvg) return
108
+
109
+ // Check if element is still in DOM (React may have replaced it)
110
+ if (!document.contains(currentHighlight)) {
111
+ // Element was removed, clear highlight
112
+ highlightElement(null)
113
+ return
114
+ }
115
+
116
+ const svg = currentHighlight.closest('svg') as SVGSVGElement | null
117
+ if (!svg) return
118
+
119
+ // Use getBoundingClientRect for screen position
120
+ const rect = svg.getBoundingClientRect()
121
+
122
+ // Update viewBox to match current zoom level
123
+ const viewBox = svg.getAttribute('viewBox')
124
+ if (viewBox) {
125
+ currentMiniSvg.setAttribute('viewBox', viewBox)
126
+ }
127
+
128
+ currentMiniSvg.style.left = `${rect.left}px`
129
+ currentMiniSvg.style.top = `${rect.top}px`
130
+ currentMiniSvg.style.width = `${rect.width}px`
131
+ currentMiniSvg.style.height = `${rect.height}px`
132
+ }
133
+
134
+ function highlightElement(el: Element | null): void {
135
+ // Skip if already highlighting the same element
136
+ if (el === currentHighlight) {
137
+ return
138
+ }
139
+
140
+ // Remove previous highlight
141
+ if (currentHighlight) {
142
+ currentHighlight.classList.remove('shumoku-highlight')
143
+ }
144
+
145
+ // Clear previous mini SVG
146
+ const container = getHighlightContainer()
147
+ container.innerHTML = ''
148
+ currentMiniSvg = null
149
+
150
+ currentHighlight = el
151
+ const ov = getOverlay()
152
+
153
+ if (el) {
154
+ el.classList.add('shumoku-highlight')
155
+
156
+ const svg = el.closest('svg') as SVGSVGElement | null
157
+ if (svg) {
158
+ const viewBox = svg.getAttribute('viewBox')
159
+ if (viewBox) {
160
+ // Clone the highlighted element
161
+ const clone = el.cloneNode(true) as Element
162
+ clone.classList.remove('shumoku-highlight')
163
+
164
+ // Create mini SVG with same viewBox
165
+ const miniSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
166
+ miniSvg.setAttribute('viewBox', viewBox)
167
+ miniSvg.style.cssText = `
168
+ position: absolute;
169
+ overflow: visible;
170
+ filter: drop-shadow(0 0 4px #fff) drop-shadow(0 0 8px #fff) drop-shadow(0 0 12px rgba(100, 150, 255, 0.8));
171
+ `
172
+
173
+ // Copy defs (for gradients, patterns, etc.)
174
+ const defs = svg.querySelector('defs')
175
+ if (defs) {
176
+ miniSvg.appendChild(defs.cloneNode(true))
177
+ }
178
+
179
+ miniSvg.appendChild(clone)
180
+ container.appendChild(miniSvg)
181
+ currentMiniSvg = miniSvg
182
+
183
+ // Update position
184
+ updateHighlightPosition()
185
+ }
186
+ }
187
+
188
+ ov.style.opacity = '1'
189
+ } else {
190
+ ov.style.opacity = '0'
191
+ }
192
+ }
193
+
194
+ function injectHighlightStyles(): void {
195
+ // No styles needed for current approach
196
+ }
197
+
198
+ interface TooltipInfo {
199
+ text: string
200
+ element: Element
201
+ }
202
+
203
+ function getTooltipInfo(el: Element): TooltipInfo | null {
204
+ // Port tooltip (ports are now separate layer)
205
+ const port = el.closest('.port[data-port]')
206
+ if (port) {
207
+ const portId = port.getAttribute('data-port') || ''
208
+ const deviceId = port.getAttribute('data-port-device') || ''
209
+ return { text: `${deviceId}:${portId}`, element: port }
210
+ }
211
+
212
+ // Link tooltip
213
+ const linkGroup = el.closest('.link-group[data-link-id]')
214
+ if (linkGroup) {
215
+ const from = linkGroup.getAttribute('data-link-from') || ''
216
+ const to = linkGroup.getAttribute('data-link-to') || ''
217
+ const bw = linkGroup.getAttribute('data-link-bandwidth')
218
+ const vlan = linkGroup.getAttribute('data-link-vlan')
219
+
220
+ let text = `${from} ↔ ${to}`
221
+ if (bw) text += `\n${bw}`
222
+ if (vlan) text += `\nVLAN: ${vlan}`
223
+ return { text, element: linkGroup }
224
+ }
225
+
226
+ // Device tooltip (single label only)
227
+ const node = el.closest('.node[data-id]')
228
+ if (node) {
229
+ const json = node.getAttribute('data-device-json')
230
+ let text: string
231
+ if (json) {
232
+ try {
233
+ const data = JSON.parse(json)
234
+ text = data.label || data.id
235
+ } catch {
236
+ text = node.getAttribute('data-id') || ''
237
+ }
238
+ } else {
239
+ text = node.getAttribute('data-id') || ''
240
+ }
241
+ return { text, element: node }
242
+ }
243
+
244
+ return null
245
+ }
246
+
247
+ // ViewBox utilities
248
+ interface ViewBox {
249
+ x: number
250
+ y: number
251
+ width: number
252
+ height: number
253
+ }
254
+
255
+ function parseViewBox(svg: SVGSVGElement): ViewBox | null {
256
+ const vb = svg.getAttribute('viewBox')
257
+ if (!vb) return null
258
+ const parts = vb.split(/\s+|,/).map(Number)
259
+ if (parts.length !== 4 || parts.some(Number.isNaN)) return null
260
+ return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] }
261
+ }
262
+
263
+ function setViewBox(svg: SVGSVGElement, vb: ViewBox): void {
264
+ svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.width} ${vb.height}`)
265
+ }
266
+
267
+ export function initInteractive(options: InteractiveOptions): InteractiveInstance {
268
+ const target =
269
+ typeof options.target === 'string' ? document.querySelector(options.target) : options.target
270
+
271
+ if (!target) throw new Error('Target not found')
272
+
273
+ const svg = target.closest('svg') || target.querySelector('svg') || (target as SVGSVGElement)
274
+ if (!(svg instanceof SVGSVGElement)) throw new Error('SVG element not found')
275
+
276
+ // Inject highlight styles
277
+ injectHighlightStyles()
278
+
279
+ // Pan/Zoom settings
280
+ const panZoomEnabled = options.panZoom?.enabled ?? true
281
+ const minScale = options.panZoom?.minScale ?? 0.1
282
+ const maxScale = options.panZoom?.maxScale ?? 10
283
+ const ZOOM_FACTOR = 1.2
284
+
285
+ // Store original viewBox for reset and scale calculation
286
+ let originalViewBox: ViewBox | null = null
287
+ const initViewBox = () => {
288
+ originalViewBox = parseViewBox(svg)
289
+ if (!originalViewBox) {
290
+ // Fallback: use SVG bounding box
291
+ const bbox = svg.getBBox()
292
+ originalViewBox = { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }
293
+ setViewBox(svg, originalViewBox)
294
+ }
295
+ }
296
+ initViewBox()
297
+
298
+ // Calculate current scale
299
+ const getScale = (): number => {
300
+ if (!originalViewBox) return 1
301
+ const current = parseViewBox(svg)
302
+ if (!current) return 1
303
+ return originalViewBox.width / current.width
304
+ }
305
+
306
+ // Track if we're currently showing a touch tooltip
307
+ let touchTooltipActive = false
308
+
309
+ // Pan state
310
+ let isPanning = false
311
+ let panStartX = 0
312
+ let panStartY = 0
313
+ let panStartViewBox: ViewBox | null = null
314
+
315
+ // Pinch state
316
+ let initialPinchDistance = 0
317
+ let pinchStartViewBox: ViewBox | null = null
318
+ let pinchCenter: { x: number; y: number } | null = null
319
+
320
+ // Mouse move handler (desktop hover)
321
+ const handleMouseMove = (e: Event) => {
322
+ if (isPanning) return
323
+ if (isTouchDevice) return // Skip on touch devices
324
+
325
+ const me = e as MouseEvent
326
+ const info = getTooltipInfo(me.target as Element)
327
+ if (info) {
328
+ showTooltip(info.text, me.clientX, me.clientY)
329
+ highlightElement(info.element)
330
+ } else {
331
+ hideTooltip()
332
+ highlightElement(null)
333
+ }
334
+ }
335
+
336
+ // Mouse leave handler
337
+ const handleMouseLeave = () => {
338
+ if (isTouchDevice) return
339
+ if (isPanning) return
340
+ hideTooltip()
341
+ highlightElement(null)
342
+ }
343
+
344
+ // Mouse down - start pan
345
+ const handleMouseDown = (e: Event) => {
346
+ if (!panZoomEnabled) return
347
+ const me = e as MouseEvent
348
+ if (me.button !== 0) return // Left button only
349
+
350
+ // Don't start pan if clicking on interactive element
351
+ const info = getTooltipInfo(me.target as Element)
352
+ if (info) return
353
+
354
+ isPanning = true
355
+ panStartX = me.clientX
356
+ panStartY = me.clientY
357
+ panStartViewBox = parseViewBox(svg)
358
+ svg.style.cursor = 'grabbing'
359
+ e.preventDefault()
360
+ }
361
+
362
+ // Mouse move - pan
363
+ const handlePan = (e: Event) => {
364
+ if (!isPanning || !panStartViewBox) return
365
+ const me = e as MouseEvent
366
+
367
+ const rect = svg.getBoundingClientRect()
368
+ const dx = me.clientX - panStartX
369
+ const dy = me.clientY - panStartY
370
+
371
+ // Convert screen delta to viewBox delta
372
+ const scaleX = panStartViewBox.width / rect.width
373
+ const scaleY = panStartViewBox.height / rect.height
374
+
375
+ setViewBox(svg, {
376
+ x: panStartViewBox.x - dx * scaleX,
377
+ y: panStartViewBox.y - dy * scaleY,
378
+ width: panStartViewBox.width,
379
+ height: panStartViewBox.height,
380
+ })
381
+ }
382
+
383
+ // Mouse up - end pan
384
+ const handleMouseUp = () => {
385
+ if (isPanning) {
386
+ isPanning = false
387
+ panStartViewBox = null
388
+ svg.style.cursor = ''
389
+ }
390
+ }
391
+
392
+ // Wheel zoom
393
+ const handleWheel = (e: Event) => {
394
+ if (!panZoomEnabled) return
395
+ const we = e as WheelEvent
396
+ we.preventDefault()
397
+
398
+ const vb = parseViewBox(svg)
399
+ if (!vb || !originalViewBox) return
400
+
401
+ const rect = svg.getBoundingClientRect()
402
+ const mouseXRatio = (we.clientX - rect.left) / rect.width
403
+ const mouseYRatio = (we.clientY - rect.top) / rect.height
404
+
405
+ const mouseX = vb.x + vb.width * mouseXRatio
406
+ const mouseY = vb.y + vb.height * mouseYRatio
407
+
408
+ const zoomFactor = we.deltaY > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR
409
+ const newWidth = vb.width * zoomFactor
410
+ const newHeight = vb.height * zoomFactor
411
+
412
+ // Check scale limits
413
+ const newScale = originalViewBox.width / newWidth
414
+ if (newScale < minScale || newScale > maxScale) return
415
+
416
+ setViewBox(svg, {
417
+ x: mouseX - newWidth * mouseXRatio,
418
+ y: mouseY - newHeight * mouseYRatio,
419
+ width: newWidth,
420
+ height: newHeight,
421
+ })
422
+ }
423
+
424
+ // Touch start handler
425
+ const handleTouchStart = (e: Event) => {
426
+ isTouchDevice = true
427
+ const te = e as TouchEvent
428
+
429
+ if (te.touches.length === 1 && panZoomEnabled) {
430
+ // Single touch - check if on interactive element first
431
+ const touch = te.touches[0]
432
+ const targetEl = document.elementFromPoint(touch.clientX, touch.clientY)
433
+ const info = targetEl ? getTooltipInfo(targetEl) : null
434
+
435
+ if (!info) {
436
+ // Start pan
437
+ isPanning = true
438
+ panStartX = touch.clientX
439
+ panStartY = touch.clientY
440
+ panStartViewBox = parseViewBox(svg)
441
+ }
442
+ } else if (te.touches.length === 2 && panZoomEnabled) {
443
+ // Two touches - start pinch
444
+ isPanning = false
445
+ const t1 = te.touches[0]
446
+ const t2 = te.touches[1]
447
+ initialPinchDistance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
448
+ pinchStartViewBox = parseViewBox(svg)
449
+
450
+ // Calculate pinch center in viewBox coordinates
451
+ const rect = svg.getBoundingClientRect()
452
+ const centerX = (t1.clientX + t2.clientX) / 2
453
+ const centerY = (t1.clientY + t2.clientY) / 2
454
+ const vb = pinchStartViewBox
455
+ if (vb) {
456
+ const xRatio = (centerX - rect.left) / rect.width
457
+ const yRatio = (centerY - rect.top) / rect.height
458
+ pinchCenter = {
459
+ x: vb.x + vb.width * xRatio,
460
+ y: vb.y + vb.height * yRatio,
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ // Touch move handler
467
+ const handleTouchMove = (e: Event) => {
468
+ const te = e as TouchEvent
469
+
470
+ if (te.touches.length === 1 && isPanning && panStartViewBox) {
471
+ // Pan
472
+ const touch = te.touches[0]
473
+ const rect = svg.getBoundingClientRect()
474
+ const dx = touch.clientX - panStartX
475
+ const dy = touch.clientY - panStartY
476
+
477
+ const scaleX = panStartViewBox.width / rect.width
478
+ const scaleY = panStartViewBox.height / rect.height
479
+
480
+ setViewBox(svg, {
481
+ x: panStartViewBox.x - dx * scaleX,
482
+ y: panStartViewBox.y - dy * scaleY,
483
+ width: panStartViewBox.width,
484
+ height: panStartViewBox.height,
485
+ })
486
+
487
+ te.preventDefault()
488
+ } else if (te.touches.length === 2 && pinchStartViewBox && pinchCenter && originalViewBox) {
489
+ // Pinch zoom
490
+ const t1 = te.touches[0]
491
+ const t2 = te.touches[1]
492
+ const distance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
493
+ const scale = distance / initialPinchDistance
494
+
495
+ const newWidth = pinchStartViewBox.width / scale
496
+ const newHeight = pinchStartViewBox.height / scale
497
+
498
+ // Check scale limits
499
+ const newScale = originalViewBox.width / newWidth
500
+ if (newScale < minScale || newScale > maxScale) return
501
+
502
+ // Zoom towards pinch center
503
+ const rect = svg.getBoundingClientRect()
504
+ const centerX = (t1.clientX + t2.clientX) / 2
505
+ const centerY = (t1.clientY + t2.clientY) / 2
506
+ const xRatio = (centerX - rect.left) / rect.width
507
+ const yRatio = (centerY - rect.top) / rect.height
508
+
509
+ setViewBox(svg, {
510
+ x: pinchCenter.x - newWidth * xRatio,
511
+ y: pinchCenter.y - newHeight * yRatio,
512
+ width: newWidth,
513
+ height: newHeight,
514
+ })
515
+
516
+ te.preventDefault()
517
+ }
518
+ }
519
+
520
+ // Touch end handler
521
+ const handleTouchEnd = (e: Event) => {
522
+ const te = e as TouchEvent
523
+
524
+ if (te.touches.length === 0) {
525
+ isPanning = false
526
+ panStartViewBox = null
527
+ pinchStartViewBox = null
528
+ pinchCenter = null
529
+ } else if (te.touches.length === 1) {
530
+ // Switched from pinch to pan
531
+ pinchStartViewBox = null
532
+ pinchCenter = null
533
+ isPanning = true
534
+ const touch = te.touches[0]
535
+ panStartX = touch.clientX
536
+ panStartY = touch.clientY
537
+ panStartViewBox = parseViewBox(svg)
538
+ }
539
+ }
540
+
541
+ // Touch/click handler for mobile tooltip
542
+ const handleTap = (e: Event) => {
543
+ if (!isTouchDevice) return
544
+ if (isPanning) return
545
+
546
+ const me = e as MouseEvent
547
+ const targetEl = e.target as Element
548
+ const info = getTooltipInfo(targetEl)
549
+
550
+ if (info) {
551
+ // Show tooltip at tap position
552
+ showTooltip(info.text, me.clientX, me.clientY)
553
+ highlightElement(info.element)
554
+ touchTooltipActive = true
555
+ e.preventDefault()
556
+ } else if (touchTooltipActive) {
557
+ // Tap on empty area - hide tooltip
558
+ hideTooltip()
559
+ highlightElement(null)
560
+ touchTooltipActive = false
561
+ }
562
+ }
563
+
564
+ // Position update handler for scroll/resize events
565
+ const handlePositionUpdate = () => {
566
+ updateHighlightPosition()
567
+ }
568
+
569
+ // Track viewBox changes for pan/zoom
570
+ let rafId: number | null = null
571
+ let lastViewBox = ''
572
+ const trackViewBox = () => {
573
+ if (currentHighlight) {
574
+ const viewBox = svg.getAttribute('viewBox') || ''
575
+ if (viewBox !== lastViewBox) {
576
+ lastViewBox = viewBox
577
+ updateHighlightPosition()
578
+ }
579
+ }
580
+ rafId = requestAnimationFrame(trackViewBox)
581
+ }
582
+ rafId = requestAnimationFrame(trackViewBox)
583
+
584
+ // Reset view
585
+ const resetView = () => {
586
+ if (originalViewBox) {
587
+ setViewBox(svg, originalViewBox)
588
+ }
589
+ }
590
+
591
+ // Set touch-action to prevent browser gestures
592
+ if (panZoomEnabled) {
593
+ svg.style.touchAction = 'none'
594
+ }
595
+
596
+ // Add event listeners
597
+ svg.addEventListener('mousemove', handleMouseMove)
598
+ svg.addEventListener('mouseleave', handleMouseLeave)
599
+ svg.addEventListener('mousedown', handleMouseDown)
600
+ svg.addEventListener('click', handleTap)
601
+ svg.addEventListener('wheel', handleWheel, { passive: false })
602
+ svg.addEventListener('touchstart', handleTouchStart, { passive: true })
603
+ svg.addEventListener('touchmove', handleTouchMove, { passive: false })
604
+ svg.addEventListener('touchend', handleTouchEnd)
605
+
606
+ // Global mouse events for pan
607
+ document.addEventListener('mousemove', handlePan)
608
+ document.addEventListener('mouseup', handleMouseUp)
609
+
610
+ // Listen for scroll/resize to update highlight position
611
+ window.addEventListener('scroll', handlePositionUpdate, true)
612
+ window.addEventListener('resize', handlePositionUpdate)
613
+
614
+ return {
615
+ destroy: () => {
616
+ svg.removeEventListener('mousemove', handleMouseMove)
617
+ svg.removeEventListener('mouseleave', handleMouseLeave)
618
+ svg.removeEventListener('mousedown', handleMouseDown)
619
+ svg.removeEventListener('click', handleTap)
620
+ svg.removeEventListener('wheel', handleWheel)
621
+ svg.removeEventListener('touchstart', handleTouchStart)
622
+ svg.removeEventListener('touchmove', handleTouchMove)
623
+ svg.removeEventListener('touchend', handleTouchEnd)
624
+ document.removeEventListener('mousemove', handlePan)
625
+ document.removeEventListener('mouseup', handleMouseUp)
626
+ window.removeEventListener('scroll', handlePositionUpdate, true)
627
+ window.removeEventListener('resize', handlePositionUpdate)
628
+ if (rafId !== null) cancelAnimationFrame(rafId)
629
+ highlightElement(null)
630
+ if (tooltip) {
631
+ tooltip.remove()
632
+ tooltip = null
633
+ }
634
+ if (overlay) {
635
+ overlay.remove()
636
+ overlay = null
637
+ }
638
+ if (highlightContainer) {
639
+ highlightContainer.remove()
640
+ highlightContainer = null
641
+ }
642
+ },
643
+ showDeviceModal: () => {},
644
+ hideModal: () => {},
645
+ showLinkTooltip: () => {},
646
+ hideTooltip: () => {
647
+ hideTooltip()
648
+ highlightElement(null)
649
+ touchTooltipActive = false
650
+ },
651
+ resetView,
652
+ getScale,
653
+ }
654
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @shumoku/renderer - SVG and HTML renderers for network diagrams
3
+ */
4
+
5
+ import * as html from './html/index.js'
6
+ // Namespace exports
7
+ import * as svg from './svg.js'
8
+
9
+ export { svg, html }
10
+
11
+ // Types
12
+ export type {
13
+ DataAttributeOptions,
14
+ DeviceInfo,
15
+ EndpointInfo,
16
+ HTMLRendererOptions,
17
+ InteractiveInstance,
18
+ InteractiveOptions,
19
+ LinkInfo,
20
+ PortInfo,
21
+ RenderMode,
22
+ } from './types.js'