@picked/svelte 0.0.1-dev.1793776638554 → 0.0.1-dev.1793923852884
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/.tsbuildinfo +1 -1
- package/dist/experience/set-picked-context.js +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +155 -80
- package/dist/plugins/devtools/customTools.d.ts +2 -2
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +14 -0
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/answer-display/index.js +1 -1
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/index.d.ts +5 -5
- package/dist/plugins/devtools/derive/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/personas/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +88 -7
- package/dist/plugins/devtools/derive/personas/personas.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +5 -5
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/index.d.ts +1 -1
- package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
- package/dist/plugins/devtools/options.d.ts +6 -4
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +54 -8
- package/dist/plugins/devtools/review/personas/ChoiceButton.svelte +40 -6
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +69 -45
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +226 -43
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +171 -74
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +41 -20
- package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
- package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
- package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
- package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
- package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +14 -48
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +2 -2
- package/dist/plugins/devtools/sidebar/nav.d.ts +26 -39
- package/dist/plugins/devtools/sidebar/nav.js +1 -1
- package/package.json +2 -2
- package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
- package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
- package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
- package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
- package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/** @file D3-based tree visualization of an experience's node graph in the devtools panel. */
|
|
3
|
-
import { onDestroy } from 'svelte'
|
|
4
|
-
import * as d3 from 'd3'
|
|
5
|
-
import type { ExperienceEdge, ExperienceNode, ExperienceStore } from '@picked/client'
|
|
6
|
-
import { Badge, Button, Spinner, Stack, Text, VisualizationContainer } from '../../ui'
|
|
7
|
-
|
|
8
|
-
export let experience: ExperienceStore
|
|
9
|
-
|
|
10
|
-
let svgEl: SVGSVGElement
|
|
11
|
-
let zoom = 1
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future node details panel
|
|
13
|
-
let selectedNode = null
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, sonarjs/no-unused-collection -- reserved for future path highlighting
|
|
15
|
-
let highlightedPath: string[][] = []
|
|
16
|
-
let error: string | null = null
|
|
17
|
-
const loading = false
|
|
18
|
-
|
|
19
|
-
let treeRenderingDone = false
|
|
20
|
-
let tooltipEl: d3.Selection<HTMLDivElement, unknown, null, undefined> | null = null
|
|
21
|
-
|
|
22
|
-
onDestroy(() => {
|
|
23
|
-
tooltipEl?.remove()
|
|
24
|
-
tooltipEl = null
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const tree = experience.treeStore
|
|
28
|
-
|
|
29
|
-
$: nodes = $tree.nodes
|
|
30
|
-
$: edges = $tree.edges
|
|
31
|
-
$: conditions = $tree.conditions
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown>
|
|
35
|
-
|
|
36
|
-
interface ProcessedNode {
|
|
37
|
-
id: string
|
|
38
|
-
name: string
|
|
39
|
-
tooltip?: string
|
|
40
|
-
type: string
|
|
41
|
-
children: ProcessedNode[]
|
|
42
|
-
_originalNode: ExperienceNode
|
|
43
|
-
_originalEdges: ExperienceEdge[]
|
|
44
|
-
_visited?: boolean
|
|
45
|
-
_isReference?: boolean
|
|
46
|
-
_referenceTo?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Type guard that checks if a value is a non-null, non-array object.
|
|
51
|
-
*
|
|
52
|
-
* @param val - The value to check.
|
|
53
|
-
* @example
|
|
54
|
-
* ```typescript
|
|
55
|
-
* isRecord({ key: 'value' }) // true
|
|
56
|
-
* isRecord([1, 2]) // false
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
function isRecord(val: unknown): val is Record<string, unknown> {
|
|
60
|
-
return val !== null && typeof val === 'object' && !Array.isArray(val)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Recursively formats a JSON Logic rule into a human-readable string.
|
|
65
|
-
*
|
|
66
|
-
* @param rule - The JSON Logic rule object to format.
|
|
67
|
-
* @example
|
|
68
|
-
* ```typescript
|
|
69
|
-
* formatLogicRule({ '==': [{ var: 'age' }, 18] }) // 'age = 18'
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity -- recursive logic rule formatter handles all operator types
|
|
73
|
-
function formatLogicRule(rule: unknown): string {
|
|
74
|
-
if (rule === null || rule === undefined) return '?'
|
|
75
|
-
if (typeof rule === 'string') return rule
|
|
76
|
-
if (typeof rule === 'number' || typeof rule === 'boolean') return String(rule)
|
|
77
|
-
if (Array.isArray(rule)) {
|
|
78
|
-
return rule.map(formatLogicRule).join(', ')
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (isRecord(rule)) {
|
|
82
|
-
const keys = Object.keys(rule)
|
|
83
|
-
if (keys.length === 0) return 'Condition'
|
|
84
|
-
const op = keys[0]
|
|
85
|
-
const args = rule[op]
|
|
86
|
-
|
|
87
|
-
if (op === 'var') {
|
|
88
|
-
return String(Array.isArray(args) ? args[0] : args)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const compOps: Record<string, string> = {
|
|
92
|
-
'==': '=',
|
|
93
|
-
'===': '=',
|
|
94
|
-
'!=': '≠',
|
|
95
|
-
'!==': '≠',
|
|
96
|
-
'>': '>',
|
|
97
|
-
'<': '<',
|
|
98
|
-
'>=': '≥',
|
|
99
|
-
'<=': '≤',
|
|
100
|
-
}
|
|
101
|
-
if (op in compOps && Array.isArray(args) && args.length >= 2) {
|
|
102
|
-
return `${formatLogicRule(args[0])} ${compOps[op]} ${formatLogicRule(args[1])}`
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (op === 'and' && Array.isArray(args)) {
|
|
106
|
-
return args.map(formatLogicRule).join(' & ')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (op === 'or' && Array.isArray(args)) {
|
|
110
|
-
return args.map(formatLogicRule).join(' | ')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if ((op === '!' || op === 'not') && Array.isArray(args)) {
|
|
114
|
-
return `!${formatLogicRule(args[0])}`
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (op === 'in' && Array.isArray(args) && args.length >= 2) {
|
|
118
|
-
return `${formatLogicRule(args[0])} in ${formatLogicRule(args[1])}`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return `${op}(?)`
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return '?'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Renders the experience node graph as an interactive D3 tree visualization.
|
|
129
|
-
*
|
|
130
|
-
* @example
|
|
131
|
-
* ```typescript
|
|
132
|
-
* renderGraph() // builds and renders the SVG tree
|
|
133
|
-
* ```
|
|
134
|
-
*/
|
|
135
|
-
function renderGraph() {
|
|
136
|
-
d3.select(svgEl).selectAll('*').remove()
|
|
137
|
-
|
|
138
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
139
|
-
const conditionsMap = new Map(conditions.map((c) => [c.id, c]))
|
|
140
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
141
|
-
const outgoingEdges = new Map<string, ExperienceEdge[]>()
|
|
142
|
-
edges.forEach((e) => {
|
|
143
|
-
if (!outgoingEdges.has(e.sourceNodeId)) outgoingEdges.set(e.sourceNodeId, [])
|
|
144
|
-
outgoingEdges.get(e.sourceNodeId)?.push(e)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const getNodeLabel = (node: ExperienceNode): string => {
|
|
148
|
-
if (node.type === 'origin') return 'Start'
|
|
149
|
-
if (node.type === 'step') return node.name ?? 'Unknown Step'
|
|
150
|
-
|
|
151
|
-
if (node.type === 'condition') {
|
|
152
|
-
const conditionItem = node.conditionId ? conditionsMap.get(node.conditionId) : undefined
|
|
153
|
-
return conditionItem?.name ?? 'Condition'
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (node.type === 'action') return node.name ?? node.actionKey ?? 'Action'
|
|
157
|
-
|
|
158
|
-
return 'Unknown Node'
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const getNodeTooltip = (node: ExperienceNode): string | undefined => {
|
|
162
|
-
if (node.type !== 'condition') return undefined
|
|
163
|
-
const conditionItem = node.conditionId ? conditionsMap.get(node.conditionId) : undefined
|
|
164
|
-
if (conditionItem?.logic) return formatLogicRule(conditionItem.logic)
|
|
165
|
-
return undefined
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
169
|
-
const processedNodes = new Map<string, ProcessedNode>()
|
|
170
|
-
nodes.forEach((n) => {
|
|
171
|
-
processedNodes.set(n.id, {
|
|
172
|
-
id: n.id,
|
|
173
|
-
name: getNodeLabel(n),
|
|
174
|
-
tooltip: getNodeTooltip(n),
|
|
175
|
-
type: n.type,
|
|
176
|
-
children: [],
|
|
177
|
-
_originalNode: n,
|
|
178
|
-
_originalEdges: [],
|
|
179
|
-
})
|
|
180
|
-
})
|
|
181
|
-
edges.forEach((e) => {
|
|
182
|
-
const src = processedNodes.get(e.sourceNodeId)
|
|
183
|
-
const tgt = processedNodes.get(e.destinationNodeId)
|
|
184
|
-
if (src && tgt && !src.children.some((c) => c.id === tgt.id)) {
|
|
185
|
-
src._originalEdges.push(e)
|
|
186
|
-
src.children.push(tgt)
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const rootNode = processedNodes.get(nodes.find((n) => n.type === 'origin')?.id ?? '')
|
|
191
|
-
if (!rootNode) {
|
|
192
|
-
error = 'Origin node not found'
|
|
193
|
-
} else {
|
|
194
|
-
error = null
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (rootNode) {
|
|
198
|
-
// Use a global visited set so join nodes (multiple incoming branches converging
|
|
199
|
-
// to the same node) only appear once in the tree, with reference nodes on
|
|
200
|
-
// subsequent visits to indicate the convergence point.
|
|
201
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
202
|
-
const globalVisited = new Set<string>()
|
|
203
|
-
const convertGraphToTree = (node: ProcessedNode): ProcessedNode => {
|
|
204
|
-
if (globalVisited.has(node.id)) {
|
|
205
|
-
return {
|
|
206
|
-
...node,
|
|
207
|
-
id: `${node.id}_ref`,
|
|
208
|
-
name: `↩ ${node.name}`,
|
|
209
|
-
type: `${node.type}_ref`,
|
|
210
|
-
children: [],
|
|
211
|
-
_isReference: true,
|
|
212
|
-
_referenceTo: node.id,
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
globalVisited.add(node.id)
|
|
217
|
-
return { ...node, children: node.children.map((c) => convertGraphToTree(c)) }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const treeData = convertGraphToTree(rootNode)
|
|
221
|
-
const width = svgEl.clientWidth
|
|
222
|
-
const height = svgEl.clientHeight
|
|
223
|
-
const margin = { top: 50, right: 120, bottom: 50, left: 120 }
|
|
224
|
-
const innerWidth = width - margin.left - margin.right
|
|
225
|
-
const innerHeight = height - margin.top - margin.bottom
|
|
226
|
-
|
|
227
|
-
const treeLayout = d3
|
|
228
|
-
.tree<ProcessedNode>()
|
|
229
|
-
.size([innerHeight, innerWidth])
|
|
230
|
-
.nodeSize([80, 200])
|
|
231
|
-
const root = d3.hierarchy(treeData)
|
|
232
|
-
const treeRoot = treeLayout(root)
|
|
233
|
-
|
|
234
|
-
zoomBehavior = d3
|
|
235
|
-
.zoom<SVGSVGElement, unknown>()
|
|
236
|
-
.scaleExtent([0.1, 3])
|
|
237
|
-
.on('zoom', (event) => {
|
|
238
|
-
g.attr('transform', event.transform)
|
|
239
|
-
zoom = event.transform.k
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
const svg = d3.select(svgEl).attr('width', width).attr('height', height).call(zoomBehavior)
|
|
243
|
-
|
|
244
|
-
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
|
|
245
|
-
|
|
246
|
-
const linkGenerator = d3
|
|
247
|
-
.linkHorizontal<d3.HierarchyPointLink<ProcessedNode>, d3.HierarchyPointNode<ProcessedNode>>()
|
|
248
|
-
.x((d) => d.y)
|
|
249
|
-
.y((d) => d.x)
|
|
250
|
-
|
|
251
|
-
g.selectAll('.link')
|
|
252
|
-
.data(treeRoot.links())
|
|
253
|
-
.enter()
|
|
254
|
-
.append('path')
|
|
255
|
-
.attr('class', 'link')
|
|
256
|
-
.attr('fill', 'none')
|
|
257
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
258
|
-
.attr('stroke', (d: any) => {
|
|
259
|
-
if (d.source.data.type !== 'condition') return '#94a3b8'
|
|
260
|
-
const targetId = d.target.data._isReference
|
|
261
|
-
? d.target.data._referenceTo
|
|
262
|
-
: d.target.data.id
|
|
263
|
-
const edge = d.source.data._originalEdges.find(
|
|
264
|
-
(e: ExperienceEdge) => e.destinationNodeId === targetId,
|
|
265
|
-
)
|
|
266
|
-
if (edge?.sourceHandle === 'true') return '#10b981'
|
|
267
|
-
if (edge?.sourceHandle === 'false') return '#ef4444'
|
|
268
|
-
return '#94a3b8'
|
|
269
|
-
})
|
|
270
|
-
.attr('stroke-width', 2)
|
|
271
|
-
.attr('d', linkGenerator)
|
|
272
|
-
|
|
273
|
-
// Label true/false branches from condition nodes
|
|
274
|
-
g.selectAll('.edge-label')
|
|
275
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
276
|
-
.data(treeRoot.links().filter((d: any) => d.source.data.type === 'condition'))
|
|
277
|
-
.enter()
|
|
278
|
-
.append('text')
|
|
279
|
-
.attr('class', 'edge-label')
|
|
280
|
-
.attr('text-anchor', 'middle')
|
|
281
|
-
.attr('font-size', '10px')
|
|
282
|
-
.attr('font-weight', 'bold')
|
|
283
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
284
|
-
.attr('x', (d: any) => (d.source.y + d.target.y) / 2)
|
|
285
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
286
|
-
.attr('y', (d: any) => (d.source.x + d.target.x) / 2 - 6)
|
|
287
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
288
|
-
.attr('fill', (d: any) => {
|
|
289
|
-
const targetId = d.target.data._isReference
|
|
290
|
-
? d.target.data._referenceTo
|
|
291
|
-
: d.target.data.id
|
|
292
|
-
const edge = d.source.data._originalEdges.find(
|
|
293
|
-
(e: ExperienceEdge) => e.destinationNodeId === targetId,
|
|
294
|
-
)
|
|
295
|
-
if (edge?.sourceHandle === 'true') return '#10b981'
|
|
296
|
-
if (edge?.sourceHandle === 'false') return '#ef4444'
|
|
297
|
-
return '#64748b'
|
|
298
|
-
})
|
|
299
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
300
|
-
.text((d: any) => {
|
|
301
|
-
const targetId = d.target.data._isReference
|
|
302
|
-
? d.target.data._referenceTo
|
|
303
|
-
: d.target.data.id
|
|
304
|
-
const edge = d.source.data._originalEdges.find(
|
|
305
|
-
(e: ExperienceEdge) => e.destinationNodeId === targetId,
|
|
306
|
-
)
|
|
307
|
-
if (edge?.sourceHandle === 'true') return 'Yes'
|
|
308
|
-
if (edge?.sourceHandle === 'false') return 'No'
|
|
309
|
-
return ''
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
const nodesSelection = g
|
|
313
|
-
.selectAll('.node')
|
|
314
|
-
.data(treeRoot.descendants())
|
|
315
|
-
.enter()
|
|
316
|
-
.append('g')
|
|
317
|
-
.attr('class', 'node')
|
|
318
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
319
|
-
.attr('transform', (d: any) => `translate(${d.y},${d.x})`)
|
|
320
|
-
|
|
321
|
-
const circles = nodesSelection
|
|
322
|
-
.append('circle')
|
|
323
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
324
|
-
.attr('r', (d: any) => (d.data.type === 'origin' ? 15 : 10))
|
|
325
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
326
|
-
.attr('fill', (d: any) => {
|
|
327
|
-
if (d.data._isReference) return '#38bdf8'
|
|
328
|
-
if (d.data.type === 'origin') return '#6b7280'
|
|
329
|
-
if (d.data.type === 'condition') return '#f59e0b'
|
|
330
|
-
if (d.data.type === 'action') return '#22c55e'
|
|
331
|
-
return '#3b82f6'
|
|
332
|
-
})
|
|
333
|
-
.attr('stroke', '#fff')
|
|
334
|
-
.attr('stroke-width', 2)
|
|
335
|
-
|
|
336
|
-
tooltipEl?.remove()
|
|
337
|
-
tooltipEl = d3
|
|
338
|
-
.select(svgEl.parentElement)
|
|
339
|
-
.append('div')
|
|
340
|
-
|
|
341
|
-
const tooltip = tooltipEl
|
|
342
|
-
.style('position', 'absolute')
|
|
343
|
-
.style('pointer-events', 'none')
|
|
344
|
-
.style('background', '#1e293b')
|
|
345
|
-
.style('color', '#f8fafc')
|
|
346
|
-
.style('padding', '6px 10px')
|
|
347
|
-
.style('border-radius', '6px')
|
|
348
|
-
.style('font-size', '11px')
|
|
349
|
-
.style('max-width', '260px')
|
|
350
|
-
.style('white-space', 'pre-wrap')
|
|
351
|
-
.style('opacity', '0')
|
|
352
|
-
.style('z-index', '10')
|
|
353
|
-
|
|
354
|
-
circles
|
|
355
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
356
|
-
.filter((d: any) => d.data.tooltip)
|
|
357
|
-
.style('cursor', 'default')
|
|
358
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
359
|
-
.on('mouseenter', function showTooltip(event: MouseEvent, d: any) {
|
|
360
|
-
const container = svgEl.parentElement
|
|
361
|
-
if (!container || !d.data.tooltip) return
|
|
362
|
-
const rect = container.getBoundingClientRect()
|
|
363
|
-
tooltip
|
|
364
|
-
.text(d.data.tooltip)
|
|
365
|
-
.style('left', `${event.clientX - rect.left + 12}px`)
|
|
366
|
-
.style('top', `${event.clientY - rect.top - 8}px`)
|
|
367
|
-
.style('opacity', '1')
|
|
368
|
-
})
|
|
369
|
-
.on('mouseleave', function hideTooltip() {
|
|
370
|
-
tooltip.style('opacity', '0')
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
nodesSelection
|
|
374
|
-
.append('text')
|
|
375
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
376
|
-
.attr('dy', (d: any) => (d.children ? -15 : 25))
|
|
377
|
-
.attr('text-anchor', 'middle')
|
|
378
|
-
.attr('font-size', '12px')
|
|
379
|
-
.attr('fill', '#1e293b')
|
|
380
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
381
|
-
.text((d: any) =>
|
|
382
|
-
d.data.name.length > 30 ? `${d.data.name.slice(0, 27)}...` : d.data.name,
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
svg.on('dblclick.zoom', null) // Remove default double click zoom
|
|
386
|
-
svg.on('dblclick', () => {
|
|
387
|
-
// Custom double click to reset highlights
|
|
388
|
-
svg.selectAll('.link').attr('stroke-opacity', 1).attr('stroke-width', 2)
|
|
389
|
-
svg.selectAll('.node').attr('opacity', 1)
|
|
390
|
-
selectedNode = null
|
|
391
|
-
highlightedPath = []
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
if (treeRoot.y !== undefined && treeRoot.x !== undefined) {
|
|
395
|
-
const originX = treeRoot.y
|
|
396
|
-
const originY = treeRoot.x
|
|
397
|
-
const initialTransform = d3.zoomIdentity.translate(
|
|
398
|
-
width / 2 - originX - margin.left,
|
|
399
|
-
height / 2 - originY - margin.top,
|
|
400
|
-
)
|
|
401
|
-
svg.call(zoomBehavior.transform, initialTransform)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
$: if (svgEl && !treeRenderingDone) {
|
|
407
|
-
renderGraph()
|
|
408
|
-
treeRenderingDone = true
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Zooms the tree visualization in by 20%.
|
|
413
|
-
*
|
|
414
|
-
* @example
|
|
415
|
-
* ```typescript
|
|
416
|
-
* handleZoomIn() // increases zoom level
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
function handleZoomIn() {
|
|
420
|
-
if (!svgEl || !zoomBehavior) return
|
|
421
|
-
d3.select(svgEl).transition().duration(250).call(zoomBehavior.scaleBy, 1.2)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Zooms the tree visualization out by 20%.
|
|
426
|
-
*
|
|
427
|
-
* @example
|
|
428
|
-
* ```typescript
|
|
429
|
-
* handleZoomOut() // decreases zoom level
|
|
430
|
-
* ```
|
|
431
|
-
*/
|
|
432
|
-
function handleZoomOut() {
|
|
433
|
-
if (!svgEl || !zoomBehavior) return
|
|
434
|
-
d3.select(svgEl)
|
|
435
|
-
.transition()
|
|
436
|
-
.duration(250)
|
|
437
|
-
.call(zoomBehavior.scaleBy, 1 / 1.2)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Resets the tree visualization zoom and position to the default centered view.
|
|
442
|
-
*
|
|
443
|
-
* @example
|
|
444
|
-
* ```typescript
|
|
445
|
-
* handleReset() // resets zoom to default
|
|
446
|
-
* ```
|
|
447
|
-
*/
|
|
448
|
-
function handleReset() {
|
|
449
|
-
if (!svgEl || !zoomBehavior) return
|
|
450
|
-
const width = svgEl.clientWidth
|
|
451
|
-
const height = svgEl.clientHeight
|
|
452
|
-
const margin = { top: 50, right: 120, bottom: 50, left: 120 }
|
|
453
|
-
|
|
454
|
-
const originX = width / 2 - margin.left
|
|
455
|
-
const originY = height / 2 - margin.top
|
|
456
|
-
const initialTransform = d3.zoomIdentity.translate(originX, originY)
|
|
457
|
-
|
|
458
|
-
d3.select(svgEl).transition().duration(750).call(zoomBehavior.transform, initialTransform)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Closes the node details panel and resets all link/node highlights.
|
|
463
|
-
*
|
|
464
|
-
* @example
|
|
465
|
-
* ```typescript
|
|
466
|
-
* closeNodeDetails() // resets selection and highlighting
|
|
467
|
-
* ```
|
|
468
|
-
*/
|
|
469
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future node details panel
|
|
470
|
-
function closeNodeDetails() {
|
|
471
|
-
selectedNode = null
|
|
472
|
-
if (svgEl) {
|
|
473
|
-
const svg = d3.select(svgEl)
|
|
474
|
-
svg.selectAll('.link').attr('stroke-opacity', 1).attr('stroke-width', 2)
|
|
475
|
-
svg.selectAll('.node').attr('opacity', 1)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
highlightedPath = []
|
|
479
|
-
}
|
|
480
|
-
</script>
|
|
481
|
-
|
|
482
|
-
{#if loading}
|
|
483
|
-
<Stack direction="horizontal" align="center" justify="center" fill>
|
|
484
|
-
<Spinner label="Loading visualization" />
|
|
485
|
-
</Stack>
|
|
486
|
-
{:else if error}
|
|
487
|
-
<Stack direction="horizontal" align="center" justify="center" fill>
|
|
488
|
-
<Text color="error">{error}</Text>
|
|
489
|
-
</Stack>
|
|
490
|
-
{:else}
|
|
491
|
-
<VisualizationContainer>
|
|
492
|
-
{#snippet controls()}
|
|
493
|
-
<Button variant="secondary" size="small" onclick={handleZoomIn}>Zoom in</Button>
|
|
494
|
-
<Button variant="secondary" size="small" onclick={handleZoomOut}>Zoom out</Button>
|
|
495
|
-
<Button variant="secondary" size="small" onclick={handleReset}>Reset</Button>
|
|
496
|
-
<Badge status="default" size="small">{Math.round(zoom * 100)}%</Badge>
|
|
497
|
-
{/snippet}
|
|
498
|
-
<svg bind:this={svgEl}></svg>
|
|
499
|
-
</VisualizationContainer>
|
|
500
|
-
{/if}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/** @file Screen listing all registered experiences as tabs in the devtools panel. */
|
|
3
|
-
import Tabs from '../../components/Tabs.svelte'
|
|
4
|
-
import { getPicked } from '../../../../hooks'
|
|
5
|
-
import ExperienceScreen from './ExperienceScreen.svelte'
|
|
6
|
-
|
|
7
|
-
const client = getPicked()
|
|
8
|
-
|
|
9
|
-
const experiences = client.experiences.listStore
|
|
10
|
-
$: tabs = $experiences.map((experience) => {
|
|
11
|
-
return {
|
|
12
|
-
id: experience.id,
|
|
13
|
-
label: experience.name,
|
|
14
|
-
component: ExperienceScreen,
|
|
15
|
-
props: {
|
|
16
|
-
experience,
|
|
17
|
-
},
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<Tabs {tabs} />
|