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
@@ -13,222 +13,73 @@ const _DefaultConfiguration =
13
13
 
14
14
  EnablePalette: true,
15
15
 
16
- CSS: /*css*/`
17
- .pict-flow-toolbar {
18
- display: flex;
19
- align-items: center;
20
- gap: 0.5em;
21
- padding: 0.5em 0.75em;
22
- background-color: #ffffff;
23
- border-bottom: 1px solid #e0e0e0;
24
- flex-wrap: wrap;
25
- }
26
- .pict-flow-toolbar-group {
27
- display: flex;
28
- align-items: center;
29
- gap: 0.25em;
30
- padding-right: 0.75em;
31
- border-right: 1px solid #e0e0e0;
32
- }
33
- .pict-flow-toolbar-group:last-child {
34
- border-right: none;
35
- padding-right: 0;
36
- }
37
- .pict-flow-toolbar-btn {
38
- display: inline-flex;
39
- align-items: center;
40
- justify-content: center;
41
- padding: 0.35em 0.65em;
42
- border: 1px solid #bdc3c7;
43
- border-radius: 4px;
44
- background-color: #fff;
45
- color: #2c3e50;
46
- font-size: 0.85em;
47
- cursor: pointer;
48
- transition: background-color 0.15s, border-color 0.15s;
49
- user-select: none;
50
- -webkit-user-select: none;
51
- }
52
- .pict-flow-toolbar-btn:hover {
53
- background-color: #ecf0f1;
54
- border-color: #95a5a6;
55
- }
56
- .pict-flow-toolbar-btn:active {
57
- background-color: #d5dbdb;
58
- }
59
- .pict-flow-toolbar-btn.danger {
60
- color: #e74c3c;
61
- border-color: #e74c3c;
62
- }
63
- .pict-flow-toolbar-btn.danger:hover {
64
- background-color: #fdedec;
65
- }
66
- .pict-flow-toolbar-label {
67
- font-size: 0.8em;
68
- color: #7f8c8d;
69
- margin-right: 0.25em;
70
- }
71
- .pict-flow-toolbar-select {
72
- padding: 0.3em 0.5em;
73
- border: 1px solid #bdc3c7;
74
- border-radius: 4px;
75
- font-size: 0.85em;
76
- background-color: #fff;
77
- color: #2c3e50;
78
- }
79
- .pict-flow-palette-container {
80
- border-bottom: 1px solid #e0e0e0;
81
- background-color: #fafafa;
82
- }
83
- .pict-flow-palette-toggle {
84
- display: flex;
85
- align-items: center;
86
- justify-content: space-between;
87
- padding: 0.4em 0.75em;
88
- cursor: pointer;
89
- user-select: none;
90
- -webkit-user-select: none;
91
- font-size: 0.8em;
92
- color: #7f8c8d;
93
- background-color: #f4f4f5;
94
- border-bottom: 1px solid #e0e0e0;
95
- }
96
- .pict-flow-palette-toggle:hover {
97
- background-color: #ecf0f1;
98
- color: #2c3e50;
99
- }
100
- .pict-flow-palette-toggle-arrow {
101
- font-size: 0.7em;
102
- transition: transform 0.2s;
103
- }
104
- .pict-flow-palette-toggle-arrow.open {
105
- transform: rotate(180deg);
106
- }
107
- .pict-flow-palette-body {
108
- display: none;
109
- padding: 0.5em 0.75em 0.75em 0.75em;
110
- max-height: 280px;
111
- overflow-y: auto;
112
- }
113
- .pict-flow-palette-body.open {
114
- display: block;
115
- }
116
- .pict-flow-palette-category {
117
- margin-bottom: 0.5em;
118
- }
119
- .pict-flow-palette-category:last-child {
120
- margin-bottom: 0;
121
- }
122
- .pict-flow-palette-category-label {
123
- font-size: 0.7em;
124
- font-weight: 700;
125
- text-transform: uppercase;
126
- letter-spacing: 0.05em;
127
- color: #95a5a6;
128
- margin-bottom: 0.35em;
129
- padding-bottom: 0.2em;
130
- border-bottom: 1px solid #ecf0f1;
131
- }
132
- .pict-flow-palette-cards {
133
- display: flex;
134
- flex-wrap: wrap;
135
- gap: 0.35em;
136
- }
137
- .pict-flow-palette-card {
138
- display: inline-flex;
139
- align-items: center;
140
- gap: 0.35em;
141
- padding: 0.35em 0.6em;
142
- border: 1px solid #d5d8dc;
143
- border-radius: 4px;
144
- background-color: #ffffff;
145
- font-size: 0.8em;
146
- cursor: pointer;
147
- transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
148
- user-select: none;
149
- -webkit-user-select: none;
150
- position: relative;
151
- }
152
- .pict-flow-palette-card:hover {
153
- background-color: #eaf2f8;
154
- border-color: #3498db;
155
- box-shadow: 0 1px 3px rgba(52, 152, 219, 0.15);
156
- }
157
- .pict-flow-palette-card.disabled {
158
- opacity: 0.45;
159
- pointer-events: none;
160
- cursor: default;
161
- }
162
- .pict-flow-palette-card-icon {
163
- font-size: 1.1em;
164
- line-height: 1;
165
- }
166
- .pict-flow-palette-card-swatch {
167
- width: 10px;
168
- height: 10px;
169
- border-radius: 2px;
170
- flex-shrink: 0;
171
- }
172
- .pict-flow-palette-card-title {
173
- font-weight: 500;
174
- color: #2c3e50;
175
- white-space: nowrap;
176
- }
177
- .pict-flow-palette-card-code {
178
- font-size: 0.8em;
179
- color: #95a5a6;
180
- font-family: monospace;
181
- }
182
- .pict-flow-toolbar-select.layout-select {
183
- min-width: 120px;
184
- max-width: 200px;
185
- }
186
- `,
16
+ CSS: false,
187
17
 
188
18
  Templates:
