pict-section-flow 0.0.2 → 0.0.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.
Files changed (38) hide show
  1. package/.claude/launch.json +11 -0
  2. package/docs/README.md +51 -0
  3. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
  4. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
  8. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
  9. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
  10. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
  11. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
  12. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
  13. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
  14. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
  15. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
  16. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
  17. package/package.json +2 -2
  18. package/source/Pict-Section-Flow.js +8 -1
  19. package/source/PictFlowCard.js +49 -1
  20. package/source/providers/PictProvider-Flow-CSS.js +1440 -0
  21. package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
  22. package/source/providers/PictProvider-Flow-Geometry.js +43 -0
  23. package/source/providers/PictProvider-Flow-Icons.js +335 -0
  24. package/source/providers/PictProvider-Flow-Layouts.js +214 -2
  25. package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
  26. package/source/providers/PictProvider-Flow-Noise.js +241 -0
  27. package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
  28. package/source/providers/PictProvider-Flow-Theme.js +755 -0
  29. package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
  30. package/source/services/PictService-Flow-PanelManager.js +188 -0
  31. package/source/services/PictService-Flow-SelectionManager.js +109 -0
  32. package/source/services/PictService-Flow-Tether.js +52 -25
  33. package/source/services/PictService-Flow-ViewportManager.js +176 -0
  34. package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
  35. package/source/views/PictView-Flow-Node.js +654 -169
  36. package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
  37. package/source/views/PictView-Flow-Toolbar.js +846 -379
  38. package/source/views/PictView-Flow.js +279 -671
