@picked/svelte 0.0.47-next.1 → 0.0.47-next.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.
@@ -35,10 +35,62 @@
35
35
  _referenceTo?: string
36
36
  }
37
37
 
38
+ function isRecord(val: unknown): val is Record<string, unknown> {
39
+ return val !== null && typeof val === 'object' && !Array.isArray(val)
40
+ }
41
+
42
+ function formatLogicRule(rule: unknown): string {
43
+ if (rule === null || rule === undefined) return '?'
44
+ if (typeof rule === 'string') return rule
45
+ if (typeof rule === 'number' || typeof rule === 'boolean') return String(rule)
46
+ if (Array.isArray(rule)) {
47
+ return rule.map(formatLogicRule).join(', ')
48
+ }
49
+ if (isRecord(rule)) {
50
+ const keys = Object.keys(rule)
51
+ if (keys.length === 0) return 'Condition'
52
+ const op = keys[0]
53
+ const args = rule[op]
54
+
55
+ if (op === 'var') {
56
+ return String(Array.isArray(args) ? args[0] : args)
57
+ }
58
+
59
+ const compOps: Record<string, string> = {
60
+ '==': '=',
61
+ '===': '=',
62
+ '!=': '≠',
63
+ '!==': '≠',
64
+ '>': '>',
65
+ '<': '<',
66
+ '>=': '≥',
67
+ '<=': '≤',
68
+ }
69
+ if (op in compOps && Array.isArray(args) && args.length >= 2) {
70
+ return `${formatLogicRule(args[0])} ${compOps[op]} ${formatLogicRule(args[1])}`
71
+ }
72
+ if (op === 'and' && Array.isArray(args)) {
73
+ return args.map(formatLogicRule).join(' & ')
74
+ }
75
+ if (op === 'or' && Array.isArray(args)) {
76
+ return args.map(formatLogicRule).join(' | ')
77
+ }
78
+ if ((op === '!' || op === 'not') && Array.isArray(args)) {
79
+ return `!${formatLogicRule(args[0])}`
80
+ }
81
+ if (op === 'in' && Array.isArray(args) && args.length >= 2) {
82
+ return `${formatLogicRule(args[0])} in ${formatLogicRule(args[1])}`
83
+ }
84
+ return `${op}(?)`
85
+ }
86
+ return '?'
87
+ }
88
+
38
89
  function renderGraph() {
39
90
  d3.select(svgEl).selectAll('*').remove()
40
91
 
41
92
  const stepsMap = new Map(steps.map((s) => [s.id, s]))
93
+ const conditionalsMap = new Map(conditionals.map((c) => [c.id, c]))
42
94
  const outgoingEdges = new Map<string, ExperienceEdge[]>()
43
95
  edges.forEach((e) => {
44
96
  if (!outgoingEdges.has(e.sourceNodeId)) outgoingEdges.set(e.sourceNodeId, [])
@@ -49,7 +101,12 @@
49
101
  if (node.type === 'origin') return 'Start'
50
102
  if (node.type === 'step' && node.stepId)
51
103
  return stepsMap.get(node.stepId)?.label ?? 'Unknown Step'
52
- if (node.type === 'conditional' && node.conditionalId) return `Condition`
104
+ if (node.type === 'conditional' && node.conditionalId) {
105
+ const conditional = conditionalsMap.get(node.conditionalId)
106
+ if (conditional?.label) return conditional.label
107
+ if (conditional?.condition) return formatLogicRule(conditional.condition)
108
+ return 'Condition'
109
+ }
53
110
  return 'Unknown Node'
54
111
  }
55
112
 
@@ -81,15 +138,16 @@
81
138
  }
82
139
 
83
140
  if (rootNode) {
84
- const convertGraphToTree = (
85
- node: ProcessedNode,
86
- visited = new Set<string>(),
87
- ): ProcessedNode => {
88
- if (visited.has(node.id)) {
141
+ // Use a global visited set so join nodes (multiple incoming branches converging
142
+ // to the same node) only appear once in the tree, with reference nodes on
143
+ // subsequent visits to indicate the convergence point.
144
+ const globalVisited = new Set<string>()
145
+ const convertGraphToTree = (node: ProcessedNode): ProcessedNode => {
146
+ if (globalVisited.has(node.id)) {
89
147
  return {
90
148
  ...node,
91
149
  id: `${node.id}_ref`,
92
- name: `${node.name} (ref)`,
150
+ name: `↩ ${node.name}`,
93
151
  type: `${node.type}_ref`,
94
152
  children: [],
95
153
  _isReference: true,
@@ -97,9 +155,8 @@
97
155
  }
98
156
  }
99
157
 
100
- const newVisited = new Set(visited)
101
- newVisited.add(node.id)
102
- return { ...node, children: node.children.map((c) => convertGraphToTree(c, newVisited)) }
158
+ globalVisited.add(node.id)
159
+ return { ...node, children: node.children.map((c) => convertGraphToTree(c)) }
103
160
  }
104
161
 
105
162
  const treeData = convertGraphToTree(rootNode)
@@ -128,31 +185,75 @@
128
185
 
129
186
  const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
130
187
 
188
+ const linkGenerator = d3
189
+ .linkHorizontal<d3.HierarchyPointLink<ProcessedNode>, ProcessedNode>()
190
+ .x((d) => d.y)
191
+ .y((d) => d.x)
192
+
131
193
  g.selectAll('.link')
132
194
  .data(treeRoot.links())
133
195
  .enter()
134
196
  .append('path')
135
197
  .attr('class', 'link')
136
198
  .attr('fill', 'none')
137
- .attr('stroke', '#94a3b8')
199
+ .attr('stroke', (d: any) => {
200
+ if (d.source.data.type !== 'conditional') return '#94a3b8'
201
+ const targetId = d.target.data._isReference
202
+ ? d.target.data._referenceTo
203
+ : d.target.data.id
204
+ const edge = d.source.data._originalEdges.find(
205
+ (e: ExperienceEdge) => e.destinationNodeId === targetId,
206
+ )
207
+ if (edge?.sourceNodeHandle === 'true') return '#10b981'
208
+ if (edge?.sourceNodeHandle === 'false') return '#ef4444'
209
+ return '#94a3b8'
210
+ })
138
211
  .attr('stroke-width', 2)
139
- .attr(
140
- 'd',
141
- d3
142
- .linkHorizontal<d3.HierarchyPointLink<ProcessedNode>, ProcessedNode>()
143
- .x((d) => d.y)
144
- .y((d) => d.x),
145
- )
212
+ .attr('d', linkGenerator)
213
+
214
+ // Label true/false branches from conditional nodes
215
+ g.selectAll('.edge-label')
216
+ .data(treeRoot.links().filter((d: any) => d.source.data.type === 'conditional'))
217
+ .enter()
218
+ .append('text')
219
+ .attr('class', 'edge-label')
220
+ .attr('text-anchor', 'middle')
221
+ .attr('font-size', '10px')
222
+ .attr('font-weight', 'bold')
223
+ .attr('x', (d: any) => (d.source.y + d.target.y) / 2)
224
+ .attr('y', (d: any) => (d.source.x + d.target.x) / 2 - 6)
225
+ .attr('fill', (d: any) => {
226
+ const targetId = d.target.data._isReference
227
+ ? d.target.data._referenceTo
228
+ : d.target.data.id
229
+ const edge = d.source.data._originalEdges.find(
230
+ (e: ExperienceEdge) => e.destinationNodeId === targetId,
231
+ )
232
+ if (edge?.sourceNodeHandle === 'true') return '#10b981'
233
+ if (edge?.sourceNodeHandle === 'false') return '#ef4444'
234
+ return '#64748b'
235
+ })
236
+ .text((d: any) => {
237
+ const targetId = d.target.data._isReference
238
+ ? d.target.data._referenceTo
239
+ : d.target.data.id
240
+ const edge = d.source.data._originalEdges.find(
241
+ (e: ExperienceEdge) => e.destinationNodeId === targetId,
242
+ )
243
+ if (edge?.sourceNodeHandle === 'true') return 'Yes'
244
+ if (edge?.sourceNodeHandle === 'false') return 'No'
245
+ return ''
246
+ })
146
247
 
147
- const nodes = g
248
+ const nodesSelection = g
148
249
  .selectAll('.node')
149
250
  .data(treeRoot.descendants())
150
251
  .enter()
151
252
  .append('g')
152
253
  .attr('class', 'node')
153
- .attr('transform', (d: any) => `translate(${d.y},${d.x})`) // d3.HierarchyPointNode uses x, y
254
+ .attr('transform', (d: any) => `translate(${d.y},${d.x})`)
154
255
 
155
- nodes
256
+ nodesSelection
156
257
  .append('circle')
157
258
  .attr('r', (d: any) => (d.data.type === 'origin' ? 15 : 10))
158
259
  .attr('fill', (d: any) => {
@@ -164,14 +265,14 @@
164
265
  .attr('stroke', '#fff')
165
266
  .attr('stroke-width', 2)
166
267
 
167
- nodes
268
+ nodesSelection
168
269
  .append('text')
169
270
  .attr('dy', (d: any) => (d.children ? -15 : 25))
170
271
  .attr('text-anchor', 'middle')
171
272
  .attr('font-size', '12px')
172
273
  .attr('fill', '#1e293b')
173
274
  .text((d: any) =>
174
- d.data.name.length > 25 ? `${d.data.name.slice(0, 22)}...` : d.data.name,
275
+ d.data.name.length > 30 ? `${d.data.name.slice(0, 27)}...` : d.data.name,
175
276
  )
176
277
 
177
278
  svg.on('dblclick.zoom', null) // Remove default double click zoom
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picked/svelte",
3
- "version": "0.0.47-next.1",
3
+ "version": "0.0.47-next.3",
4
4
  "dependencies": {
5
5
  "d3": "^7.9.0"
6
6
  },
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "module": "./dist/index.js",
88
88
  "peerDependencies": {
89
- "@picked/client": "0.0.47-next.1",
89
+ "@picked/client": "0.0.47-next.3",
90
90
  "effector": "^23.3.0",
91
91
  "svelte": "^5.34.3"
92
92
  },