189
19
  [
190
20
  {
191
21
  Hash: 'Flow-Toolbar-Template',
192
22
  Template: /*html*/`
193
- <div class="pict-flow-toolbar">
23
+ <div class="pict-flow-toolbar" id="Flow-Toolbar-Bar-{~D:Record.FlowViewIdentifier~}">
194
24
  <div class="pict-flow-toolbar-group">
195
- <span class="pict-flow-toolbar-label">Node:</span>
196
- <select class="pict-flow-toolbar-select" id="Flow-Toolbar-NodeType-{~D:Record.FlowViewIdentifier~}">
197
- </select>
198
- <button class="pict-flow-toolbar-btn" data-flow-action="add-node">+ Add Node</button>
25
+ <button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node">
26
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-plus-{~D:Record.FlowViewIdentifier~}"></span>
27
+ <span class="pict-flow-toolbar-btn-text">Node</span>
28
+ </button>
29
+ <button class="pict-flow-toolbar-btn danger" data-flow-action="delete-selected" title="Delete Node">
30
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
31
+ </button>
199
32
  </div>
200
33
  <div class="pict-flow-toolbar-group">
201
- <button class="pict-flow-toolbar-btn danger" data-flow-action="delete-selected">Delete</button>
34
+ <button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette">
35
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-cards-{~D:Record.FlowViewIdentifier~}"></span>
36
+ <span class="pict-flow-toolbar-btn-text">Cards</span>
37
+ <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-CardsChevron-{~D:Record.FlowViewIdentifier~}"></span>
38
+ </button>
202
39
  </div>
203
40
  <div class="pict-flow-toolbar-group">
204
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-in">Zoom +</button>
205
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-out">Zoom -</button>
206
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit">Fit</button>
41
+ <button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage Layouts">
42
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-layout-{~D:Record.FlowViewIdentifier~}"></span>
43
+ <span class="pict-flow-toolbar-btn-text">Layout</span>
44
+ <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-LayoutChevron-{~D:Record.FlowViewIdentifier~}"></span>
45
+ </button>
46
+ <button class="pict-flow-toolbar-btn" data-flow-action="auto-layout" title="Auto Layout">
47
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-auto-layout-{~D:Record.FlowViewIdentifier~}"></span>
48
+ <span class="pict-flow-toolbar-btn-text">Auto Layout</span>
49
+ </button>
207
50
  </div>
208
51
  <div class="pict-flow-toolbar-group">
209
- <button class="pict-flow-toolbar-btn" data-flow-action="auto-layout">Auto Layout</button>
52
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In">
53
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-in-{~D:Record.FlowViewIdentifier~}"></span>
54
+ </button>
55
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out">
56
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-out-{~D:Record.FlowViewIdentifier~}"></span>
57
+ </button>
58
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View">
59
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-fit-{~D:Record.FlowViewIdentifier~}"></span>
60
+ </button>
210
61
  </div>
211
- <div class="pict-flow-toolbar-group">
212
- <span class="pict-flow-toolbar-label">Layouts:</span>
213
- <select class="pict-flow-toolbar-select layout-select"
214
- id="Flow-Toolbar-LayoutSelect-{~D:Record.FlowViewIdentifier~}">
215
- <option value="">-- select layout --</option>
216
- </select>
217
- <button class="pict-flow-toolbar-btn" data-flow-action="save-layout" title="Save the current node positions as a named layout">Save</button>
218
- <button class="pict-flow-toolbar-btn" data-flow-action="restore-layout" title="Restore the selected layout">Restore</button>
219
- <button class="pict-flow-toolbar-btn danger" data-flow-action="delete-layout" title="Delete the selected saved layout">Delete</button>
220
- </div>
221
- <div class="pict-flow-toolbar-group">
222
- <button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen">&#x26F6; Fullscreen</button>
62
+ <div class="pict-flow-toolbar-group pict-flow-toolbar-right">
63
+ <button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings">
64
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-settings-{~D:Record.FlowViewIdentifier~}"></span>
65
+ </button>
66
+ <button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen">
67
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Fullscreen-Icon-{~D:Record.FlowViewIdentifier~}"></span>
68
+ </button>
69
+ <button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float">
70
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-grip-{~D:Record.FlowViewIdentifier~}"></span>
71
+ </button>
72
+ <button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar">
73
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-collapse-{~D:Record.FlowViewIdentifier~}"></span>
74
+ </button>
223
75
  </div>
224
76
  </div>
225
- <div class="pict-flow-palette-container" id="Flow-Palette-{~D:Record.FlowViewIdentifier~}">
226
- <div class="pict-flow-palette-toggle" data-flow-action="toggle-palette">
227
- <span>Card Palette</span>
228
- <span class="pict-flow-palette-toggle-arrow" id="Flow-Palette-Arrow-{~D:Record.FlowViewIdentifier~}">&#9660;</span>
229
- </div>
230
- <div class="pict-flow-palette-body" id="Flow-Palette-Body-{~D:Record.FlowViewIdentifier~}">
231
- </div>
77
+ <div class="pict-flow-toolbar-collapsed" id="Flow-Toolbar-Collapsed-{~D:Record.FlowViewIdentifier~}">
78
+ <button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}">
79
+ <span id="Flow-Toolbar-Icon-expand-{~D:Record.FlowViewIdentifier~}"></span>
80
+ </button>
81
+ </div>
82
+ <div class="pict-flow-toolbar-popup-anchor" id="Flow-Toolbar-PopupAnchor-{~D:Record.FlowViewIdentifier~}">
232
83
  </div>
233
84
  `
234
85
  }
@@ -255,7 +106,13 @@ class PictViewFlowToolbar extends libPictView
255
106
  this.serviceType = 'PictViewFlowToolbar';
256
107
 
257
108
  this._FlowView = null;
258
- this._PaletteOpen = false;
109
+
110
+ // Toolbar mode state
111
+ this._ToolbarMode = 'docked'; // 'docked' | 'floating' | 'collapsed'
112
+ this._ActivePopup = null; // 'add-node' | 'cards' | 'layout' | null
113
+ this._FloatingPosition = { X: 80, Y: 80 };
114
+ this._DocumentClickHandler = null;
115
+ this._FloatingToolbarView = null;
259
116
  }
260
117
 
261
118
  render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
@@ -267,12 +124,13 @@ class PictViewFlowToolbar extends libPictView
267
124
 
268
125
  onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
269
126
  {
127
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
128
+
270
129
  // Bind toolbar button events via event delegation
271
- let tmpToolbarElements = this.pict.ContentAssignment.getElement(`.pict-flow-toolbar`);
272
- if (tmpToolbarElements.length > 0)
130
+ let tmpToolbarBar = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Bar-${tmpFlowViewIdentifier}`);
131
+ if (tmpToolbarBar.length > 0)
273
132
  {
274
- let tmpToolbar = tmpToolbarElements[0];
275
- tmpToolbar.addEventListener('click', (pEvent) =>
133
+ tmpToolbarBar[0].addEventListener('click', (pEvent) =>
276
134
  {
277
135
  let tmpTarget = pEvent.target;
278
136
  if (!tmpTarget) return;
@@ -286,170 +144,395 @@ class PictViewFlowToolbar extends libPictView
286
144
  });
287
145
  }
288
146
 
289
- // Bind palette toggle and card click events
290
- let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
291
- let tmpPaletteContainer = this.pict.ContentAssignment.getElement(`#Flow-Palette-${tmpFlowViewIdentifier}`);
292
- if (tmpPaletteContainer.length > 0)
147
+ // Bind expand button click (it's outside the main toolbar bar)
148
+ let tmpExpandBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-ExpandBtn-${tmpFlowViewIdentifier}`);
149
+ if (tmpExpandBtn.length > 0)
293
150
  {
294
- tmpPaletteContainer[0].addEventListener('click', (pEvent) =>
151
+ tmpExpandBtn[0].addEventListener('click', () =>
295
152
  {
296
- let tmpTarget = pEvent.target;
297
- if (!tmpTarget) return;
298
-
299
- // Check for toggle
300
- let tmpToggle = tmpTarget.closest('[data-flow-action="toggle-palette"]');
301
- if (tmpToggle)
302
- {
303
- this._togglePalette();
304
- return;
305
- }
306
-
307
- // Check for card click
308
- let tmpCard = tmpTarget.closest('[data-card-type]');
309
- if (tmpCard)
310
- {
311
- let tmpCardType = tmpCard.getAttribute('data-card-type');
312
- this._addCardFromPalette(tmpCardType);
313
- }
153
+ this._setToolbarMode('docked');
314
154
  });
315
155
  }
316
156
 
317
- // Populate the node type dropdown, palette, and layout dropdown
318
- this._populateNodeTypeDropdown();
319
- this._renderPalette();
320
- this._populateLayoutDropdown();
157
+ // Populate SVG icons for toolbar buttons
158
+ this._populateToolbarIcons();
321
159
 
322
160
  return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
323
161
  }
324
162
 
163
+ // ── Icon Population ───────────────────────────────────────────────────
164
+
325
165
  /**
326
- * Populate the node type dropdown from the registered node types.
166
+ * Populate SVG icons for all toolbar buttons.
327
167
  */
328
- _populateNodeTypeDropdown()
168
+ _populateToolbarIcons()
329
169
  {
330
- if (!this._FlowView || !this._FlowView._NodeTypeProvider)
170
+ let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
171
+ if (!tmpIconProvider) return;
172
+
173
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
174
+
175
+ // Map of element ID suffix → icon key
176
+ let tmpIconMap =
331
177
  {
332
- return;
178
+ 'plus': 'plus',
179
+ 'trash': 'trash',
180
+ 'zoom-in': 'zoom-in',
181
+ 'zoom-out': 'zoom-out',
182
+ 'zoom-fit': 'zoom-fit',
183
+ 'auto-layout': 'auto-layout',
184
+ 'cards': 'cards',
185
+ 'layout': 'layout',
186
+ 'settings': 'settings',
187
+ 'grip': 'grip',
188
+ 'collapse': 'collapse',
189
+ 'expand': 'expand'
190
+ };
191
+
192
+ let tmpKeys = Object.keys(tmpIconMap);
193
+ for (let i = 0; i < tmpKeys.length; i++)
194
+ {
195
+ let tmpElementId = `Flow-Toolbar-Icon-${tmpKeys[i]}-${tmpFlowViewIdentifier}`;
196
+ let tmpElements = this.pict.ContentAssignment.getElement(`#${tmpElementId}`);
197
+ if (tmpElements.length > 0)
198
+ {
199
+ tmpElements[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpIconMap[tmpKeys[i]], 14);
200
+ }
333
201
  }
