@shumoku/renderer 0.2.1 → 0.2.3
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.js +265 -157
- package/dist/html/index.js.map +1 -1
- package/dist/html/runtime.d.ts +2 -1
- package/dist/html/runtime.d.ts.map +1 -1
- package/dist/html/runtime.js +212 -486
- 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 +105 -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.js +1 -1
- package/dist/shumoku-interactive.iife.js +23 -20
- package/package.json +1 -1
- package/src/html/index.ts +334 -226
- package/src/html/runtime.ts +370 -654
- package/src/html/spotlight.ts +135 -0
- package/src/html/tooltip.ts +115 -0
- package/src/html/viewbox.ts +28 -0
|
@@ -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,115 @@
|
|
|
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
|
+
|
|
89
|
+
let text = `${from} ↔ ${to}`
|
|
90
|
+
if (bw) text += `\n${bw}`
|
|
91
|
+
if (vlan) text += `\nVLAN: ${vlan}`
|
|
92
|
+
return { text, element: linkGroup }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const node = el.closest('.node[data-id]')
|
|
96
|
+
if (node) {
|
|
97
|
+
const json = node.getAttribute('data-device-json')
|
|
98
|
+
if (json) {
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(json)
|
|
101
|
+
const lines: string[] = []
|
|
102
|
+
if (data.label) lines.push(Array.isArray(data.label) ? data.label.join(' ') : data.label)
|
|
103
|
+
if (data.type) lines.push(`Type: ${data.type}`)
|
|
104
|
+
if (data.vendor) lines.push(`Vendor: ${data.vendor}`)
|
|
105
|
+
if (data.model) lines.push(`Model: ${data.model}`)
|
|
106
|
+
return { text: lines.join('\n'), element: node }
|
|
107
|
+
} catch {
|
|
108
|
+
// fallthrough
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { text: node.getAttribute('data-id') || '', element: node }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
@@ -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
|
+
}
|