@@ -0,0 +1,335 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictProvider-Flow-Icons
5
+ *
6
+ * Centralized SVG icon provider for the flow diagram.
7
+ * All icons use a duotone style: 2px outline (#2c3e50) with
8
+ * subtle filled accent shapes (#d5e8f7).
9
+ *
10
+ * Each icon is registered as a pict template with hash `Flow-Icon-{key}`,
11
+ * making them individually overridable by consumers.
12
+ */
13
+
14
+ const _ProviderConfiguration =
15
+ {
16
+ ProviderIdentifier: 'PictProviderFlowIcons'
17
+ };
18
+
19
+ // ── Default Icon SVG Markup ────────────────────────────────────────────────
20
+ // All icons: viewBox="0 0 24 24", duotone style
21
+ // Accent fill: #d5e8f7, Stroke: #2c3e50, Stroke-width: 2
22
+ //
23
+ // The {FlowIconSize} placeholder is replaced at render time with the
24
+ // requested pixel size. Each template is a self-contained <svg> element.
25
+
26
+ const _DefaultIcons =
27
+ {
28
+ // ── FlowCard Icons ─────────────────────────────────────────────────────
29
+
30
+ 'ITE': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="6" r="3" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><circle cx="6" cy="18" r="2.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><circle cx="18" cy="18" r="2.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M12 9v2M9.5 12.5L6 15.5M14.5 12.5L18 15.5" stroke="#2c3e50" stroke-width="2"/></svg>',
31
+
32
+ 'SW': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M3 12h5M16 12h5M14.8 9.2l3.7-5.2M14.8 14.8l3.7 5.2" stroke="#2c3e50" stroke-width="2"/></svg>',
33
+
34
+ 'EACH': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="14" y="3" width="7" height="7" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="3" y="14" width="7" height="7" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="14" y="14" width="7" height="7" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/></svg>',
35
+
36
+ 'FREAD': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9l-7-7z" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M13 2v7h7" stroke="#2c3e50" stroke-width="2"/><path d="M9 13h6M9 17h4" stroke="#2c3e50" stroke-width="2"/></svg>',
37
+
38
+ 'FWRITE': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9l-7-7z" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M13 2v7h7" stroke="#2c3e50" stroke-width="2"/><path d="M12 13v5M9.5 15.5L12 13l2.5 2.5" stroke="#2c3e50" stroke-width="2"/></svg>',
39
+
40
+ 'LOG': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><circle cx="7.5" cy="8" r="1" fill="#2c3e50"/><circle cx="7.5" cy="12" r="1" fill="#2c3e50"/><circle cx="7.5" cy="16" r="1" fill="#2c3e50"/><path d="M11 8h5.5M11 12h5.5M11 16h3.5" stroke="#2c3e50" stroke-width="2"/></svg>',
41
+
42
+ 'GET': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M21 21l-5.15-5.15" stroke="#2c3e50" stroke-width="2"/></svg>',
43
+
44
+ 'SET': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4.5 1.5L4 16Z" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M14 6l3 3" stroke="#2c3e50" stroke-width="2"/></svg>',
45
+
46
+ // ── UI Icons ───────────────────────────────────────────────────────────
47
+
48
+ 'fullscreen': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',
49
+
50
+ 'exit-fullscreen': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>',
51
+
52
+ 'close': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
53
+
54
+ 'chevron-down': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
55
+
56
+ // ── Toolbar & Popup Icons ─────────────────────────────────────────────
57
+
58
+ 'search': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M21 21l-4.35-4.35" stroke="#2c3e50" stroke-width="2"/></svg>',
59
+
60
+ 'cards': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="16" height="12" rx="2" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M6 7V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-2" stroke="#2c3e50" stroke-width="2"/></svg>',
61
+
62
+ 'layout': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="10" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="13" y="3" width="8" height="6" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="3" y="15" width="8" height="6" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="13" y="11" width="8" height="10" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/></svg>',
63
+
64
+ 'collapse': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M8 12h8" stroke="#2c3e50" stroke-width="2"/></svg>',
65
+
66
+ 'expand': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M12 8v8M8 12h8" stroke="#2c3e50" stroke-width="2"/></svg>',
67
+
68
+ 'grip': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="#2c3e50"><circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/></svg>',
69
+
70
+ 'settings': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3" fill="#d5e8f7"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
71
+
72
+ 'plus': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M12 8v8M8 12h8" stroke="#2c3e50" stroke-width="2"/></svg>',
73
+
74
+ 'trash': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18" stroke="#2c3e50" stroke-width="2"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="#2c3e50" stroke-width="2"/><rect x="5" y="6" width="14" height="14" rx="2" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M10 11v6M14 11v6" stroke="#2c3e50" stroke-width="2"/></svg>',
75
+
76
+ 'save': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M17 21v-8H7v8" stroke="#2c3e50" stroke-width="2"/><path d="M7 3v5h8" stroke="#2c3e50" stroke-width="2"/></svg>',
77
+
78
+ 'auto-layout': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="6" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="2" y="16" width="8" height="6" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><rect x="14" y="16" width="8" height="6" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M12 8v4M6 16v-4h12v4" stroke="#2c3e50" stroke-width="2"/></svg>',
79
+
80
+ 'zoom-in': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M21 21l-4.35-4.35" stroke="#2c3e50" stroke-width="2"/><path d="M11 8v6M8 11h6" stroke="#2c3e50" stroke-width="2"/></svg>',
81
+
82
+ 'zoom-out': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M21 21l-4.35-4.35" stroke="#2c3e50" stroke-width="2"/><path d="M8 11h6" stroke="#2c3e50" stroke-width="2"/></svg>',
83
+
84
+ 'zoom-fit': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V3h4"/><path d="M17 3h4v4"/><path d="M21 17v4h-4"/><path d="M7 21H3v-4"/><rect x="7" y="7" width="10" height="10" rx="1.5" fill="#d5e8f7"/></svg>',
85
+
86
+ 'dock': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="4" rx="1.5" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M12 20V11M8 14l4-4 4 4" stroke="#2c3e50" stroke-width="2"/></svg>',
87
+
88
+ 'restore': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="#2c3e50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>',
89
+
90
+ 'delete-node': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18" stroke="#2c3e50" stroke-width="2"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="#2c3e50" stroke-width="2"/><rect x="5" y="6" width="14" height="14" rx="2" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><path d="M10 11v6M14 11v6" stroke="#2c3e50" stroke-width="2"/></svg>',
91
+
92
+ // ── Fallback ───────────────────────────────────────────────────────────
93
+
94
+ 'default': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4" fill="#d5e8f7" stroke="#2c3e50" stroke-width="2"/><circle cx="12" cy="12" r="2.5" fill="#2c3e50"/></svg>'
95
+ };
96
+
97
+ class PictProviderFlowIcons extends libFableServiceProviderBase
98
+ {
99
+ constructor(pFable, pOptions, pServiceHash)
100
+ {
101
+ let tmpOptions = Object.assign({}, _ProviderConfiguration, pOptions);
102
+ super(pFable, tmpOptions, pServiceHash);
103
+
104
+ this.serviceType = 'PictProviderFlowIcons';
105
+
106
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
107
+
108
+ // Deep copy the default icons
109
+ this._Icons = JSON.parse(JSON.stringify(_DefaultIcons));
110
+
111
+ // Merge any additional icons passed via options
112
+ if (pOptions && pOptions.AdditionalIcons && typeof pOptions.AdditionalIcons === 'object')
113
+ {
114
+ let tmpKeys = Object.keys(pOptions.AdditionalIcons);
115
+ for (let i = 0; i < tmpKeys.length; i++)
116
+ {
117
+ this._Icons[tmpKeys[i]] = pOptions.AdditionalIcons[tmpKeys[i]];
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Register all icons as pict templates with hash Flow-Icon-{key}.
124
+ * Consumers can override any icon by registering a template with
125
+ * the same hash before the flow view renders.
126
+ */
127
+ registerIconTemplates()
128
+ {
129
+ if (!this.fable || !this.fable.TemplateProvider)
130
+ {
131
+ this.log.warn('PictProviderFlowIcons: TemplateProvider not available; icon templates not registered.');
132
+ return;
133
+ }
134
+
135
+ let tmpKeys = Object.keys(this._Icons);
136
+ for (let i = 0; i < tmpKeys.length; i++)
137
+ {
138
+ let tmpHash = 'Flow-Icon-' + tmpKeys[i];
139
+
140
+ // Only register if not already present (allow consumer overrides)
141
+ if (!this.fable.TemplateProvider.getTemplate(tmpHash))
142
+ {
143
+ this.fable.TemplateProvider.addTemplate(tmpHash, this._Icons[tmpKeys[i]]);
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Determine if the given icon string is an emoji (legacy) or an icon key.
150
+ * Returns true if the string contains characters with code points above U+00FF,
151
+ * indicating emoji or Unicode symbol characters.
152
+ *
153
+ * @param {string} pIconValue - The icon value to check
154
+ * @returns {boolean}
155
+ */
156
+ isEmojiIcon(pIconValue)
157
+ {
158
+ if (!pIconValue || typeof pIconValue !== 'string')
159
+ {
160
+ return false;
161
+ }
162
+
163
+ for (let i = 0; i < pIconValue.length; i++)
164
+ {
165
+ if (pIconValue.charCodeAt(i) > 255)
166
+ {
167
+ return true;
168
+ }
169
+ }
170
+
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Resolve the icon key to use for a given CardMetadata object.
176
+ * Tries Icon field first, then Code field, then falls back to 'default'.
177
+ *
178
+ * @param {Object} pCardMetadata - The CardMetadata object
179
+ * @returns {string} The icon key
180
+ */
181
+ resolveIconKey(pCardMetadata)
182
+ {
183
+ if (!pCardMetadata)
184
+ {
185
+ return 'default';
186
+ }
187
+
188
+ // If Icon is a known key, use it
189
+ if (pCardMetadata.Icon && this._Icons.hasOwnProperty(pCardMetadata.Icon))
190
+ {
191
+ return pCardMetadata.Icon;
192
+ }
193
+
194
+ // If Icon is a non-emoji string, check if it matches a registered template
195
+ if (pCardMetadata.Icon && !this.isEmojiIcon(pCardMetadata.Icon))
196
+ {
197
+ return pCardMetadata.Icon;
198
+ }
199
+
200
+ // Fall back to Code field
201
+ if (pCardMetadata.Code && this._Icons.hasOwnProperty(pCardMetadata.Code))
202
+ {
203
+ return pCardMetadata.Code;
204
+ }
205
+
206
+ return 'default';
207
+ }
208
+
209
+ /**
210
+ * Get the raw SVG markup string for a given icon key, with size applied.
211
+ *
212
+ * @param {string} pIconKey - The icon key
213
+ * @param {number} pSize - Pixel size (default 16)
214
+ * @returns {string} The SVG markup string
215
+ */
216
+ getIconSVGMarkup(pIconKey, pSize)
217
+ {
218
+ let tmpSize = pSize || 16;
219
+ let tmpKey = pIconKey || 'default';
220
+ let tmpMarkup = this._Icons[tmpKey] || this._Icons['default'];
221
+
222
+ // Replace the size placeholder
223
+ return tmpMarkup.replace(/\{FlowIconSize\}/g, String(tmpSize));
224
+ }
225
+
226
+ /**
227
+ * Render an icon into an SVG canvas context using createElementNS.
228
+ * Creates a <g> element containing the icon paths, positioned at (pX, pY)
229
+ * with the given size via a scale transform.
230
+ *
231
+ * @param {string} pIconKey - The icon key
232
+ * @param {SVGElement} pParentGroup - The parent SVG group to append to
233
+ * @param {number} pX - X position (top-left of icon bounding box)
234
+ * @param {number} pY - Y position (top-left of icon bounding box)
235
+ * @param {number} pSize - Pixel size (default 16)
236
+ * @returns {SVGGElement|null} The created group element, or null on failure
237
+ */
238
+ renderIconIntoSVGGroup(pIconKey, pParentGroup, pX, pY, pSize)
239
+ {
240
+ if (!pParentGroup)
241
+ {
242
+ return null;
243
+ }
244
+
245
+ let tmpSize = pSize || 16;
246
+ let tmpScale = tmpSize / 24;
247
+ let tmpKey = pIconKey || 'default';
248
+ let tmpMarkup = this._Icons[tmpKey] || this._Icons['default'];
249
+
250
+ // Replace size placeholder (for consistency, though we scale via transform)
251
+ tmpMarkup = tmpMarkup.replace(/\{FlowIconSize\}/g, '24');
252
+
253
+ try
254
+ {
255
+ // Create a temporary SVG element to parse the icon markup
256
+ let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
257
+ tmpTempSVG.innerHTML = tmpMarkup;
258
+
259
+ // Find the inner SVG (the icon's root <svg> element)
260
+ let tmpInnerSVG = tmpTempSVG.querySelector('svg');
261
+ if (!tmpInnerSVG)
262
+ {
263
+ return null;
264
+ }
265
+
266
+ // Create a group to hold the icon content
267
+ let tmpGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
268
+ tmpGroup.setAttribute('transform', 'translate(' + pX + ',' + pY + ') scale(' + tmpScale + ')');
269
+ tmpGroup.setAttribute('pointer-events', 'none');
270
+ tmpGroup.setAttribute('class', 'pict-flow-icon-svg');
271
+
272
+ // Move all children from the parsed SVG into the group
273
+ while (tmpInnerSVG.childNodes.length > 0)
274
+ {
275
+ tmpGroup.appendChild(tmpInnerSVG.childNodes[0]);
276
+ }
277
+
278
+ pParentGroup.appendChild(tmpGroup);
279
+ return tmpGroup;
280
+ }
281
+ catch (pError)
282
+ {
283
+ this.log.warn('PictProviderFlowIcons renderIconIntoSVGGroup error: ' + pError.message);
284
+ return null;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Get all registered icon keys.
290
+ * @returns {Array<string>}
291
+ */
292
+ getIconKeys()
293
+ {
294
+ return Object.keys(this._Icons);
295
+ }
296
+
297
+ /**
298
+ * Check if a given key has a registered icon.
299
+ * @param {string} pIconKey
300
+ * @returns {boolean}
301
+ */
302
+ hasIcon(pIconKey)
303
+ {
304
+ return this._Icons.hasOwnProperty(pIconKey);
305
+ }
306
+
307
+ /**
308
+ * Register a new icon or override an existing one.
309
+ * @param {string} pIconKey - The icon key
310
+ * @param {string} pSVGMarkup - The SVG markup string (must contain {FlowIconSize} placeholders)
311
+ * @returns {boolean}
312
+ */
313
+ registerIcon(pIconKey, pSVGMarkup)
314
+ {
315
+ if (!pIconKey || !pSVGMarkup)
316
+ {
317
+ return false;
318
+ }
319
+
320
+ this._Icons[pIconKey] = pSVGMarkup;
321
+
322
+ // Also update the pict template if TemplateProvider is available
323
+ if (this.fable && this.fable.TemplateProvider)
324
+ {
325
+ this.fable.TemplateProvider.addTemplate('Flow-Icon-' + pIconKey, pSVGMarkup);
326
+ }
327
+
328
+ return true;
329
+ }
330
+ }
331
+
332
+ module.exports = PictProviderFlowIcons;
333
+
334
+ module.exports.default_configuration = _ProviderConfiguration;
335
+ module.exports.DefaultIcons = _DefaultIcons;
@@ -13,6 +13,22 @@ const _ProviderConfiguration =
13
13
  * still exist are placed at their saved positions and any new nodes are
14
14
  * auto-laid-out to the right.
15
15
  *
16
+ * ## Persistence
17
+ *
18
+ * By default, layouts are persisted to the browser's `localStorage` using a
19
+ * key derived from the flow view identifier. This means layouts survive page
20
+ * refreshes out of the box.
21
+ *
22
+ * Developers can override the storage backend (e.g., to use a REST API or
23
+ * IndexedDB) by replacing the three storage hook methods on the instance or
24
+ * in a subclass:
25
+ *
26
+ * - `storageWrite(pLayouts, fCallback)` — persist the full layout array
27
+ * - `storageRead(fCallback)` — load the persisted layout array
28
+ * - `storageDelete(fCallback)` — remove all persisted layouts
29
+ *
30
+ * Each callback follows the Node convention: `fCallback(pError, pResult)`.
31
+ *
16
32
  * Saved layout data structure:
17
33
  * {
18
34
  * Hash: "layout-<UUID>",
@@ -33,8 +49,174 @@ class PictProviderFlowLayouts extends libPictProvider
33
49
  this.serviceType = 'PictProviderFlowLayouts';
34
50
 
35
51
  this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
52
+
53
+ // Storage key for localStorage persistence.
54
+ // Defaults to a key derived from the FlowView identifier, or can be
55
+ // set via options.StorageKey. Pass `false` to disable localStorage.
56
+ if (pOptions && pOptions.StorageKey !== undefined)
57
+ {
58
+ this._StorageKey = pOptions.StorageKey;
59
+ }
60
+ else if (this._FlowView && this._FlowView.options && this._FlowView.options.ViewIdentifier)
61
+ {
62
+ this._StorageKey = `pict-flow-layouts-${this._FlowView.options.ViewIdentifier}`;
63
+ }
64
+ else
65
+ {
66
+ this._StorageKey = 'pict-flow-layouts';
67
+ }
68
+ }
69
+
70
+ // ── Storage Hooks ─────────────────────────────────────────────────────
71
+ // These three methods form the persistence contract. The default
72
+ // implementation uses localStorage. Override them on the instance or
73
+ // subclass to use a REST API, IndexedDB, or any other backend.
74
+ //
75
+ // All callbacks follow `fCallback(pError, pResult)`.
76
+
77
+ /**
78
+ * Persist the full array of layout objects.
79
+ *
80
+ * Default implementation writes JSON to `localStorage`.
81
+ *
82
+ * @param {Array} pLayouts - The array of layout objects to persist
83
+ * @param {Function} fCallback - `function(pError)` called when done
84
+ */
85
+ storageWrite(pLayouts, fCallback)
86
+ {
87
+ if (this._StorageKey === false)
88
+ {
89
+ return fCallback(null);
90
+ }
91
+ try
92
+ {
93
+ if (typeof localStorage !== 'undefined')
94
+ {
95
+ localStorage.setItem(this._StorageKey, JSON.stringify(pLayouts));
96
+ }
97
+ return fCallback(null);
98
+ }
99
+ catch (pError)
100
+ {
101
+ this.log.warn(`PictProviderFlowLayouts storageWrite error: ${pError.message}`);
102
+ return fCallback(pError);
103
+ }
36
104
  }
37
105
 
106
+ /**
107
+ * Load the persisted array of layout objects.
108
+ *
109
+ * Default implementation reads JSON from `localStorage`.
110
+ *
111
+ * @param {Function} fCallback - `function(pError, pLayouts)` where
112
+ * pLayouts is an Array (or null/empty if nothing stored)
113
+ */
114
+ storageRead(fCallback)
115
+ {
116
+ if (this._StorageKey === false)
117
+ {
118
+ return fCallback(null, []);
119
+ }
120
+ try
121
+ {
122
+ if (typeof localStorage !== 'undefined')
123
+ {
124
+ let tmpRaw = localStorage.getItem(this._StorageKey);
125
+ if (tmpRaw)
126
+ {
127
+ let tmpParsed = JSON.parse(tmpRaw);
128
+ if (Array.isArray(tmpParsed))
129
+ {
130
+ return fCallback(null, tmpParsed);
131
+ }
132
+ }
133
+ }
134
+ return fCallback(null, []);
135
+ }
136
+ catch (pError)
137
+ {
138
+ this.log.warn(`PictProviderFlowLayouts storageRead error: ${pError.message}`);
139
+ return fCallback(pError, []);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Remove all persisted layout data.
145
+ *
146
+ * Default implementation removes the key from `localStorage`.
147
+ *
148
+ * @param {Function} fCallback - `function(pError)` called when done
149
+ */
150
+ storageDelete(fCallback)
151
+ {
152
+ if (this._StorageKey === false)
153
+ {
154
+ return fCallback(null);
155
+ }
156
+ try
157
+ {
158
+ if (typeof localStorage !== 'undefined')
159
+ {
160
+ localStorage.removeItem(this._StorageKey);
161
+ }
162
+ return fCallback(null);
163
+ }
164
+ catch (pError)
165
+ {
166
+ this.log.warn(`PictProviderFlowLayouts storageDelete error: ${pError.message}`);
167
+ return fCallback(pError);
168
+ }
169
+ }
170
+
171
+ // ── Initialization ────────────────────────────────────────────────────
172
+
173
+ /**
174
+ * Load persisted layouts and merge them into _FlowData.SavedLayouts.
175
+ * Layouts already present in _FlowData (e.g., from setFlowData) are
176
+ * kept; persisted layouts with new hashes are appended.
177
+ *
178
+ * Call this after _FlowData is populated.
179
+ */
180
+ loadPersistedLayouts()
181
+ {
182
+ this.storageRead((pError, pLayouts) =>
183
+ {
184
+ if (pError || !Array.isArray(pLayouts) || pLayouts.length === 0)
185
+ {
186
+ return;
187
+ }
188
+
189
+ if (!this._FlowView || !this._FlowView._FlowData)
190
+ {
191
+ return;
192
+ }
193
+
194
+ let tmpExisting = this._FlowView._FlowData.SavedLayouts;
195
+ let tmpExistingHashes = {};
196
+ for (let i = 0; i < tmpExisting.length; i++)
197
+ {
198
+ tmpExistingHashes[tmpExisting[i].Hash] = true;
199
+ }
200
+
201
+ let tmpAdded = 0;
202
+ for (let i = 0; i < pLayouts.length; i++)
203
+ {
204
+ if (!tmpExistingHashes[pLayouts[i].Hash])
205
+ {
206
+ tmpExisting.push(pLayouts[i]);
207
+ tmpAdded++;
208
+ }
209
+ }
210
+
211
+ if (tmpAdded > 0)
212
+ {
213
+ this.log.trace(`PictProviderFlowLayouts loaded ${tmpAdded} persisted layout(s)`);
214
+ }
215
+ });
216
+ }
217
+
218
+ // ── Public API ────────────────────────────────────────────────────────
219
+
38
220
  /**
39
221
  * Save the current node and panel positions as a named layout.
40
222
  * @param {string} pName - The display name for this layout
@@ -53,7 +235,7 @@ class PictProviderFlowLayouts extends libPictProvider
53
235
  let tmpNodePositions = {};
54
236
  let tmpPanelPositions = {};
55
237
 
56
- // Capture node positions (arrangement only, no content)
238
+ // Capture node positions and per-instance overrides (Title, Style)
57
239
  for (let i = 0; i < tmpFlowData.Nodes.length; i++)
58
240
  {
59
241
  let tmpNode = tmpFlowData.Nodes[i];
@@ -62,8 +244,15 @@ class PictProviderFlowLayouts extends libPictProvider
62
244
  X: tmpNode.X,
63
245
  Y: tmpNode.Y,
64
246
  Width: tmpNode.Width,
65
- Height: tmpNode.Height
247
+ Height: tmpNode.Height,
248
+ Title: tmpNode.Title
66
249
  };
250
+
251
+ // Only include Style if it has been customized
252
+ if (tmpNode.Style && Object.keys(tmpNode.Style).length > 0)
253
+ {
254
+ tmpNodePositions[tmpNode.Hash].Style = JSON.parse(JSON.stringify(tmpNode.Style));
255
+ }
67
256
  }
68
257
 
69
258
  // Capture panel positions keyed by NodeHash (panels get new hashes on each open)
@@ -97,6 +286,15 @@ class PictProviderFlowLayouts extends libPictProvider
97
286
  tmpFlowData.SavedLayouts.push(tmpLayout);
98
287
  this._FlowView.marshalFromView();
99
288
 
289
+ // Persist to storage
290
+ this.storageWrite(tmpFlowData.SavedLayouts, (pError) =>
291
+ {
292
+ if (pError)
293
+ {
294
+ this.log.warn(`PictProviderFlowLayouts: failed to persist after save: ${pError.message}`);
295
+ }
296
+ });
297
+
100
298
  if (this._FlowView._EventHandlerProvider)
101
299
  {
102
300
  this._FlowView._EventHandlerProvider.fireEvent('onLayoutSaved', tmpLayout);
@@ -150,6 +348,11 @@ class PictProviderFlowLayouts extends libPictProvider
150
348
  tmpNode.Y = tmpSaved.Y;
151
349
  if (typeof tmpSaved.Width === 'number') tmpNode.Width = tmpSaved.Width;
152
350
  if (typeof tmpSaved.Height === 'number') tmpNode.Height = tmpSaved.Height;
351
+ if (typeof tmpSaved.Title === 'string') tmpNode.Title = tmpSaved.Title;
352
+ if (tmpSaved.Style && typeof tmpSaved.Style === 'object')
353
+ {
354
+ tmpNode.Style = JSON.parse(JSON.stringify(tmpSaved.Style));
355
+ }
153
356
  tmpMatchedNodes.push(tmpNode);
154
357
  }
155
358
  else
@@ -244,6 +447,15 @@ class PictProviderFlowLayouts extends libPictProvider
244
447
  let tmpRemovedLayout = tmpFlowData.SavedLayouts.splice(tmpIndex, 1)[0];
245
448
  this._FlowView.marshalFromView();
246
449
 
450
+ // Persist to storage (with the layout removed)
451
+ this.storageWrite(tmpFlowData.SavedLayouts, (pError) =>
452
+ {
453
+ if (pError)
454
+ {
455
+ this.log.warn(`PictProviderFlowLayouts: failed to persist after delete: ${pError.message}`);
456
+ }
457
+ });
458
+
247
459
  if (this._FlowView._EventHandlerProvider)
248
460
  {
249
461
  this._FlowView._EventHandlerProvider.fireEvent('onLayoutDeleted', tmpRemovedLayout);
@@ -30,9 +30,7 @@ const _DefaultNodeTypes =
30
30
  BodyStyle:
31
31
  {
32
32
  'fill': '#eafaf1',
33
- 'stroke': '#27ae60',
34
- 'rx': '25',
35
- 'ry': '25'
33
+ 'stroke': '#27ae60'
36
34
  }
37
35
  },
38
36
  'end':
@@ -45,13 +43,28 @@ const _DefaultNodeTypes =
45
43
  [
46
44
  { Hash: null, Direction: 'input', Side: 'left', Label: 'In' }
47
45
  ],
46
+ TitleBarColor: '#1abc9c',
47
+ BodyStyle:
48
+ {
49
+ 'fill': '#e8f8f5',
50
+ 'stroke': '#1abc9c'
51
+ }
52
+ },
53
+ 'halt':
54
+ {
55
+ Hash: 'halt',
56
+ Label: 'Halt',
57
+ DefaultWidth: 140,
58
+ DefaultHeight: 80,
59
+ DefaultPorts:
60
+ [
61
+ { Hash: null, Direction: 'input', Side: 'left', Label: 'In' }
62
+ ],
48
63
  TitleBarColor: '#e74c3c',
49
64
  BodyStyle:
50
65
  {
51
66
  'fill': '#fdedec',
52
- 'stroke': '#e74c3c',
53
- 'rx': '25',
54
- 'ry': '25'
67
+ 'stroke': '#e74c3c'
55
68
  }
56
69
  },
57
70
  'decision':
@@ -100,11 +113,21 @@ class PictProviderFlowNodeTypes extends libPictProvider
100
113
  let tmpAdditionalKeys = Object.keys(pOptions.AdditionalNodeTypes);
101
114
  for (let i = 0; i < tmpAdditionalKeys.length; i++)
102
115
  {
116
+ let tmpOriginal = pOptions.AdditionalNodeTypes[tmpAdditionalKeys[i]];
103
117
  this._NodeTypes[tmpAdditionalKeys[i]] = Object.assign(
104
118
  {},
105
119
  this._NodeTypes[tmpAdditionalKeys[i]] || {},
106
- JSON.parse(JSON.stringify(pOptions.AdditionalNodeTypes[tmpAdditionalKeys[i]]))
120
+ JSON.parse(JSON.stringify(tmpOriginal))
107
121
  );
122
+ // Preserve BodyContent.RenderCallback (functions are stripped by JSON serialization)
123
+ if (tmpOriginal.BodyContent && typeof tmpOriginal.BodyContent.RenderCallback === 'function')
124
+ {
125
+ if (!this._NodeTypes[tmpAdditionalKeys[i]].BodyContent)
126
+ {
127
+ this._NodeTypes[tmpAdditionalKeys[i]].BodyContent = {};
128
+ }
129
+ this._NodeTypes[tmpAdditionalKeys[i]].BodyContent.RenderCallback = tmpOriginal.BodyContent.RenderCallback;
130
+ }
108
131
  }
109
132
  }
110
133
  }