334
202
 
335
- let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
336
- let tmpSelectElements = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-NodeType-${tmpFlowViewIdentifier}`);
337
- if (tmpSelectElements.length < 1)
203
+ // Fullscreen icon
204
+ let tmpFullscreenIcon = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-Icon-${tmpFlowViewIdentifier}`);
205
+ if (tmpFullscreenIcon.length > 0)
338
206
  {
339
- return;
207
+ tmpFullscreenIcon[0].innerHTML = tmpIconProvider.getIconSVGMarkup('fullscreen', 14);
340
208
  }
341
209
 
342
- let tmpSelect = tmpSelectElements[0];
210
+ // Chevrons (smaller)
211
+ let tmpCardsChevron = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-CardsChevron-${tmpFlowViewIdentifier}`);
212
+ if (tmpCardsChevron.length > 0)
213
+ {
214
+ tmpCardsChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
215
+ }
343
216
 
344
- // Clear existing options
345
- while (tmpSelect.firstChild)
217
+ let tmpLayoutChevron = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-LayoutChevron-${tmpFlowViewIdentifier}`);
218
+ if (tmpLayoutChevron.length > 0)
346
219
  {
347
- tmpSelect.removeChild(tmpSelect.firstChild);
220
+ tmpLayoutChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
348
221
  }
222
+ }
349
223
 
350
- let tmpTypes = this._FlowView._NodeTypeProvider.getNodeTypes();
351
- let tmpTypeKeys = Object.keys(tmpTypes);
224
+ // ── Popup Management ──────────────────────────────────────────────────
352
225
 
353
- for (let i = 0; i < tmpTypeKeys.length; i++)
226
+ /**
227
+ * Open a popup below a trigger button.
228
+ * @param {string} pType - 'add-node' | 'cards' | 'layout'
229
+ */
230
+ _openPopup(pType)
231
+ {
232
+ // Toggle off if already open
233
+ if (this._ActivePopup === pType)
354
234
  {
355
- let tmpTypeConfig = tmpTypes[tmpTypeKeys[i]];
235
+ this._closePopup();
236
+ return;
237
+ }
356
238
 
357
- // Skip disabled cards
358
- if (tmpTypeConfig.CardMetadata && tmpTypeConfig.CardMetadata.Enabled === false)
359
- {
360
- continue;
361
- }
239
+ // Close any existing popup first
240
+ this._closePopup();
362
241
 
363
- let tmpOption = document.createElement('option');
364
- tmpOption.value = tmpTypeKeys[i];
242
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
243
+ let tmpAnchor = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-PopupAnchor-${tmpFlowViewIdentifier}`);
244
+ if (tmpAnchor.length < 1) return;
245
+
246
+ // Create popup div
247
+ let tmpPopup = document.createElement('div');
248
+ tmpPopup.className = 'pict-flow-toolbar-popup';
249
+ tmpPopup.setAttribute('id', `Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
250
+
251
+ // Build popup content
252
+ switch (pType)
253
+ {
254
+ case 'add-node':
255
+ this._buildAddNodePopup(tmpPopup);
256
+ break;
257
+ case 'cards':
258
+ this._buildCardsPopup(tmpPopup);
259
+ break;
260
+ case 'layout':
261
+ this._buildLayoutPopup(tmpPopup);
262
+ break;
263
+ case 'settings':
264
+ this._buildSettingsPopup(tmpPopup);
265
+ break;
266
+ }
365
267
 
366
- if (tmpTypeConfig.CardMetadata && tmpTypeConfig.CardMetadata.Icon)
268
+ tmpAnchor[0].appendChild(tmpPopup);
269
+ this._ActivePopup = pType;
270
+
271
+ // Position the popup below the trigger button
272
+ this._positionPopup(tmpPopup, pType);
273
+
274
+ // Click-outside-to-close handler (delayed to avoid catching the opening click)
275
+ setTimeout(() =>
276
+ {
277
+ this._DocumentClickHandler = (pEvent) =>
367
278
  {
368
- tmpOption.textContent = tmpTypeConfig.CardMetadata.Icon + ' ' + tmpTypeConfig.Label;
369
- }
370
- else
279
+ if (!tmpPopup.contains(pEvent.target))
280
+ {
281
+ // Check if click was on the trigger button itself (toggle behavior)
282
+ let tmpButton = pEvent.target.closest('[data-flow-action]');
283
+ if (tmpButton)
284
+ {
285
+ let tmpAction = tmpButton.getAttribute('data-flow-action');
286
+ if (tmpAction === pType || tmpAction === pType.replace('-popup', '') + '-popup')
287
+ {
288
+ return; // Let the toggle handle it
289
+ }
290
+ }
291
+ this._closePopup();
292
+ }
293
+ };
294
+ document.addEventListener('click', this._DocumentClickHandler, true);
295
+ }, 0);
296
+
297
+ // Focus search input if Add Node popup
298
+ if (pType === 'add-node')
299
+ {
300
+ let tmpSearch = tmpPopup.querySelector('.pict-flow-popup-search');
301
+ if (tmpSearch)
371
302
  {
372
- tmpOption.textContent = tmpTypeConfig.Label;
303
+ setTimeout(() => { tmpSearch.focus(); }, 50);
373
304
  }
374
-
375
- tmpSelect.appendChild(tmpOption);
376
305
  }
377
306
  }
378
307
 
379
308
  /**
380
- * Populate the layout dropdown from saved layouts in the flow data.
309
+ * Close the active popup and clean up.
381
310
  */
382
- _populateLayoutDropdown()
311
+ _closePopup()
383
312
  {
384
- if (!this._FlowView || !this._FlowView._LayoutProvider)
313
+ if (this._DocumentClickHandler)
385
314
  {
386
- return;
315
+ document.removeEventListener('click', this._DocumentClickHandler, true);
316
+ this._DocumentClickHandler = null;
387
317
  }
388
318
 
389
319
  let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
390
- let tmpSelectElements = this.pict.ContentAssignment.getElement(
391
- `#Flow-Toolbar-LayoutSelect-${tmpFlowViewIdentifier}`
392
- );
393
- if (tmpSelectElements.length < 1)
320
+ let tmpPopup = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
321
+ if (tmpPopup.length > 0)
394
322
  {
395
- return;
323
+ tmpPopup[0].parentNode.removeChild(tmpPopup[0]);
396
324
  }
397
325
 
398
- let tmpSelect = tmpSelectElements[0];
326
+ this._ActivePopup = null;
327
+ }
399
328
 
