@shumoku/renderer 0.2.5 → 0.2.14
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/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +140 -0
- package/dist/cli.js.map +1 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +590 -587
- package/dist/html/index.js.map +1 -1
- package/dist/html/navigation.js +138 -138
- package/dist/html/spotlight.js +21 -21
- package/dist/html/tooltip.js +18 -18
- package/dist/html/tooltip.js.map +1 -1
- package/dist/iife-string.d.ts.map +1 -0
- package/dist/iife-string.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/svg.js +97 -97
- package/package.json +6 -2
- package/src/build-iife-string.ts +26 -26
- package/src/cli.ts +160 -0
- package/src/html/index.ts +883 -880
- package/src/html/navigation.ts +256 -256
- package/src/html/runtime.ts +412 -412
- package/src/html/spotlight.ts +135 -135
- package/src/html/tooltip.ts +141 -141
- package/src/html/viewbox.ts +28 -28
- package/src/iife-string.ts +8 -0
- package/src/index.ts +24 -25
- package/src/svg.ts +1640 -1640
- package/src/types.ts +127 -127
package/src/html/spotlight.ts
CHANGED
|
@@ -1,135 +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
|
-
}
|
|
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
|
+
}
|
package/src/html/tooltip.ts
CHANGED
|
@@ -1,141 +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
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
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
|
+
const from = linkGroup.getAttribute('data-link-from') || ''
|
|
85
|
+
const 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
|
+
}
|
package/src/html/viewbox.ts
CHANGED
|
@@ -1,28 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IIFE string placeholder
|
|
3
|
+
* This file is compiled by tsc, but dist/iife-string.js is overwritten
|
|
4
|
+
* by build:iife-string with the actual IIFE content.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Placeholder - replaced at build time
|
|
8
|
+
export const INTERACTIVE_IIFE = '/* IIFE placeholder - rebuild to get actual content */'
|
package/src/index.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
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
|
-
//
|
|
12
|
-
export type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export type { SheetData } from './html/index.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
|
+
// Re-export SheetData for hierarchical rendering
|
|
12
|
+
export type { SheetData } from './html/index.js'
|
|
13
|
+
// Types
|
|
14
|
+
export type {
|
|
15
|
+
DataAttributeOptions,
|
|
16
|
+
DeviceInfo,
|
|
17
|
+
EndpointInfo,
|
|
18
|
+
HTMLRendererOptions,
|
|
19
|
+
InteractiveInstance,
|
|
20
|
+
InteractiveOptions,
|
|
21
|
+
LinkInfo,
|
|
22
|
+
PortInfo,
|
|
23
|
+
RenderMode,
|
|
24
|
+
} from './types.js'
|