@shumoku/renderer 0.2.0 → 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.
@@ -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
+ }