400
- // Clear existing options
401
- while (tmpSelect.firstChild)
329
+ /**
330
+ * Position a popup below its trigger button.
331
+ * @param {HTMLElement} pPopupDiv
332
+ * @param {string} pType
333
+ */
334
+ _positionPopup(pPopupDiv, pType)
335
+ {
336
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
337
+
338
+ // Determine which button triggered the popup
339
+ let tmpTriggerSelector;
340
+ switch (pType)
402
341
  {
403
- tmpSelect.removeChild(tmpSelect.firstChild);
342
+ case 'add-node':
343
+ tmpTriggerSelector = `#Flow-Toolbar-AddNode-${tmpFlowViewIdentifier}`;
344
+ break;
345
+ case 'cards':
346
+ tmpTriggerSelector = `#Flow-Toolbar-Cards-${tmpFlowViewIdentifier}`;
347
+ break;
348
+ case 'layout':
349
+ tmpTriggerSelector = `#Flow-Toolbar-Layout-${tmpFlowViewIdentifier}`;
350
+ break;
351
+ case 'settings':
352
+ tmpTriggerSelector = `#Flow-Toolbar-Settings-${tmpFlowViewIdentifier}`;
353
+ break;
354
+ default:
355
+ return;
404
356
  }
405
357
 
406
- // Add placeholder option
407
- let tmpPlaceholder = document.createElement('option');
408
- tmpPlaceholder.value = '';
409
- tmpPlaceholder.textContent = '-- select layout --';
410
- tmpSelect.appendChild(tmpPlaceholder);
358
+ let tmpTriggerElements = this.pict.ContentAssignment.getElement(tmpTriggerSelector);
359
+ if (tmpTriggerElements.length < 1) return;
411
360
 
412
- let tmpLayouts = this._FlowView._LayoutProvider.getLayouts();
413
- for (let i = 0; i < tmpLayouts.length; i++)
361
+ let tmpAnchor = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-PopupAnchor-${tmpFlowViewIdentifier}`);
362
+ if (tmpAnchor.length < 1) return;
363
+
364
+ let tmpTriggerRect = tmpTriggerElements[0].getBoundingClientRect();
365
+ let tmpAnchorRect = tmpAnchor[0].getBoundingClientRect();
366
+
367
+ let tmpLeft = tmpTriggerRect.left - tmpAnchorRect.left;
368
+ pPopupDiv.style.left = tmpLeft + 'px';
369
+ pPopupDiv.style.top = '0px';
370
+ }
371
+
372
+ // ── Add Node Popup ────────────────────────────────────────────────────
373
+
374
+ /**
375
+ * Build the searchable Add Node popup content.
376
+ * @param {HTMLElement} pContainer
377
+ */
378
+ _buildAddNodePopup(pContainer)
379
+ {
380
+ // Search wrapper
381
+ let tmpSearchWrapper = document.createElement('div');
382
+ tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
383
+
384
+ let tmpSearchIcon = document.createElement('span');
385
+ tmpSearchIcon.className = 'pict-flow-popup-search-icon';
386
+ let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
387
+ if (tmpIconProvider)
414
388
  {
415
- let tmpLayout = tmpLayouts[i];
416
- let tmpOption = document.createElement('option');
417
- tmpOption.value = tmpLayout.Hash;
418
- tmpOption.textContent = tmpLayout.Name;
419
- tmpSelect.appendChild(tmpOption);
389
+ tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
420
390
  }
391
+ tmpSearchWrapper.appendChild(tmpSearchIcon);
392
+
393
+ let tmpSearchInput = document.createElement('input');
394
+ tmpSearchInput.className = 'pict-flow-popup-search';
395
+ tmpSearchInput.setAttribute('type', 'text');
396
+ tmpSearchInput.setAttribute('placeholder', 'Search node types...');
397
+ tmpSearchWrapper.appendChild(tmpSearchInput);
398
+ pContainer.appendChild(tmpSearchWrapper);
399
+
400
+ // Node list
401
+ let tmpListDiv = document.createElement('div');
402
+ tmpListDiv.className = 'pict-flow-popup-node-list';
403
+ pContainer.appendChild(tmpListDiv);
404
+
405
+ // Initial population
406
+ this._populateNodeList(tmpListDiv, '');
407
+
408
+ // Filter on input
409
+ tmpSearchInput.addEventListener('input', () =>
410
+ {
411
+ this._populateNodeList(tmpListDiv, tmpSearchInput.value);
412
+ });
421
413
  }
422
414
 
423
415
  /**
424
- * Render the card palette with categories and card chips.
416
+ * Populate the node list in the Add Node popup, filtered by search text.
417
+ * @param {HTMLElement} pListDiv
418
+ * @param {string} pFilter
425
419
  */
426
- _renderPalette()
420
+ _populateNodeList(pListDiv, pFilter)
427
421
  {
428
422
  if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
429
423
 
430
- let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
431
- let tmpPaletteBody = this.pict.ContentAssignment.getElement(`#Flow-Palette-Body-${tmpFlowViewIdentifier}`);
432
- if (tmpPaletteBody.length < 1) return;
424
+ // Clear
425
+ while (pListDiv.firstChild)
426
+ {
427
+ pListDiv.removeChild(pListDiv.firstChild);
428
+ }
429
+
430
+ let tmpTypes = this._FlowView._NodeTypeProvider.getNodeTypes();
431
+ let tmpTypeKeys = Object.keys(tmpTypes);
432
+ let tmpFilter = (pFilter || '').toLowerCase().trim();
433
+ let tmpIconProvider = this._FlowView._IconProvider;
434
+ let tmpMatchCount = 0;
435
+
436
+ for (let i = 0; i < tmpTypeKeys.length; i++)
437
+ {
438
+ let tmpTypeConfig = tmpTypes[tmpTypeKeys[i]];
439
+ let tmpMeta = tmpTypeConfig.CardMetadata || {};
440
+
441
+ // Skip disabled cards
442
+ if (tmpMeta.Enabled === false) continue;
443
+
444
+ // Filter match: label, code, or category
445
+ if (tmpFilter)
446
+ {
447
+ let tmpLabel = (tmpTypeConfig.Label || '').toLowerCase();
448
+ let tmpCode = (tmpMeta.Code || '').toLowerCase();
449
+ let tmpCategory = (tmpMeta.Category || '').toLowerCase();
450
+ if (tmpLabel.indexOf(tmpFilter) < 0 &&
451
+ tmpCode.indexOf(tmpFilter) < 0 &&
452
+ tmpCategory.indexOf(tmpFilter) < 0)
453
+ {
454
+ continue;
455
+ }
456
+ }
433
457
 
434
- let tmpBody = tmpPaletteBody[0];
458
+ tmpMatchCount++;
435
459
 
436
- // Clear existing palette content
437
- while (tmpBody.firstChild)
460
+ let tmpRow = document.createElement('div');
461
+ tmpRow.className = 'pict-flow-popup-list-item';
462
+ tmpRow.setAttribute('data-node-type', tmpTypeKeys[i]);
463
+
464
+ // Icon
465
+ let tmpIconSpan = document.createElement('span');
466
+ tmpIconSpan.className = 'pict-flow-popup-list-item-icon';
467
+ if (tmpIconProvider)
468
+ {
469
+ let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
470
+ tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
471
+ }
472
+ tmpRow.appendChild(tmpIconSpan);
473
+
474
+ // Label
475
+ let tmpLabelSpan = document.createElement('span');
476
+ tmpLabelSpan.className = 'pict-flow-popup-list-item-label';
477
+ tmpLabelSpan.textContent = tmpTypeConfig.Label;
478
+ tmpRow.appendChild(tmpLabelSpan);
479
+
480
+ // Code badge
481
+ if (tmpMeta.Code)
482
+ {
483
+ let tmpCodeSpan = document.createElement('span');
484
+ tmpCodeSpan.className = 'pict-flow-popup-list-item-code';
485
+ tmpCodeSpan.textContent = tmpMeta.Code;
486
+ tmpRow.appendChild(tmpCodeSpan);
487
+ }
488
+
489
+ // Click handler
490
+ tmpRow.addEventListener('click', () =>
491
+ {
492
+ this._addNodeAtCenter(tmpTypeKeys[i]);
493
+ this._closePopup();
494
+ });
495
+
496
+ pListDiv.appendChild(tmpRow);
497
+ }
498
+
499
+ if (tmpMatchCount === 0)
438
500
  {
439
- tmpBody.removeChild(tmpBody.firstChild);
501
+ let tmpEmpty = document.createElement('div');
502
+ tmpEmpty.className = 'pict-flow-popup-list-empty';
503
+ tmpEmpty.textContent = 'No matching node types';
504
+ pListDiv.appendChild(tmpEmpty);
440
505
  }
506
+ }
507
+
508
+ // ── Cards Popup ───────────────────────────────────────────────────────
509
+
510
+ /**
511
+ * Build the Cards popup content (reuses palette rendering).
512
+ * @param {HTMLElement} pContainer
513
+ */
514
+ _buildCardsPopup(pContainer)
515
+ {
516
+ this._renderPalette(pContainer);
517
+ }
518
+
519
+ /**
520
+ * Render the card palette with categories and card chips into a container.
521
+ * @param {HTMLElement} pContainer - The target container element
522
+ */
523
+ _renderPalette(pContainer)
524
+ {
525
+ if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
441
526
 
442
527
  let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
443
528
  let tmpCategoryKeys = Object.keys(tmpCategories);
444
529
 
445
530
  if (tmpCategoryKeys.length === 0)
446
531
  {
447
- // No FlowCards registered - hide the palette
448
- let tmpPaletteContainer = this.pict.ContentAssignment.getElement(`#Flow-Palette-${tmpFlowViewIdentifier}`);
449
- if (tmpPaletteContainer.length > 0)
450
- {
451
- tmpPaletteContainer[0].style.display = 'none';
452
- }
532
+ let tmpEmpty = document.createElement('div');
533
+ tmpEmpty.className = 'pict-flow-popup-list-empty';
534
+ tmpEmpty.textContent = 'No card types available';
535
+ pContainer.appendChild(tmpEmpty);
453
536
  return;
454
537
  }
455
538
 
@@ -460,6 +543,7 @@ class PictViewFlowToolbar extends libPictView
460
543
 
461
544
  let tmpCategoryDiv = document.createElement('div');
462
545
  tmpCategoryDiv.className = 'pict-flow-palette-category';
546
+ tmpCategoryDiv.style.padding = '0.35em 0.5em';
463
547
 
464
548
  let tmpCategoryLabel = document.createElement('div');
465
549
  tmpCategoryLabel.className = 'pict-flow-palette-category-label';
@@ -496,7 +580,23 @@ class PictViewFlowToolbar extends libPictView
496
580
  {
497
581
  let tmpIconSpan = document.createElement('span');
498
582
  tmpIconSpan.className = 'pict-flow-palette-card-icon';
499
- tmpIconSpan.textContent = tmpMeta.Icon;
583
+ let tmpIconProvider = this._FlowView._IconProvider;
584
+ if (tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
585
+ {
586
+ let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
587
+ tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 14);
588
+ }
589
+ else
590
+ {
591
+ tmpIconSpan.textContent = tmpMeta.Icon;
592
+ }
593
+ tmpCardEl.appendChild(tmpIconSpan);
594
+ }
595
+ else if (this._FlowView._IconProvider)
596
+ {
597
+ let tmpIconSpan = document.createElement('span');
598
+ tmpIconSpan.className = 'pict-flow-palette-card-icon';
599
+ tmpIconSpan.innerHTML = this._FlowView._IconProvider.getIconSVGMarkup('default', 14);
500
600
  tmpCardEl.appendChild(tmpIconSpan);
501
601
  }
502
602
  else if (tmpCardConfig.TitleBarColor)
@@ -522,48 +622,439 @@ class PictViewFlowToolbar extends libPictView
522
622
  tmpCardEl.appendChild(tmpCodeSpan);
523
623
  }
