@shumoku/renderer 0.2.3 → 0.2.5
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 +478 -2
- 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.map +1 -1
- package/dist/html/runtime.js +37 -0
- package/dist/html/runtime.js.map +1 -1
- package/dist/html/tooltip.d.ts.map +1 -1
- package/dist/html/tooltip.js +30 -2
- package/dist/html/tooltip.js.map +1 -1
- 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 +10 -8
- 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 +6 -3
- package/src/build-iife-string.ts +26 -19
- package/src/html/index.ts +550 -4
- package/src/html/navigation.ts +256 -0
- package/src/html/runtime.ts +42 -0
- package/src/html/tooltip.ts +28 -2
- package/src/index.ts +25 -22
- package/src/svg.ts +1640 -1502
- package/src/types.ts +127 -125
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation UI for hierarchical network diagrams
|
|
3
|
+
* Provides breadcrumb, tabs, and back button for sheet navigation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Navigation state for hierarchical diagrams
|
|
8
|
+
*/
|
|
9
|
+
export interface NavigationState {
|
|
10
|
+
/**
|
|
11
|
+
* Current sheet ID (undefined = root)
|
|
12
|
+
*/
|
|
13
|
+
currentSheet?: string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Breadcrumb path from root to current sheet
|
|
17
|
+
*/
|
|
18
|
+
breadcrumb: string[]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Available sheets
|
|
22
|
+
*/
|
|
23
|
+
sheets: Map<string, SheetInfo>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Information about a sheet
|
|
28
|
+
*/
|
|
29
|
+
export interface SheetInfo {
|
|
30
|
+
id: string
|
|
31
|
+
label: string
|
|
32
|
+
parentId?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate breadcrumb HTML
|
|
37
|
+
*/
|
|
38
|
+
export function generateBreadcrumb(state: NavigationState): string {
|
|
39
|
+
const items = state.breadcrumb.map((id, index) => {
|
|
40
|
+
const isLast = index === state.breadcrumb.length - 1
|
|
41
|
+
const label = id === 'root' ? 'Overview' : (state.sheets.get(id)?.label ?? id)
|
|
42
|
+
|
|
43
|
+
if (isLast) {
|
|
44
|
+
return `<span class="breadcrumb-current">${escapeHtml(label)}</span>`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `<button class="breadcrumb-item" data-sheet="${escapeHtml(id)}">${escapeHtml(label)}</button>`
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return `<nav class="breadcrumb">${items.join('<span class="breadcrumb-sep">/</span>')}</nav>`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate tab navigation HTML for sibling sheets
|
|
55
|
+
*/
|
|
56
|
+
export function generateTabs(state: NavigationState, siblingIds: string[]): string {
|
|
57
|
+
if (siblingIds.length <= 1) return ''
|
|
58
|
+
|
|
59
|
+
const tabs = siblingIds.map((id) => {
|
|
60
|
+
const info = state.sheets.get(id)
|
|
61
|
+
const label = info?.label ?? id
|
|
62
|
+
const isActive = id === state.currentSheet
|
|
63
|
+
|
|
64
|
+
return `<button class="tab ${isActive ? 'active' : ''}" data-sheet="${escapeHtml(id)}">${escapeHtml(label)}</button>`
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return `<div class="tabs">${tabs.join('')}</div>`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate back button HTML
|
|
72
|
+
*/
|
|
73
|
+
export function generateBackButton(state: NavigationState): string {
|
|
74
|
+
if (state.breadcrumb.length <= 1) return ''
|
|
75
|
+
|
|
76
|
+
const parentId = state.breadcrumb[state.breadcrumb.length - 2]
|
|
77
|
+
const parentLabel =
|
|
78
|
+
parentId === 'root' ? 'Overview' : (state.sheets.get(parentId)?.label ?? parentId)
|
|
79
|
+
|
|
80
|
+
return `<button class="back-btn" data-sheet="${escapeHtml(parentId)}">
|
|
81
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
82
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
83
|
+
</svg>
|
|
84
|
+
${escapeHtml(parentLabel)}
|
|
85
|
+
</button>`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate complete navigation toolbar HTML
|
|
90
|
+
*/
|
|
91
|
+
export function generateNavigationToolbar(
|
|
92
|
+
state: NavigationState,
|
|
93
|
+
siblingIds: string[] = [],
|
|
94
|
+
): string {
|
|
95
|
+
const breadcrumb = generateBreadcrumb(state)
|
|
96
|
+
const backBtn = generateBackButton(state)
|
|
97
|
+
const tabs = generateTabs(state, siblingIds)
|
|
98
|
+
|
|
99
|
+
return `<div class="nav-toolbar">
|
|
100
|
+
<div class="nav-row">
|
|
101
|
+
${backBtn}
|
|
102
|
+
${breadcrumb}
|
|
103
|
+
</div>
|
|
104
|
+
${tabs}
|
|
105
|
+
</div>`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get CSS styles for navigation components
|
|
110
|
+
*/
|
|
111
|
+
export function getNavigationStyles(): string {
|
|
112
|
+
return `
|
|
113
|
+
.nav-toolbar {
|
|
114
|
+
padding: 8px 16px;
|
|
115
|
+
background: white;
|
|
116
|
+
border-bottom: 1px solid #e5e5e5;
|
|
117
|
+
}
|
|
118
|
+
.nav-row {
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 12px;
|
|
122
|
+
}
|
|
123
|
+
.back-btn {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 4px;
|
|
127
|
+
padding: 6px 12px;
|
|
128
|
+
border: 1px solid #e5e5e5;
|
|
129
|
+
background: white;
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
font-size: 13px;
|
|
133
|
+
color: #555;
|
|
134
|
+
transition: all 0.15s;
|
|
135
|
+
}
|
|
136
|
+
.back-btn:hover {
|
|
137
|
+
background: #f5f5f5;
|
|
138
|
+
border-color: #ccc;
|
|
139
|
+
}
|
|
140
|
+
.breadcrumb {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 4px;
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
}
|
|
146
|
+
.breadcrumb-item {
|
|
147
|
+
background: none;
|
|
148
|
+
border: none;
|
|
149
|
+
color: #0066cc;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
padding: 4px 8px;
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
}
|
|
154
|
+
.breadcrumb-item:hover {
|
|
155
|
+
background: #f0f7ff;
|
|
156
|
+
}
|
|
157
|
+
.breadcrumb-current {
|
|
158
|
+
color: #333;
|
|
159
|
+
font-weight: 500;
|
|
160
|
+
}
|
|
161
|
+
.breadcrumb-sep {
|
|
162
|
+
color: #999;
|
|
163
|
+
}
|
|
164
|
+
.tabs {
|
|
165
|
+
display: flex;
|
|
166
|
+
gap: 4px;
|
|
167
|
+
margin-top: 8px;
|
|
168
|
+
border-bottom: 1px solid #e5e5e5;
|
|
169
|
+
padding-bottom: 0;
|
|
170
|
+
}
|
|
171
|
+
.tab {
|
|
172
|
+
padding: 8px 16px;
|
|
173
|
+
border: none;
|
|
174
|
+
background: none;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
color: #666;
|
|
178
|
+
border-bottom: 2px solid transparent;
|
|
179
|
+
margin-bottom: -1px;
|
|
180
|
+
transition: all 0.15s;
|
|
181
|
+
}
|
|
182
|
+
.tab:hover {
|
|
183
|
+
color: #333;
|
|
184
|
+
background: #f5f5f5;
|
|
185
|
+
}
|
|
186
|
+
.tab.active {
|
|
187
|
+
color: #0066cc;
|
|
188
|
+
border-bottom-color: #0066cc;
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get JavaScript code for navigation event handling
|
|
196
|
+
*/
|
|
197
|
+
export function getNavigationScript(): string {
|
|
198
|
+
return `
|
|
199
|
+
(function() {
|
|
200
|
+
function navigateToSheet(sheetId) {
|
|
201
|
+
// Dispatch custom event for sheet navigation
|
|
202
|
+
var event = new CustomEvent('shumoku:navigate', {
|
|
203
|
+
detail: { sheetId: sheetId },
|
|
204
|
+
bubbles: true
|
|
205
|
+
});
|
|
206
|
+
document.dispatchEvent(event);
|
|
207
|
+
|
|
208
|
+
// For standalone HTML, show alert (sheets are not embedded yet)
|
|
209
|
+
console.log('[Shumoku] Navigate to sheet:', sheetId);
|
|
210
|
+
alert('Navigate to: ' + sheetId + '\\n\\nNote: Multi-sheet navigation requires embedded sheets.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Listen for navigation events from subgraph clicks
|
|
214
|
+
document.addEventListener('shumoku:navigate', function(e) {
|
|
215
|
+
var sheetId = e.detail && e.detail.sheetId;
|
|
216
|
+
if (sheetId) {
|
|
217
|
+
console.log('[Shumoku] Subgraph clicked, sheet:', sheetId);
|
|
218
|
+
alert('Navigate to: ' + sheetId + '\\n\\nNote: Multi-sheet navigation requires embedded sheets.');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Handle breadcrumb clicks
|
|
223
|
+
document.querySelectorAll('.breadcrumb-item').forEach(function(btn) {
|
|
224
|
+
btn.addEventListener('click', function() {
|
|
225
|
+
var sheetId = this.getAttribute('data-sheet');
|
|
226
|
+
if (sheetId) navigateToSheet(sheetId);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Handle back button clicks
|
|
231
|
+
document.querySelectorAll('.back-btn').forEach(function(btn) {
|
|
232
|
+
btn.addEventListener('click', function() {
|
|
233
|
+
var sheetId = this.getAttribute('data-sheet');
|
|
234
|
+
if (sheetId) navigateToSheet(sheetId);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Handle tab clicks
|
|
239
|
+
document.querySelectorAll('.tab').forEach(function(btn) {
|
|
240
|
+
btn.addEventListener('click', function() {
|
|
241
|
+
var sheetId = this.getAttribute('data-sheet');
|
|
242
|
+
if (sheetId) navigateToSheet(sheetId);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
})();
|
|
246
|
+
`
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function escapeHtml(str: string): string {
|
|
250
|
+
return str
|
|
251
|
+
.replace(/&/g, '&')
|
|
252
|
+
.replace(/</g, '<')
|
|
253
|
+
.replace(/>/g, '>')
|
|
254
|
+
.replace(/"/g, '"')
|
|
255
|
+
.replace(/'/g, ''')
|
|
256
|
+
}
|
package/src/html/runtime.ts
CHANGED
|
@@ -242,6 +242,31 @@ export function initInteractive(options: InteractiveOptions): InteractiveInstanc
|
|
|
242
242
|
)
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// ============================================
|
|
246
|
+
// Hierarchical Navigation
|
|
247
|
+
// ============================================
|
|
248
|
+
|
|
249
|
+
const handleSubgraphClick = (e: MouseEvent) => {
|
|
250
|
+
const target = e.target as Element
|
|
251
|
+
const subgraph = target.closest('.subgraph[data-has-sheet]')
|
|
252
|
+
if (subgraph) {
|
|
253
|
+
const sheetId = subgraph.getAttribute('data-sheet-id')
|
|
254
|
+
if (sheetId) {
|
|
255
|
+
e.preventDefault()
|
|
256
|
+
e.stopPropagation()
|
|
257
|
+
dispatchNavigateEvent(sheetId)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const dispatchNavigateEvent = (sheetId: string) => {
|
|
263
|
+
const event = new CustomEvent('shumoku:navigate', {
|
|
264
|
+
detail: { sheetId },
|
|
265
|
+
bubbles: true,
|
|
266
|
+
})
|
|
267
|
+
document.dispatchEvent(event)
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
// ============================================
|
|
246
271
|
// Tap for tooltip (touch devices)
|
|
247
272
|
// ============================================
|
|
@@ -274,6 +299,18 @@ export function initInteractive(options: InteractiveOptions): InteractiveInstanc
|
|
|
274
299
|
if (Math.hypot(dx, dy) < 10 && dt < 300) {
|
|
275
300
|
const targetEl = document.elementFromPoint(touch.clientX, touch.clientY)
|
|
276
301
|
if (targetEl) {
|
|
302
|
+
// Check for hierarchical navigation first
|
|
303
|
+
const subgraph = targetEl.closest('.subgraph[data-has-sheet]')
|
|
304
|
+
if (subgraph) {
|
|
305
|
+
const sheetId = subgraph.getAttribute('data-sheet-id')
|
|
306
|
+
if (sheetId) {
|
|
307
|
+
dispatchNavigateEvent(sheetId)
|
|
308
|
+
tapStart = null
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Otherwise show tooltip
|
|
277
314
|
const info = getTooltipInfo(targetEl)
|
|
278
315
|
if (info) {
|
|
279
316
|
showTooltip(info.text, touch.clientX, touch.clientY)
|
|
@@ -333,6 +370,9 @@ export function initInteractive(options: InteractiveOptions): InteractiveInstanc
|
|
|
333
370
|
svg.addEventListener('mouseleave', handleMouseLeave)
|
|
334
371
|
svg.addEventListener('wheel', handleWheel, { passive: false })
|
|
335
372
|
|
|
373
|
+
// Hierarchical navigation: click on subgraph with sheet reference
|
|
374
|
+
svg.addEventListener('click', handleSubgraphClick)
|
|
375
|
+
|
|
336
376
|
// Listen for scroll/resize to update highlight position
|
|
337
377
|
window.addEventListener('scroll', handlePositionUpdate, true)
|
|
338
378
|
window.addEventListener('resize', handlePositionUpdate)
|
|
@@ -351,6 +391,7 @@ export function initInteractive(options: InteractiveOptions): InteractiveInstanc
|
|
|
351
391
|
document.removeEventListener('mouseup', handleMouseUp)
|
|
352
392
|
svg.removeEventListener('mouseleave', handleMouseLeave)
|
|
353
393
|
svg.removeEventListener('wheel', handleWheel)
|
|
394
|
+
svg.removeEventListener('click', handleSubgraphClick)
|
|
354
395
|
window.removeEventListener('scroll', handlePositionUpdate, true)
|
|
355
396
|
window.removeEventListener('resize', handlePositionUpdate)
|
|
356
397
|
destroyTooltip()
|
|
@@ -366,5 +407,6 @@ export function initInteractive(options: InteractiveOptions): InteractiveInstanc
|
|
|
366
407
|
},
|
|
367
408
|
resetView,
|
|
368
409
|
getScale,
|
|
410
|
+
navigateToSheet: dispatchNavigateEvent,
|
|
369
411
|
}
|
|
370
412
|
}
|
package/src/html/tooltip.ts
CHANGED
|
@@ -81,10 +81,36 @@ export function getTooltipInfo(el: Element): TooltipInfo | null {
|
|
|
81
81
|
|
|
82
82
|
const linkGroup = el.closest('.link-group[data-link-id]')
|
|
83
83
|
if (linkGroup) {
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
let from = linkGroup.getAttribute('data-link-from') || ''
|
|
85
|
+
let to = linkGroup.getAttribute('data-link-to') || ''
|
|
86
86
|
const bw = linkGroup.getAttribute('data-link-bandwidth')
|
|
87
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
|
+
}
|
|
88
114
|
|
|
89
115
|
let text = `${from} ↔ ${to}`
|
|
90
116
|
if (bw) text += `\n${bw}`
|
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'
|