@shumoku/renderer 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/index.d.ts +25 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +742 -158
- package/dist/html/index.js.map +1 -1
- package/dist/html/navigation.d.ts +54 -0
- package/dist/html/navigation.d.ts.map +1 -0
- package/dist/html/navigation.js +210 -0
- package/dist/html/navigation.js.map +1 -0
- package/dist/html/runtime.d.ts +2 -1
- package/dist/html/runtime.d.ts.map +1 -1
- package/dist/html/runtime.js +245 -482
- package/dist/html/runtime.js.map +1 -1
- package/dist/html/spotlight.d.ts +9 -0
- package/dist/html/spotlight.d.ts.map +1 -0
- package/dist/html/spotlight.js +119 -0
- package/dist/html/spotlight.js.map +1 -0
- package/dist/html/tooltip.d.ts +14 -0
- package/dist/html/tooltip.d.ts.map +1 -0
- package/dist/html/tooltip.js +133 -0
- package/dist/html/tooltip.js.map +1 -0
- package/dist/html/viewbox.d.ts +14 -0
- package/dist/html/viewbox.d.ts.map +1 -0
- package/dist/html/viewbox.js +21 -0
- package/dist/html/viewbox.js.map +1 -0
- package/dist/iife-string.d.ts +2 -0
- package/dist/iife-string.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/shumoku-interactive.iife.js +25 -20
- package/dist/svg.d.ts +27 -0
- package/dist/svg.d.ts.map +1 -1
- package/dist/svg.js +202 -101
- package/dist/svg.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/build-iife-string.ts +26 -19
- package/src/html/index.ts +880 -226
- package/src/html/navigation.ts +256 -0
- package/src/html/runtime.ts +412 -654
- package/src/html/spotlight.ts +135 -0
- package/src/html/tooltip.ts +141 -0
- package/src/html/viewbox.ts +28 -0
- package/src/index.ts +25 -22
- package/src/svg.ts +1640 -1502
- package/src/types.ts +127 -125
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spotlight Module
|
|
3
|
+
* Handles element highlighting with overlay and glow effect
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let overlay: HTMLDivElement | null = null
|
|
7
|
+
let highlightContainer: HTMLDivElement | null = null
|
|
8
|
+
let currentHighlight: Element | null = null
|
|
9
|
+
let currentMiniSvg: SVGSVGElement | null = null
|
|
10
|
+
|
|
11
|
+
function getOverlay(): HTMLDivElement {
|
|
12
|
+
if (!overlay) {
|
|
13
|
+
overlay = document.createElement('div')
|
|
14
|
+
overlay.style.cssText = `
|
|
15
|
+
position: fixed;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
right: 0;
|
|
19
|
+
bottom: 0;
|
|
20
|
+
background: rgba(0, 0, 0, 0.5);
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
opacity: 0;
|
|
23
|
+
transition: opacity 0.15s ease;
|
|
24
|
+
z-index: 9998;
|
|
25
|
+
`
|
|
26
|
+
document.body.appendChild(overlay)
|
|
27
|
+
}
|
|
28
|
+
return overlay
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getHighlightContainer(): HTMLDivElement {
|
|
32
|
+
if (!highlightContainer) {
|
|
33
|
+
highlightContainer = document.createElement('div')
|
|
34
|
+
highlightContainer.style.cssText = `
|
|
35
|
+
position: fixed;
|
|
36
|
+
top: 0;
|
|
37
|
+
left: 0;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
z-index: 9999;
|
|
40
|
+
`
|
|
41
|
+
document.body.appendChild(highlightContainer)
|
|
42
|
+
}
|
|
43
|
+
return highlightContainer
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function updateHighlightPosition(): void {
|
|
47
|
+
if (!currentHighlight || !currentMiniSvg) return
|
|
48
|
+
|
|
49
|
+
if (!document.contains(currentHighlight)) {
|
|
50
|
+
highlightElement(null)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const svg = currentHighlight.closest('svg') as SVGSVGElement | null
|
|
55
|
+
if (!svg) return
|
|
56
|
+
|
|
57
|
+
const rect = svg.getBoundingClientRect()
|
|
58
|
+
const viewBox = svg.getAttribute('viewBox')
|
|
59
|
+
if (viewBox) {
|
|
60
|
+
currentMiniSvg.setAttribute('viewBox', viewBox)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
currentMiniSvg.style.left = `${rect.left}px`
|
|
64
|
+
currentMiniSvg.style.top = `${rect.top}px`
|
|
65
|
+
currentMiniSvg.style.width = `${rect.width}px`
|
|
66
|
+
currentMiniSvg.style.height = `${rect.height}px`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function highlightElement(el: Element | null): void {
|
|
70
|
+
if (el === currentHighlight) return
|
|
71
|
+
|
|
72
|
+
if (currentHighlight) {
|
|
73
|
+
currentHighlight.classList.remove('shumoku-highlight')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const container = getHighlightContainer()
|
|
77
|
+
container.innerHTML = ''
|
|
78
|
+
currentMiniSvg = null
|
|
79
|
+
currentHighlight = el
|
|
80
|
+
|
|
81
|
+
const ov = getOverlay()
|
|
82
|
+
|
|
83
|
+
if (el) {
|
|
84
|
+
el.classList.add('shumoku-highlight')
|
|
85
|
+
|
|
86
|
+
const svg = el.closest('svg') as SVGSVGElement | null
|
|
87
|
+
if (svg) {
|
|
88
|
+
const viewBox = svg.getAttribute('viewBox')
|
|
89
|
+
if (viewBox) {
|
|
90
|
+
const clone = el.cloneNode(true) as Element
|
|
91
|
+
clone.classList.remove('shumoku-highlight')
|
|
92
|
+
|
|
93
|
+
const miniSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
94
|
+
miniSvg.setAttribute('viewBox', viewBox)
|
|
95
|
+
miniSvg.style.cssText = `
|
|
96
|
+
position: absolute;
|
|
97
|
+
overflow: visible;
|
|
98
|
+
filter: drop-shadow(0 0 4px #fff) drop-shadow(0 0 8px #fff) drop-shadow(0 0 12px rgba(100, 150, 255, 0.8));
|
|
99
|
+
`
|
|
100
|
+
|
|
101
|
+
const defs = svg.querySelector('defs')
|
|
102
|
+
if (defs) {
|
|
103
|
+
miniSvg.appendChild(defs.cloneNode(true))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
miniSvg.appendChild(clone)
|
|
107
|
+
container.appendChild(miniSvg)
|
|
108
|
+
currentMiniSvg = miniSvg
|
|
109
|
+
|
|
110
|
+
updateHighlightPosition()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ov.style.opacity = '1'
|
|
115
|
+
} else {
|
|
116
|
+
ov.style.opacity = '0'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getCurrentHighlight(): Element | null {
|
|
121
|
+
return currentHighlight
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function destroySpotlight(): void {
|
|
125
|
+
if (overlay) {
|
|
126
|
+
overlay.remove()
|
|
127
|
+
overlay = null
|
|
128
|
+
}
|
|
129
|
+
if (highlightContainer) {
|
|
130
|
+
highlightContainer.remove()
|
|
131
|
+
highlightContainer = null
|
|
132
|
+
}
|
|
133
|
+
currentHighlight = null
|
|
134
|
+
currentMiniSvg = null
|
|
135
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip Module
|
|
3
|
+
* Handles tooltip creation, positioning, and display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let tooltip: HTMLDivElement | null = null
|
|
7
|
+
|
|
8
|
+
export function getTooltip(): HTMLDivElement {
|
|
9
|
+
if (!tooltip) {
|
|
10
|
+
tooltip = document.createElement('div')
|
|
11
|
+
tooltip.style.cssText = `
|
|
12
|
+
position: fixed;
|
|
13
|
+
z-index: 10000;
|
|
14
|
+
padding: 8px 12px;
|
|
15
|
+
background: rgba(30, 41, 59, 0.95);
|
|
16
|
+
color: #fff;
|
|
17
|
+
font-size: 13px;
|
|
18
|
+
line-height: 1.4;
|
|
19
|
+
border-radius: 6px;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
opacity: 0;
|
|
22
|
+
transition: opacity 0.15s;
|
|
23
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
24
|
+
max-width: 280px;
|
|
25
|
+
white-space: pre-line;
|
|
26
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
27
|
+
`
|
|
28
|
+
document.body.appendChild(tooltip)
|
|
29
|
+
}
|
|
30
|
+
return tooltip
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function showTooltip(text: string, x: number, y: number): void {
|
|
34
|
+
const t = getTooltip()
|
|
35
|
+
t.textContent = text
|
|
36
|
+
t.style.opacity = '1'
|
|
37
|
+
|
|
38
|
+
requestAnimationFrame(() => {
|
|
39
|
+
const rect = t.getBoundingClientRect()
|
|
40
|
+
const pad = 12
|
|
41
|
+
let left = x + pad
|
|
42
|
+
let top = y - rect.height - pad
|
|
43
|
+
|
|
44
|
+
if (left + rect.width > window.innerWidth - pad) {
|
|
45
|
+
left = x - rect.width - pad
|
|
46
|
+
}
|
|
47
|
+
if (top < pad) {
|
|
48
|
+
top = y + pad
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
t.style.left = `${Math.max(pad, left)}px`
|
|
52
|
+
t.style.top = `${Math.max(pad, top)}px`
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hideTooltip(): void {
|
|
57
|
+
if (tooltip) {
|
|
58
|
+
tooltip.style.opacity = '0'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function destroyTooltip(): void {
|
|
63
|
+
if (tooltip) {
|
|
64
|
+
tooltip.remove()
|
|
65
|
+
tooltip = null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TooltipInfo {
|
|
70
|
+
text: string
|
|
71
|
+
element: Element
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getTooltipInfo(el: Element): TooltipInfo | null {
|
|
75
|
+
const port = el.closest('.port[data-port]')
|
|
76
|
+
if (port) {
|
|
77
|
+
const portId = port.getAttribute('data-port') || ''
|
|
78
|
+
const deviceId = port.getAttribute('data-port-device') || ''
|
|
79
|
+
return { text: `${deviceId}:${portId}`, element: port }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const linkGroup = el.closest('.link-group[data-link-id]')
|
|
83
|
+
if (linkGroup) {
|
|
84
|
+
let from = linkGroup.getAttribute('data-link-from') || ''
|
|
85
|
+
let to = linkGroup.getAttribute('data-link-to') || ''
|
|
86
|
+
const bw = linkGroup.getAttribute('data-link-bandwidth')
|
|
87
|
+
const vlan = linkGroup.getAttribute('data-link-vlan')
|
|
88
|
+
const jsonAttr = linkGroup.getAttribute('data-link-json')
|
|
89
|
+
|
|
90
|
+
// For export links, show device:port ↔ destination device:port
|
|
91
|
+
const fromIsExport = from.startsWith('__export_')
|
|
92
|
+
const toIsExport = to.startsWith('__export_')
|
|
93
|
+
if ((fromIsExport || toIsExport) && jsonAttr) {
|
|
94
|
+
try {
|
|
95
|
+
const linkData = JSON.parse(jsonAttr)
|
|
96
|
+
const meta = linkData.metadata
|
|
97
|
+
if (meta?._destDevice) {
|
|
98
|
+
// Get the actual device endpoint (non-export side)
|
|
99
|
+
const localDevice = fromIsExport ? to : from
|
|
100
|
+
// Get the destination device:port from metadata
|
|
101
|
+
const destDevice = meta._destPort
|
|
102
|
+
? `${meta._destDevice}:${meta._destPort}`
|
|
103
|
+
: meta._destDevice
|
|
104
|
+
// Show: "local device ↔ remote device"
|
|
105
|
+
let text = `${localDevice} ↔ ${destDevice}`
|
|
106
|
+
if (bw) text += `\n${bw}`
|
|
107
|
+
if (vlan) text += `\nVLAN: ${vlan}`
|
|
108
|
+
return { text, element: linkGroup }
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Fall through to default display
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let text = `${from} ↔ ${to}`
|
|
116
|
+
if (bw) text += `\n${bw}`
|
|
117
|
+
if (vlan) text += `\nVLAN: ${vlan}`
|
|
118
|
+
return { text, element: linkGroup }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const node = el.closest('.node[data-id]')
|
|
122
|
+
if (node) {
|
|
123
|
+
const json = node.getAttribute('data-device-json')
|
|
124
|
+
if (json) {
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(json)
|
|
127
|
+
const lines: string[] = []
|
|
128
|
+
if (data.label) lines.push(Array.isArray(data.label) ? data.label.join(' ') : data.label)
|
|
129
|
+
if (data.type) lines.push(`Type: ${data.type}`)
|
|
130
|
+
if (data.vendor) lines.push(`Vendor: ${data.vendor}`)
|
|
131
|
+
if (data.model) lines.push(`Model: ${data.model}`)
|
|
132
|
+
return { text: lines.join('\n'), element: node }
|
|
133
|
+
} catch {
|
|
134
|
+
// fallthrough
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { text: node.getAttribute('data-id') || '', element: node }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewBox Utilities
|
|
3
|
+
* Handles SVG viewBox parsing and manipulation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ViewBox {
|
|
7
|
+
x: number
|
|
8
|
+
y: number
|
|
9
|
+
width: number
|
|
10
|
+
height: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseViewBox(svg: SVGSVGElement): ViewBox | null {
|
|
14
|
+
const vb = svg.getAttribute('viewBox')
|
|
15
|
+
if (!vb) return null
|
|
16
|
+
const parts = vb.split(/\s+|,/).map(Number)
|
|
17
|
+
if (parts.length !== 4 || parts.some(Number.isNaN)) return null
|
|
18
|
+
return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setViewBox(svg: SVGSVGElement, vb: ViewBox, onUpdate?: () => void): void {
|
|
22
|
+
svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.width} ${vb.height}`)
|
|
23
|
+
onUpdate?.()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cloneViewBox(vb: ViewBox): ViewBox {
|
|
27
|
+
return { x: vb.x, y: vb.y, width: vb.width, height: vb.height }
|
|
28
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
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'
|
|
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'
|
|
23
|
+
|
|
24
|
+
// Re-export SheetData for hierarchical rendering
|
|
25
|
+
export type { SheetData } from './html/index.js'
|