524
624
 
625
+ // Click handler
626
+ tmpCardEl.addEventListener('click', () =>
627
+ {
628
+ this._addCardFromPalette(tmpCardConfig.Hash);
629
+ this._closePopup();
630
+ });
631
+
525
632
  tmpCardsDiv.appendChild(tmpCardEl);
526
633
  }
527
634
 
528
635
  tmpCategoryDiv.appendChild(tmpCardsDiv);
529
- tmpBody.appendChild(tmpCategoryDiv);
636
+ pContainer.appendChild(tmpCategoryDiv);
530
637
  }
531
638
  }
532
639
 
640
+ // ── Layout Popup ──────────────────────────────────────────────────────
641
+
533
642
  /**
534
- * Toggle the palette open/closed.
643
+ * Build the Layout popup content.
644
+ * @param {HTMLElement} pContainer
535
645
  */
536
- _togglePalette()
646
+ _buildLayoutPopup(pContainer)
537
647
  {
538
- this._PaletteOpen = !this._PaletteOpen;
648
+ let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
649
+
650
+ // Save Layout section at top
651
+ let tmpSaveSection = document.createElement('div');
652
+ tmpSaveSection.className = 'pict-flow-popup-layout-save-section';
653
+
654
+ // Save input row (hidden initially)
655
+ let tmpSaveInputRow = document.createElement('div');
656
+ tmpSaveInputRow.className = 'pict-flow-popup-layout-save-input-row';
657
+ tmpSaveInputRow.style.display = 'none';
658
+
659
+ let tmpSaveInput = document.createElement('input');
660
+ tmpSaveInput.className = 'pict-flow-popup-layout-save-input';
661
+ tmpSaveInput.setAttribute('type', 'text');
662
+ tmpSaveInput.setAttribute('placeholder', 'Layout name...');
663
+ tmpSaveInputRow.appendChild(tmpSaveInput);
664
+
665
+ let tmpSaveConfirmBtn = document.createElement('button');
666
+ tmpSaveConfirmBtn.className = 'pict-flow-popup-layout-save-confirm';
667
+ tmpSaveConfirmBtn.title = 'Save';
668
+ if (tmpIconProvider)
669
+ {
670
+ tmpSaveConfirmBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
671
+ }
672
+ else
673
+ {
674
+ tmpSaveConfirmBtn.textContent = '✓';
675
+ }
676
+ tmpSaveInputRow.appendChild(tmpSaveConfirmBtn);
539
677
 
540
- let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
678
+ // "Save Current Layout" clickable row
679
+ let tmpSaveRow = document.createElement('div');
680
+ tmpSaveRow.className = 'pict-flow-popup-layout-save';
681
+
682
+ let tmpSaveIcon = document.createElement('span');
683
+ tmpSaveIcon.className = 'pict-flow-popup-layout-save-icon';
684
+ if (tmpIconProvider)
685
+ {
686
+ tmpSaveIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
687
+ }
688
+ tmpSaveRow.appendChild(tmpSaveIcon);
689
+
690
+ let tmpSaveText = document.createElement('span');
691
+ tmpSaveText.textContent = 'Save Current Layout';
692
+ tmpSaveRow.appendChild(tmpSaveText);
693
+
694
+ // Click "Save Current Layout" to reveal the input row
695
+ tmpSaveRow.addEventListener('click', () =>
696
+ {
697
+ tmpSaveRow.style.display = 'none';
698
+ tmpSaveInputRow.style.display = '';
699
+ tmpSaveInput.value = '';
700
+ setTimeout(() => { tmpSaveInput.focus(); }, 50);
701
+ });
702
+
703
+ // Confirm save via button click
704
+ let tmpDoSave = () =>
705
+ {
706
+ let tmpName = tmpSaveInput.value.trim();
707
+ if (tmpName === '') return;
708
+ this._FlowView._LayoutProvider.saveLayout(tmpName);
709
+ // Refresh the popup content
710
+ while (pContainer.firstChild)
711
+ {
712
+ pContainer.removeChild(pContainer.firstChild);
713
+ }
714
+ this._buildLayoutPopup(pContainer);
715
+ };
716
+
717
+ tmpSaveConfirmBtn.addEventListener('click', tmpDoSave);
541
718
 
542
- let tmpBody = this.pict.ContentAssignment.getElement(`#Flow-Palette-Body-${tmpFlowViewIdentifier}`);
543
- if (tmpBody.length > 0)
719
+ // Confirm save via Enter key
720
+ tmpSaveInput.addEventListener('keydown', (pEvent) =>
544
721
  {
545
- if (this._PaletteOpen)
722
+ if (pEvent.key === 'Enter')
546
723
  {
547
- tmpBody[0].classList.add('open');
724
+ pEvent.preventDefault();
725
+ tmpDoSave();
548
726
  }
549
- else
727
+ else if (pEvent.key === 'Escape')
550
728
  {
551
- tmpBody[0].classList.remove('open');
729
+ // Cancel — hide input, show the save row again
730
+ tmpSaveInputRow.style.display = 'none';
731
+ tmpSaveRow.style.display = '';
552
732
  }
733
+ });
734
+
735
+ // Prevent clicks inside the input from closing the popup
736
+ tmpSaveInput.addEventListener('click', (pEvent) =>
737
+ {
738
+ pEvent.stopPropagation();
739
+ });
740
+
741
+ tmpSaveSection.appendChild(tmpSaveRow);
742
+ tmpSaveSection.appendChild(tmpSaveInputRow);
743
+ pContainer.appendChild(tmpSaveSection);
744
+
745
+ // Divider
746
+ let tmpDivider = document.createElement('div');
747
+ tmpDivider.className = 'pict-flow-popup-divider';
748
+ pContainer.appendChild(tmpDivider);
749
+
750
+ // Layout rows
751
+ if (!this._FlowView || !this._FlowView._LayoutProvider)
752
+ {
753
+ let tmpEmpty = document.createElement('div');
754
+ tmpEmpty.className = 'pict-flow-popup-list-empty';
755
+ tmpEmpty.textContent = 'No saved layouts';
756
+ pContainer.appendChild(tmpEmpty);
757
+ return;
553
758
  }
554
759
 
555
- let tmpArrow = this.pict.ContentAssignment.getElement(`#Flow-Palette-Arrow-${tmpFlowViewIdentifier}`);
556
- if (tmpArrow.length > 0)
760
+ let tmpLayouts = this._FlowView._LayoutProvider.getLayouts();
761
+
762
+ if (tmpLayouts.length === 0)
557
763
  {
558
- if (this._PaletteOpen)
764
+ let tmpEmpty = document.createElement('div');
765
+ tmpEmpty.className = 'pict-flow-popup-list-empty';
766
+ tmpEmpty.textContent = 'No saved layouts';
767
+ pContainer.appendChild(tmpEmpty);
768
+ return;
769
+ }
770
+
771
+ for (let i = 0; i < tmpLayouts.length; i++)
772
+ {
773
+ let tmpLayout = tmpLayouts[i];
774
+
775
+ let tmpRow = document.createElement('div');
776
+ tmpRow.className = 'pict-flow-popup-layout-row';
777
+
778
+ let tmpNameSpan = document.createElement('span');
779
+ tmpNameSpan.className = 'pict-flow-popup-layout-name';
780
+ tmpNameSpan.textContent = tmpLayout.Name;
781
+ tmpRow.appendChild(tmpNameSpan);
782
+
783
+ // Delete button (visible on hover via CSS)
784
+ let tmpDeleteBtn = document.createElement('button');
785
+ tmpDeleteBtn.className = 'pict-flow-popup-layout-delete';
786
+ tmpDeleteBtn.title = 'Delete layout';
787
+ if (tmpIconProvider)
559
788
  {
560
- tmpArrow[0].classList.add('open');
789
+ tmpDeleteBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('trash', 12);
561
790
  }
562
791
  else
563
792
  {
564
- tmpArrow[0].classList.remove('open');
793
+ tmpDeleteBtn.textContent = '×';
794
+ }
795
+ tmpRow.appendChild(tmpDeleteBtn);
796
+
797
+ // Click row → restore layout
798
+ tmpRow.addEventListener('click', (pEvent) =>
799
+ {
800
+ // Don't restore if they clicked the delete button
801
+ if (pEvent.target.closest('.pict-flow-popup-layout-delete'))
802
+ {
803
+ return;
804
+ }
805
+ this._FlowView._LayoutProvider.restoreLayout(tmpLayout.Hash);
806
+ this._closePopup();
807
+ });
808
+
809
+ // Click delete → delete layout and refresh popup
810
+ tmpDeleteBtn.addEventListener('click', (pEvent) =>
811
+ {
812
+ pEvent.stopPropagation();
813
+ this._FlowView._LayoutProvider.deleteLayout(tmpLayout.Hash);
814
+ // Refresh the popup content
815
+ while (pContainer.firstChild)
816
+ {
817
+ pContainer.removeChild(pContainer.firstChild);
818
+ }
819
+ this._buildLayoutPopup(pContainer);
820
+ });
821
+
822
+ pContainer.appendChild(tmpRow);
823
+ }
824
+ }
825
+
826
+ // ── Settings Popup ───────────────────────────────────────────────────
827
+
828
+ /**
829
+ * Build the Settings popup content (theme dropdown + noise slider).
830
+ * @param {HTMLElement} pContainer
831
+ */
832
+ _buildSettingsPopup(pContainer)
833
+ {
834
+ if (!this._FlowView || !this._FlowView._ThemeProvider) return;
835
+
836
+ let tmpThemeProvider = this._FlowView._ThemeProvider;
837
+
838
+ // Theme selector section
839
+ let tmpThemeSection = document.createElement('div');
840
+ tmpThemeSection.className = 'pict-flow-popup-settings-section';
841
+
842
+ let tmpThemeLabel = document.createElement('label');
843
+ tmpThemeLabel.className = 'pict-flow-popup-settings-label';
844
+ tmpThemeLabel.textContent = 'Theme';
845
+ tmpThemeSection.appendChild(tmpThemeLabel);
846
+
847
+ let tmpThemeSelect = document.createElement('select');
848
+ tmpThemeSelect.className = 'pict-flow-popup-settings-select';
849
+
850
+ let tmpThemeKeys = tmpThemeProvider.getThemeKeys();
851
+ let tmpActiveKey = tmpThemeProvider.getActiveThemeKey();
852
+
853
+ for (let i = 0; i < tmpThemeKeys.length; i++)
854
+ {
855
+ let tmpOption = document.createElement('option');
856
+ tmpOption.value = tmpThemeKeys[i];
857
+
858
+ let tmpTheme = tmpThemeProvider._Themes[tmpThemeKeys[i]];
859
+ tmpOption.textContent = tmpTheme.Label || tmpThemeKeys[i];
860
+
861
+ if (tmpThemeKeys[i] === tmpActiveKey)
862
+ {
863
+ tmpOption.selected = true;
864
+ }
865
+ tmpThemeSelect.appendChild(tmpOption);
866
+ }
867
+
868
+ tmpThemeSelect.addEventListener('change', () =>
869
+ {
870
+ this._FlowView.setTheme(tmpThemeSelect.value);
871
+ // Refresh the noise slider visibility
872
+ this._refreshNoiseSlider(pContainer);
873
+ });
874
+
875
+ // Prevent popup close on select interaction
876
+ tmpThemeSelect.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
877
+
878
+ tmpThemeSection.appendChild(tmpThemeSelect);
879
+ pContainer.appendChild(tmpThemeSection);
880
+
881
+ // Divider
882
+ let tmpDivider = document.createElement('div');
883
+ tmpDivider.className = 'pict-flow-popup-divider';
884
+ pContainer.appendChild(tmpDivider);
885
+
886
+ // Noise level section
887
+ let tmpNoiseSection = document.createElement('div');
888
+ tmpNoiseSection.className = 'pict-flow-popup-settings-section pict-flow-popup-settings-noise';
889
+ tmpNoiseSection.setAttribute('data-settings-type', 'noise');
890
+
891
+ let tmpNoiseLabel = document.createElement('label');
892
+ tmpNoiseLabel.className = 'pict-flow-popup-settings-label';
893
+ tmpNoiseLabel.textContent = 'Noise';
894
+ tmpNoiseSection.appendChild(tmpNoiseLabel);
895
+
896
+ let tmpNoiseRow = document.createElement('div');
897
+ tmpNoiseRow.className = 'pict-flow-popup-settings-slider-row';
898
+
899
+ let tmpNoiseSlider = document.createElement('input');
900
+ tmpNoiseSlider.type = 'range';
901
+ tmpNoiseSlider.className = 'pict-flow-popup-settings-slider';
902
+ tmpNoiseSlider.min = '0';
903
+ tmpNoiseSlider.max = '100';
904
+ tmpNoiseSlider.value = String(Math.round(tmpThemeProvider.getNoiseLevel() * 100));
905
+
906
+ let tmpNoiseValue = document.createElement('span');
907
+ tmpNoiseValue.className = 'pict-flow-popup-settings-slider-value';
908
+ tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
909
+
910
+ tmpNoiseSlider.addEventListener('input', () =>
911
+ {
912
+ let tmpLevel = parseInt(tmpNoiseSlider.value, 10) / 100;
913
+ tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
914
+ this._FlowView.setNoiseLevel(tmpLevel);
915
+ });
916
+
917
+ // Prevent popup close on slider interaction
918
+ tmpNoiseSlider.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
919
+ tmpNoiseSlider.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
920
+
921
+ tmpNoiseRow.appendChild(tmpNoiseSlider);
922
+ tmpNoiseRow.appendChild(tmpNoiseValue);
923
+ tmpNoiseSection.appendChild(tmpNoiseRow);
924
+ pContainer.appendChild(tmpNoiseSection);
925
+
926
+ // Show/hide noise slider based on active theme
927
+ this._refreshNoiseSlider(pContainer);
928
+ }
929
+
930
+ /**
931
+ * Show or hide the noise slider based on whether the active theme supports noise.
932
+ * @param {HTMLElement} pContainer - The settings popup container
933
+ */
934
+ _refreshNoiseSlider(pContainer)
935
+ {
936
+ let tmpNoiseSection = pContainer.querySelector('[data-settings-type="noise"]');
937
+ if (!tmpNoiseSection) return;
938
+
939
+ let tmpTheme = this._FlowView._ThemeProvider.getActiveTheme();
940
+ if (tmpTheme && tmpTheme.NoiseConfig && tmpTheme.NoiseConfig.Enabled)
941
+ {
942
+ tmpNoiseSection.style.display = '';
943
+ // Update slider value to reflect theme default
944
+ let tmpSlider = tmpNoiseSection.querySelector('.pict-flow-popup-settings-slider');
945
+ let tmpValueLabel = tmpNoiseSection.querySelector('.pict-flow-popup-settings-slider-value');
946
+ if (tmpSlider)
947
+ {
948
+ let tmpLevel = Math.round(this._FlowView._ThemeProvider.getNoiseLevel() * 100);
949
+ tmpSlider.value = String(tmpLevel);
950
+ if (tmpValueLabel) tmpValueLabel.textContent = tmpLevel + '%';
565
951
  }
566
952
  }
953
+ else
954
+ {
955
+ tmpNoiseSection.style.display = 'none';
956
+ }
957
+ }
958
+
959
+ // ── Toolbar Mode Switching ────────────────────────────────────────────
960
+
961
+ /**
962
+ * Switch between docked, floating, and collapsed modes.
963
+ * @param {string} pMode - 'docked' | 'floating' | 'collapsed'
964
+ */
965
+ _setToolbarMode(pMode)
966
+ {
967
+ // Close any active popup first
968
+ this._closePopup();
969
+
970
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
971
+ let tmpBar = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Bar-${tmpFlowViewIdentifier}`);
972
+ let tmpCollapsed = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Collapsed-${tmpFlowViewIdentifier}`);
973
+
974
+ switch (pMode)
975
+ {
976
+ case 'docked':
977
+ // Show toolbar bar
978
+ if (tmpBar.length > 0) tmpBar[0].style.display = '';
979
+ // Hide collapsed button
980
+ if (tmpCollapsed.length > 0) tmpCollapsed[0].classList.remove('visible');
981
+ // Hide floating toolbar
982
+ if (this._FloatingToolbarView) this._FloatingToolbarView.hide();
983
+ break;
984
+
985
+ case 'floating':
986
+ // Hide toolbar bar
987
+ if (tmpBar.length > 0) tmpBar[0].style.display = 'none';
988
+ // Hide collapsed button
989
+ if (tmpCollapsed.length > 0) tmpCollapsed[0].classList.remove('visible');
990
+ // Show floating toolbar
991
+ this._showFloatingToolbar();
992
+ break;
993
+
994
+ case 'collapsed':
995
+ // Hide toolbar bar
996
+ if (tmpBar.length > 0) tmpBar[0].style.display = 'none';
997
+ // Show collapsed button
998
+ if (tmpCollapsed.length > 0) tmpCollapsed[0].classList.add('visible');
999
+ // Hide floating toolbar
1000
+ if (this._FloatingToolbarView) this._FloatingToolbarView.hide();
1001
+ break;
1002
+ }
1003
+
1004
+ this._ToolbarMode = pMode;
1005
+ }
1006
+
1007
+ /**
1008
+ * Lazily create and show the floating toolbar.
1009
+ */
1010
+ _showFloatingToolbar()
1011
+ {
1012
+ if (!this._FlowView) return;
1013
+
1014
+ if (!this._FloatingToolbarView)
1015
+ {
1016
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1017
+ this._FloatingToolbarView = this.fable.instantiateServiceProviderWithoutRegistration(
1018
+ 'PictViewFlowFloatingToolbar',
1019
+ {
1020
+ FlowViewIdentifier: tmpFlowViewIdentifier,
1021
+ DefaultDestinationAddress: `#Flow-FloatingToolbar-Container-${tmpFlowViewIdentifier}`
1022
+ }
1023
+ );
1024
+ this._FloatingToolbarView._ToolbarView = this;
1025
+ this._FloatingToolbarView._FlowView = this._FlowView;
1026
+ this._FloatingToolbarView.render();
1027
+ }
1028
+
1029
+ this._FloatingToolbarView.show();
1030
+ }
1031
+
1032
+ // ── Node Placement Helpers ────────────────────────────────────────────
1033
+
1034
+ /**
1035
+ * Add a node at the center of the visible viewport.
1036
+ * @param {string} pNodeType - The node type hash
1037
+ */
1038
+ _addNodeAtCenter(pNodeType)
1039
+ {
1040
+ if (!this._FlowView) return;
1041
+
1042
+ let tmpVS = this._FlowView.viewState;
1043
+
1044
+ // Calculate the center of the visible SVG area
1045
+ let tmpSVGContainer = this._FlowView._SVGElement;
1046
+ let tmpWidth = tmpSVGContainer ? tmpSVGContainer.clientWidth : 600;
1047
+ let tmpHeight = tmpSVGContainer ? tmpSVGContainer.clientHeight : 400;
1048
+
1049
+ let tmpCenterX = (-tmpVS.PanX + tmpWidth / 2) / tmpVS.Zoom;
1050
+ let tmpCenterY = (-tmpVS.PanY + tmpHeight / 2) / tmpVS.Zoom;
1051
+
1052
+ // Slight offset to avoid stacking
1053
+ let tmpNodeCount = this._FlowView.flowData.Nodes.length;
1054
+ tmpCenterX += (tmpNodeCount % 5) * 30;
1055
+ tmpCenterY += (tmpNodeCount % 5) * 30;
1056
+
1057
+ this._FlowView.addNode(pNodeType, tmpCenterX, tmpCenterY);
567
1058
  }
568
1059
 
569
1060
  /**
@@ -586,6 +1077,8 @@ class PictViewFlowToolbar extends libPictView
586
1077
  this._FlowView.addNode(pCardType, tmpX, tmpY);
587
1078
  }
588
1079
 
1080
+ // ── Action Handler ────────────────────────────────────────────────────
1081
+
589
1082
  /**
590
1083
  * Handle a toolbar action
591
1084
  * @param {string} pAction
@@ -599,27 +1092,7 @@ class PictViewFlowToolbar extends libPictView
599
1092
  switch (pAction)
600
1093
  {
601
1094
  case 'add-node':
602
- {
603
- // Get selected node type from dropdown
604
- let tmpSelectElements = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-NodeType-${tmpFlowViewIdentifier}`);
605
- let tmpNodeType = 'default';
606
- if (tmpSelectElements.length > 0)
607
- {
608
- tmpNodeType = tmpSelectElements[0].value;
609
- }
610
-
611
- // Place the new node at a reasonable position
612
- let tmpVS = this._FlowView.viewState;
613
- let tmpX = (-tmpVS.PanX + 200) / tmpVS.Zoom;
614
- let tmpY = (-tmpVS.PanY + 200) / tmpVS.Zoom;
615
-
616
- // Offset if there are existing nodes to avoid overlap
617
- let tmpNodeCount = this._FlowView.flowData.Nodes.length;
618
- tmpX += (tmpNodeCount % 5) * 40;
619
- tmpY += (tmpNodeCount % 5) * 40;
620
-
621
- this._FlowView.addNode(tmpNodeType, tmpX, tmpY);
622
- }
1095
+ this._openPopup('add-node');
623
1096
  break;
624
1097
 
625
1098
  case 'delete-selected':
@@ -642,57 +1115,51 @@ class PictViewFlowToolbar extends libPictView
642
1115
  this._FlowView.autoLayout();
643
1116
  break;
644
1117
 
645
- case 'save-layout':
646
- {
647
- let tmpName = window.prompt('Enter a name for this layout:');
648
- if (tmpName !== null && tmpName.trim() !== '')
649
- {
650
- this._FlowView._LayoutProvider.saveLayout(tmpName.trim());
651
- this._populateLayoutDropdown();
652
- }
653
- }
1118
+ case 'cards-popup':
1119
+ this._openPopup('cards');
654
1120
  break;
655
1121
 
656
- case 'restore-layout':
657
- {
658
- let tmpSelectElements = this.pict.ContentAssignment.getElement(
659
- `#Flow-Toolbar-LayoutSelect-${tmpFlowViewIdentifier}`
660
- );
661
- if (tmpSelectElements.length > 0)
662
- {
663
- let tmpLayoutHash = tmpSelectElements[0].value;
664
- if (tmpLayoutHash)
665
- {
666
- this._FlowView._LayoutProvider.restoreLayout(tmpLayoutHash);
667
- }
668
- }
669
- }
1122
+ case 'layout-popup':
1123
+ this._openPopup('layout');
670
1124
  break;
671
1125
 
672
- case 'delete-layout':
1126
+ case 'settings-popup':
1127
+ this._openPopup('settings');
1128
+ break;
1129
+
1130
+ case 'toggle-floating':
1131
+ if (this._ToolbarMode === 'floating')
673
1132
  {
674
- let tmpSelectElements = this.pict.ContentAssignment.getElement(
675
- `#Flow-Toolbar-LayoutSelect-${tmpFlowViewIdentifier}`
676
- );
677
- if (tmpSelectElements.length > 0)
678
- {
679
- let tmpLayoutHash = tmpSelectElements[0].value;
680
- if (tmpLayoutHash)
681
- {
682
- this._FlowView._LayoutProvider.deleteLayout(tmpLayoutHash);
683
- this._populateLayoutDropdown();
684
- }
685
- }
1133
+ this._setToolbarMode('docked');
1134
+ }
1135
+ else
1136
+ {
1137
+ this._setToolbarMode('floating');
686
1138
  }
687
1139
  break;
688
1140
 
1141
+ case 'collapse-toolbar':
1142
+ this._setToolbarMode('collapsed');
1143
+ break;
1144
+
1145
+ case 'expand-toolbar':
1146
+ this._setToolbarMode('docked');
1147
+ break;
1148
+
689
1149
  case 'fullscreen':
690
1150
  {
691
1151
  let tmpIsFullscreen = this._FlowView.toggleFullscreen();
692
- let tmpBtnElements = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-${tmpFlowViewIdentifier}`);
693
- if (tmpBtnElements.length > 0)
1152
+ let tmpIconProvider = this._FlowView._IconProvider;
1153
+ let tmpIconElements = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-Icon-${tmpFlowViewIdentifier}`);
1154
+ if (tmpIconElements.length > 0 && tmpIconProvider)
1155
+ {
1156
+ tmpIconElements[0].innerHTML = tmpIconProvider.getIconSVGMarkup(
1157
+ tmpIsFullscreen ? 'exit-fullscreen' : 'fullscreen', 14);
1158
+ }
1159
+ let tmpFullscreenBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-${tmpFlowViewIdentifier}`);
1160
+ if (tmpFullscreenBtn.length > 0)
694
1161
  {
695
- tmpBtnElements[0].innerHTML = tmpIsFullscreen ? '&#x2716; Exit Fullscreen' : '&#x26F6; Fullscreen';
1162
+ tmpFullscreenBtn[0].setAttribute('title', tmpIsFullscreen ? 'Exit Fullscreen' : 'Toggle Fullscreen');
696
1163
  }
697
1164
  }
698
1165
  break;