flexdesk 0.16.0 → 0.17.0

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 (251) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2261 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3373 -238
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/chunk-UWO43BZU.js +111 -0
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3373 -238
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +1131 -253
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3254 -2112
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -345
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +422 -0
  144. package/src/tiles/scope_picker.js +96 -0
  145. package/src/tiles/tile_base.js +12 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +38 -1
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +26 -4
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-CWPMLX67.js +0 -475
  241. package/dist/chunk-CWPMLX67.js.map +0 -7
  242. package/dist/chunk-FBGP5KN5.js.map +0 -7
  243. package/dist/chunk-FL5KFNQH.js.map +0 -7
  244. package/dist/chunk-NLTOY57M.js.map +0 -7
  245. package/dist/chunk-NNFTETK3.js +0 -633
  246. package/dist/chunk-NNFTETK3.js.map +0 -7
  247. package/dist/chunk-O5OHMWBB.js +0 -284
  248. package/dist/chunk-O5OHMWBB.js.map +0 -7
  249. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  250. package/dist/chunk-SN4LSOAR.js.map +0 -7
  251. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,1311 @@
1
+ import {
2
+ ColorField,
3
+ ManagedWindow,
4
+ ToggleField,
5
+ choiceList,
6
+ findChoice,
7
+ flatChoices,
8
+ isHexColor,
9
+ normalizeHex,
10
+ selectBox,
11
+ toggleChips
12
+ } from "./chunk-CVMCZZX2.js";
13
+ import {
14
+ DragReorder
15
+ } from "./chunk-WVFGV5FT.js";
16
+
17
+ // src/ui/components/sortable_list.js
18
+ var ROW_CLASS = "twm-sortable-list__row";
19
+ var ROW_SELECTOR = `.${ROW_CLASS}`;
20
+ var HANDLE_CLASS = "twm-sortable-list__handle";
21
+ var HANDLE_SELECTOR = `.${HANDLE_CLASS}`;
22
+ var SortableList = class {
23
+ /**
24
+ * Create a sortable list.
25
+ * @param {Object} options - Configuration options
26
+ * @param {string} options.containerId - Unique ID for the container
27
+ * @param {Array} options.items - Initial items array
28
+ * @param {Function} options.onChange - Called with updated items array when list changes
29
+ * @param {Function} options.renderItem - Custom renderer for item content (receives item, index, callbacks)
30
+ * @param {Function} options.createItem - Factory function to create new items
31
+ * @param {string} options.emptyMessage - Message to show when list is empty
32
+ * @param {string} options.addButtonText - Text for add button
33
+ * @param {boolean} options.allowReorder - Whether to allow drag-and-drop reordering
34
+ * @param {boolean} options.allowRemove - Whether to allow removing items
35
+ * @param {number} options.minItems - Minimum number of items (blocks removal if at min)
36
+ */
37
+ constructor({
38
+ containerId,
39
+ items = [],
40
+ onChange = null,
41
+ renderItem = null,
42
+ createItem = null,
43
+ emptyMessage = "No items",
44
+ addButtonText = "Add Item",
45
+ allowReorder = true,
46
+ allowRemove = true,
47
+ minItems = 0
48
+ } = {}) {
49
+ this.containerId = containerId || `sortable-list-${Date.now()}`;
50
+ this.items = Array.isArray(items) ? [...items] : [];
51
+ this.onChange = typeof onChange === "function" ? onChange : null;
52
+ this.renderItem = typeof renderItem === "function" ? renderItem : this.#defaultRenderItem.bind(this);
53
+ this.createItem = typeof createItem === "function" ? createItem : this.#defaultCreateItem.bind(this);
54
+ this.emptyMessage = emptyMessage;
55
+ this.addButtonText = addButtonText;
56
+ this.allowReorder = allowReorder;
57
+ this.allowRemove = allowRemove;
58
+ this.minItems = minItems;
59
+ this.root = null;
60
+ this.listEl = null;
61
+ this._dragReorder = null;
62
+ this._pressedHandle = false;
63
+ }
64
+ /**
65
+ * Render the sortable list.
66
+ * @returns {HTMLElement} The root container element
67
+ */
68
+ render() {
69
+ if (this.root) {
70
+ this.#refresh();
71
+ return this.root;
72
+ }
73
+ this.root = document.createElement("div");
74
+ this.root.className = "twm-sortable-list";
75
+ this.root.id = this.containerId;
76
+ this.listEl = document.createElement("div");
77
+ this.listEl.className = "twm-sortable-list__items";
78
+ this.root.appendChild(this.listEl);
79
+ if (this.addButtonText) {
80
+ const addBtn = document.createElement("button");
81
+ addBtn.type = "button";
82
+ addBtn.className = "twm-sortable-list__add";
83
+ addBtn.innerHTML = `<span class="material-symbols-outlined">add</span> ${this.#escapeHtml(this.addButtonText)}`;
84
+ addBtn.addEventListener("click", () => this.addItem());
85
+ this.root.appendChild(addBtn);
86
+ }
87
+ this.#refresh();
88
+ return this.root;
89
+ }
90
+ /**
91
+ * Get current items.
92
+ * @returns {Array} Copy of items array
93
+ */
94
+ getItems() {
95
+ return [...this.items];
96
+ }
97
+ /**
98
+ * Set items and re-render.
99
+ * @param {Array} items - New items array
100
+ */
101
+ setItems(items) {
102
+ this.items = Array.isArray(items) ? [...items] : [];
103
+ this.#refresh();
104
+ }
105
+ /**
106
+ * Add a new item.
107
+ */
108
+ addItem() {
109
+ const newItem = this.createItem(this.items.length);
110
+ this.items.push(newItem);
111
+ this.#notifyChange();
112
+ this.#refresh();
113
+ }
114
+ /**
115
+ * Remove an item by index.
116
+ * @param {number} index - Index of item to remove
117
+ */
118
+ removeItem(index) {
119
+ if (index < 0 || index >= this.items.length) return;
120
+ if (this.items.length <= this.minItems) return;
121
+ this.items.splice(index, 1);
122
+ this.#notifyChange();
123
+ this.#refresh();
124
+ }
125
+ /**
126
+ * Update an item at index.
127
+ * @param {number} index - Index of item to update
128
+ * @param {Object} updates - Properties to merge into item
129
+ */
130
+ updateItem(index, updates) {
131
+ if (index < 0 || index >= this.items.length) return;
132
+ this.items[index] = { ...this.items[index], ...updates };
133
+ this.#notifyChange();
134
+ }
135
+ /**
136
+ * Move an item from one index to another.
137
+ * @param {number} fromIndex - Source index
138
+ * @param {number} toIndex - Destination index
139
+ */
140
+ moveItem(fromIndex, toIndex) {
141
+ if (fromIndex < 0 || fromIndex >= this.items.length) return;
142
+ if (toIndex < 0 || toIndex >= this.items.length) return;
143
+ if (fromIndex === toIndex) return;
144
+ const [item] = this.items.splice(fromIndex, 1);
145
+ this.items.splice(toIndex, 0, item);
146
+ this.#notifyChange();
147
+ this.#refresh();
148
+ }
149
+ /**
150
+ * Dispose the component.
151
+ */
152
+ dispose() {
153
+ this._dragReorder?.destroy();
154
+ this._dragReorder = null;
155
+ this.root = null;
156
+ this.listEl = null;
157
+ this.items = [];
158
+ }
159
+ // ─── Private Methods ───────────────────────────────────────────────────
160
+ #refresh() {
161
+ if (!this.listEl) return;
162
+ this.listEl.innerHTML = "";
163
+ if (this.items.length === 0) {
164
+ const empty = document.createElement("div");
165
+ empty.className = "twm-sortable-list__empty";
166
+ empty.textContent = this.emptyMessage;
167
+ this.listEl.appendChild(empty);
168
+ return;
169
+ }
170
+ this.items.forEach((item, index) => {
171
+ const row = this.#createRow(item, index);
172
+ this.listEl.appendChild(row);
173
+ });
174
+ if (this.allowReorder) {
175
+ if (!this._dragReorder) {
176
+ this.listEl.addEventListener("mousedown", (e) => {
177
+ this._pressedHandle = !!e.target.closest?.(HANDLE_SELECTOR);
178
+ });
179
+ this._dragReorder = new DragReorder({
180
+ container: this.listEl,
181
+ itemSelector: ROW_SELECTOR,
182
+ keyAttr: "index",
183
+ axis: "y",
184
+ indicatorClass: "twm-sortable-list__drop",
185
+ draggableGuard: () => {
186
+ const fromHandle = this._pressedHandle;
187
+ this._pressedHandle = false;
188
+ return fromHandle;
189
+ },
190
+ onReorder: (order) => this.#applyReorder(order)
191
+ });
192
+ }
193
+ this._dragReorder.attach();
194
+ }
195
+ }
196
+ /** Reorder `items` to match the new DOM order produced by a drag,
197
+ * then notify + re-render. `order` holds the rows' pre-drag
198
+ * `data-index` values in their new arrangement. */
199
+ #applyReorder(order) {
200
+ const next = order.map((i) => this.items[Number(i)]).filter((it) => it !== void 0);
201
+ if (next.length !== this.items.length) return;
202
+ this.items = next;
203
+ this.#notifyChange();
204
+ this.#refresh();
205
+ }
206
+ #createRow(item, index) {
207
+ const row = document.createElement("div");
208
+ row.className = ROW_CLASS;
209
+ row.dataset.index = index;
210
+ if (this.allowReorder) {
211
+ const handle = document.createElement("div");
212
+ handle.className = HANDLE_CLASS;
213
+ handle.innerHTML = '<span class="material-symbols-outlined">drag_indicator</span>';
214
+ row.appendChild(handle);
215
+ }
216
+ const content = document.createElement("div");
217
+ content.className = "twm-sortable-list__content";
218
+ const callbacks = {
219
+ update: (updates) => this.updateItem(index, updates),
220
+ remove: () => this.removeItem(index),
221
+ moveUp: () => this.moveItem(index, index - 1),
222
+ moveDown: () => this.moveItem(index, index + 1)
223
+ };
224
+ this.renderItem(content, item, index, callbacks);
225
+ row.appendChild(content);
226
+ if (this.allowRemove) {
227
+ const canRemove = this.items.length > this.minItems;
228
+ const removeBtn = document.createElement("button");
229
+ removeBtn.type = "button";
230
+ removeBtn.className = "twm-sortable-list__remove";
231
+ removeBtn.disabled = !canRemove;
232
+ removeBtn.title = canRemove ? "Remove" : `Minimum ${this.minItems} items required`;
233
+ removeBtn.innerHTML = '<span class="material-symbols-outlined">close</span>';
234
+ removeBtn.addEventListener("click", () => {
235
+ if (canRemove) this.removeItem(index);
236
+ });
237
+ row.appendChild(removeBtn);
238
+ }
239
+ return row;
240
+ }
241
+ #defaultRenderItem(container, item, index, callbacks) {
242
+ const input = document.createElement("input");
243
+ input.type = "text";
244
+ input.className = "twm-sortable-list__input";
245
+ input.value = item.name ?? item.label ?? "";
246
+ input.placeholder = `Item ${index + 1}`;
247
+ input.addEventListener("change", () => {
248
+ callbacks.update({ name: input.value });
249
+ });
250
+ container.appendChild(input);
251
+ }
252
+ #defaultCreateItem(index) {
253
+ return {
254
+ id: `item_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
255
+ name: `item${index + 1}`
256
+ };
257
+ }
258
+ #notifyChange() {
259
+ if (this.onChange) {
260
+ this.onChange([...this.items]);
261
+ }
262
+ }
263
+ #escapeHtml(str) {
264
+ return String(str ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
265
+ }
266
+ };
267
+
268
+ // src/ui/components/color_list_field.js
269
+ var _seq = 0;
270
+ var hexOf = (text) => {
271
+ const t = String(text ?? "").trim();
272
+ return isHexColor(t) ? normalizeHex(t) : null;
273
+ };
274
+ var ColorListField = class {
275
+ /**
276
+ * @param {Object} [o]
277
+ * @param {string[]} [o.value] initial colours (hex or rgb())
278
+ * @param {string[]} [o.presets] preset colours offered on every row
279
+ * @param {string} [o.newColor] colour a newly added row starts on
280
+ * @param {string} [o.addLabel] text of the add button
281
+ * @param {string} [o.name] `name` of the hidden input, for a host form
282
+ * @param {Function} [o.onChange] `(colors, { commit })`, colors as `getValue()`
283
+ */
284
+ constructor({
285
+ value = [],
286
+ presets = [],
287
+ newColor = "#000000",
288
+ addLabel = "Add colour",
289
+ name = null,
290
+ onChange = null
291
+ } = {}) {
292
+ this.presets = Array.isArray(presets) ? presets : [];
293
+ this.newColor = normalizeHex(String(newColor ?? "")) ?? String(newColor ?? "");
294
+ this.addLabel = addLabel;
295
+ this.name = name;
296
+ this.onChange = typeof onChange === "function" ? onChange : null;
297
+ this._fields = [];
298
+ this._commit = true;
299
+ this._rowEdit = false;
300
+ this.list = new SortableList({
301
+ items: this.#toItems(value),
302
+ addButtonText: addLabel,
303
+ emptyMessage: "No colours",
304
+ createItem: () => ({ id: ++_seq, text: this.newColor }),
305
+ renderItem: (container, item, index, callbacks) => this.#renderRow(container, item, index, callbacks),
306
+ // Every SortableList change but a row's own edit (add, remove,
307
+ // move, drag) is followed by a refresh that rebuilds all rows —
308
+ // down to none — so the old fields go first.
309
+ onChange: () => {
310
+ if (!this._rowEdit) this.#dropFields();
311
+ this.#emit(this._commit);
312
+ }
313
+ });
314
+ this.root = null;
315
+ this.input = null;
316
+ }
317
+ render() {
318
+ if (this.root) return this.root;
319
+ const root = document.createElement("div");
320
+ root.className = "twm-color-list";
321
+ if (this.name) {
322
+ const input = document.createElement("input");
323
+ input.type = "hidden";
324
+ input.name = this.name;
325
+ root.appendChild(input);
326
+ this.input = input;
327
+ }
328
+ root.appendChild(this.list.render());
329
+ this.root = root;
330
+ this.#writeInput();
331
+ return root;
332
+ }
333
+ /** The colours in order as `#rrggbb`, or null while any row is not one. */
334
+ getValue() {
335
+ const out = this.getTexts().map(hexOf);
336
+ return out.includes(null) ? null : out;
337
+ }
338
+ /** The rows' text as it stands, valid or not. */
339
+ getTexts() {
340
+ return this.list.getItems().map((item) => String(item.text ?? ""));
341
+ }
342
+ /** Indexes of the rows that do not hold a hex colour. */
343
+ invalidIndexes() {
344
+ const bad = [];
345
+ this.getTexts().forEach((text, i) => {
346
+ if (!hexOf(text)) bad.push(i);
347
+ });
348
+ return bad;
349
+ }
350
+ /** Replace the colours without firing `onChange`. */
351
+ setValue(colors) {
352
+ this.#dropFields();
353
+ this.list.setItems(this.#toItems(colors));
354
+ this.#writeInput();
355
+ }
356
+ /** Focus the first invalid row, else the first row, else the add button. */
357
+ focus() {
358
+ const bad = this.invalidIndexes()[0];
359
+ const field = this._fields[bad ?? 0];
360
+ if (field) {
361
+ field.focus();
362
+ return;
363
+ }
364
+ this.root?.querySelector(".twm-sortable-list__add")?.focus();
365
+ }
366
+ dispose() {
367
+ this.#dropFields();
368
+ this.list.dispose();
369
+ this.root = null;
370
+ this.input = null;
371
+ }
372
+ #toItems(colors) {
373
+ return (Array.isArray(colors) ? colors : []).map((c) => ({
374
+ id: ++_seq,
375
+ text: normalizeHex(String(c ?? "")) ?? String(c ?? "")
376
+ }));
377
+ }
378
+ /** Dispose every row's field: the rows are about to be rebuilt. */
379
+ #dropFields() {
380
+ for (const f of this._fields) f?.dispose();
381
+ this._fields = [];
382
+ }
383
+ #renderRow(container, item, index, callbacks) {
384
+ const count = this.list.getItems().length;
385
+ const row = document.createElement("div");
386
+ row.className = "twm-color-list__row";
387
+ const field = new ColorField({
388
+ value: item.text,
389
+ presets: this.presets,
390
+ label: `Colour ${index + 1}`,
391
+ // Every edit goes into the list's items, so the list never holds
392
+ // a stale row; `_commit` tells its onChange which kind it was.
393
+ onChange: (_v, { commit }) => {
394
+ this._commit = commit;
395
+ this._rowEdit = true;
396
+ try {
397
+ callbacks.update({ text: field.getText() });
398
+ } finally {
399
+ this._commit = true;
400
+ this._rowEdit = false;
401
+ }
402
+ }
403
+ });
404
+ this._fields[index] = field;
405
+ row.appendChild(field.render());
406
+ const move = (icon, label, disabled, fn) => {
407
+ const btn = document.createElement("button");
408
+ btn.type = "button";
409
+ btn.className = "twm-color-list__move";
410
+ btn.title = label;
411
+ btn.setAttribute("aria-label", label);
412
+ btn.disabled = disabled;
413
+ const glyph = document.createElement("span");
414
+ glyph.className = "material-symbols-outlined";
415
+ glyph.setAttribute("aria-hidden", "true");
416
+ glyph.textContent = icon;
417
+ btn.appendChild(glyph);
418
+ btn.addEventListener("click", fn);
419
+ row.appendChild(btn);
420
+ };
421
+ move("arrow_upward", "Move up", index === 0, callbacks.moveUp);
422
+ move("arrow_downward", "Move down", index === count - 1, callbacks.moveDown);
423
+ container.appendChild(row);
424
+ }
425
+ #writeInput() {
426
+ if (this.input) this.input.value = JSON.stringify(this.getTexts());
427
+ }
428
+ #emit(commit) {
429
+ this.#writeInput();
430
+ if (this.input) {
431
+ this.input.dispatchEvent(new Event("input", { bubbles: true }));
432
+ if (commit) this.input.dispatchEvent(new Event("change", { bubbles: true }));
433
+ }
434
+ this.onChange?.(this.getValue(), { commit });
435
+ }
436
+ };
437
+
438
+ // src/ui/components/modal.js
439
+ var _modalHost = null;
440
+ function setModalHost(el) {
441
+ _modalHost = el || null;
442
+ }
443
+ function modalHost() {
444
+ return _modalHost;
445
+ }
446
+ var _modalSeq = 1;
447
+ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFieldChange = null } = {}) {
448
+ return new Promise((resolve) => {
449
+ const body = document.createElement("div");
450
+ body.className = "twm-modal__body-host";
451
+ body.innerHTML = `
452
+ <form class="twm-modal__form">
453
+ <div class="twm-modal__fields">
454
+ ${fields.map((f) => _renderEntry(f, defaults)).join("")}
455
+ </div>
456
+ <div class="twm-modal__error" data-role="form-error" hidden></div>
457
+ <div class="twm-modal__actions">
458
+ <button type="button" class="twm-btn" data-action="cancel">Cancel</button>
459
+ <button type="submit" class="twm-btn twm-btn--primary">${submitLabel}</button>
460
+ </div>
461
+ </form>
462
+ `;
463
+ const win = new ManagedWindow({
464
+ container: _modalHost,
465
+ id: `twm-modal-${_modalSeq++}`,
466
+ title: title || "Dialog",
467
+ icon: "edit_note",
468
+ content: body,
469
+ modal: true,
470
+ canMinimize: false,
471
+ canMaximize: false,
472
+ canResize: false,
473
+ canDrag: true,
474
+ defaultWidth: _estimateWidth(fields),
475
+ defaultHeight: _estimateHeight(fields),
476
+ onClose: () => {
477
+ disposeControls();
478
+ if (!_resolved) {
479
+ _resolved = true;
480
+ resolve(null);
481
+ }
482
+ }
483
+ });
484
+ let _resolved = false;
485
+ const close = (result) => {
486
+ if (_resolved) return;
487
+ _resolved = true;
488
+ resolve(result);
489
+ try {
490
+ win.close({ force: true });
491
+ } catch {
492
+ }
493
+ disposeControls();
494
+ };
495
+ const errEl = body.querySelector('[data-role="form-error"]');
496
+ const showError = (msg) => {
497
+ if (!errEl) return;
498
+ errEl.textContent = msg;
499
+ errEl.hidden = !msg;
500
+ };
501
+ body.querySelector('[data-action="cancel"]')?.addEventListener("click", () => close(null));
502
+ const formFields = _formFields(fields);
503
+ const optionsOf = new Map(formFields.map((f) => [f.name, f.options]));
504
+ const comboHints = /* @__PURE__ */ new Map();
505
+ for (const f of formFields) {
506
+ if (f.type === "select" && f.create) {
507
+ const update = _wireComboboxHint(body, f, () => optionsOf.get(f.name));
508
+ if (update) comboHints.set(f.name, update);
509
+ }
510
+ if (f.type === "picker") _wirePicker(body, f);
511
+ }
512
+ const rowHidden = (name) => !!body.querySelector(`[data-field-row="${name}"]`)?.hidden;
513
+ const clearFieldError = (name) => {
514
+ const row = body.querySelector(`[data-field-row="${name}"]`);
515
+ const errEl2 = body.querySelector(`[data-field-error="${name}"]`);
516
+ if (row) row.classList.remove("twm-is-invalid");
517
+ if (errEl2) {
518
+ errEl2.textContent = "";
519
+ errEl2.hidden = true;
520
+ }
521
+ };
522
+ const setFieldError = (name, msg) => {
523
+ const row = body.querySelector(`[data-field-row="${name}"]`);
524
+ const errEl2 = body.querySelector(`[data-field-error="${name}"]`);
525
+ if (row) row.classList.add("twm-is-invalid");
526
+ if (errEl2) {
527
+ errEl2.textContent = msg;
528
+ errEl2.hidden = false;
529
+ }
530
+ };
531
+ let onChoice = (name) => clearFieldError(name);
532
+ const controls = /* @__PURE__ */ new Map();
533
+ for (const f of formFields) {
534
+ const c = _mountControl(body, f, defaults[f.name] ?? f.default, (name) => onChoice(name));
535
+ if (c) controls.set(f.name, c);
536
+ }
537
+ function disposeControls() {
538
+ for (const c of controls.values()) c.dispose?.();
539
+ controls.clear();
540
+ }
541
+ win.show();
542
+ const comboOption = (f, text) => findChoice(flatChoices(optionsOf.get(f.name)), text, true);
543
+ const unavailable = (f, value) => {
544
+ const c = controls.get(f.name);
545
+ if (c) return c.unavailable();
546
+ if (f.type !== "select" || !f.create || value == null || value === "") return null;
547
+ const o = comboOption(f, value);
548
+ return o?.disabled ? o.reason || "" : null;
549
+ };
550
+ for (const f of formFields) {
551
+ const el = body.querySelector(`[name="${f.name}"]`);
552
+ el?.addEventListener("input", () => clearFieldError(f.name));
553
+ el?.addEventListener("change", () => clearFieldError(f.name));
554
+ if (f.type === "number" && _bounded(f)) {
555
+ el?.addEventListener("invalid", (e) => {
556
+ if (!el.validity.rangeUnderflow && !el.validity.rangeOverflow) return;
557
+ e.preventDefault();
558
+ setFieldError(f.name, _rangeMessage(f));
559
+ });
560
+ }
561
+ }
562
+ if (typeof onFieldChange === "function") {
563
+ const fieldOf = (name) => formFields.find((f) => f.name === name) || null;
564
+ const api = {
565
+ setVisible(name, on) {
566
+ const row = body.querySelector(`[data-field-row="${name}"]`);
567
+ if (!row) return;
568
+ row.hidden = !on;
569
+ row.style.display = on ? "" : "none";
570
+ clearFieldError(name);
571
+ for (const el of row.querySelectorAll("input, select, textarea")) {
572
+ if (!on && el.required) {
573
+ el.required = false;
574
+ el.dataset.twmRequired = "1";
575
+ } else if (on && el.dataset.twmRequired) {
576
+ el.required = true;
577
+ delete el.dataset.twmRequired;
578
+ }
579
+ for (const [attr, key] of [["min", "twmMin"], ["max", "twmMax"]]) {
580
+ if (!on && el.hasAttribute(attr)) {
581
+ el.dataset[key] = el.getAttribute(attr);
582
+ el.removeAttribute(attr);
583
+ } else if (on && el.dataset[key] != null) {
584
+ el.setAttribute(attr, el.dataset[key]);
585
+ delete el.dataset[key];
586
+ }
587
+ }
588
+ }
589
+ },
590
+ get(name) {
591
+ if (controls.has(name)) return controls.get(name).read();
592
+ const el = body.querySelector(`[name="${name}"]`);
593
+ if (!el) return void 0;
594
+ if (el.type === "checkbox") return !!el.checked;
595
+ const f = fieldOf(name);
596
+ if (f?.type === "select" && f.create) {
597
+ const match = comboOption(f, el.value);
598
+ return match ? match.value : el.value;
599
+ }
600
+ return el.value;
601
+ },
602
+ set(name, value) {
603
+ if (controls.has(name)) {
604
+ controls.get(name).write(value);
605
+ return;
606
+ }
607
+ const el = body.querySelector(`[name="${name}"]`);
608
+ if (!el) return;
609
+ if (el.type === "checkbox") {
610
+ el.checked = !!value;
611
+ return;
612
+ }
613
+ const f = fieldOf(name);
614
+ const shut = f?.type === "select" && f.create && comboOption(f, value)?.disabled;
615
+ el.value = value == null || shut ? "" : String(value);
616
+ comboHints.get(name)?.();
617
+ if (f?.type === "picker") {
618
+ const display = body.querySelector(`[data-picker-display="${name}"]`);
619
+ if (display) display.value = _pickerLabel(f, el.value);
620
+ }
621
+ },
622
+ setOptions(name, options) {
623
+ const f = fieldOf(name);
624
+ if (!f) return;
625
+ const c = controls.get(name);
626
+ if (c?.setOptions) {
627
+ optionsOf.set(name, options);
628
+ c.setOptions(options);
629
+ return;
630
+ }
631
+ if (f.type !== "select" || !f.create) return;
632
+ optionsOf.set(name, options);
633
+ const list = body.querySelector(`[data-datalist-for="${name}"]`);
634
+ if (list) list.innerHTML = _datalistOptions(options);
635
+ comboHints.get(name)?.();
636
+ }
637
+ };
638
+ const fire = (name) => {
639
+ try {
640
+ onFieldChange(name, name == null ? void 0 : api.get(name), api);
641
+ } catch (err) {
642
+ console.error("[modal] onFieldChange threw", err);
643
+ }
644
+ };
645
+ for (const f of formFields) {
646
+ const el = body.querySelector(`[name="${f.name}"]`);
647
+ el?.addEventListener("change", () => fire(f.name));
648
+ }
649
+ onChoice = (name) => {
650
+ clearFieldError(name);
651
+ fire(name);
652
+ };
653
+ fire(null);
654
+ }
655
+ body.querySelector("form").addEventListener("submit", async (e) => {
656
+ e.preventDefault();
657
+ const submitBtn = body.querySelector('button[type="submit"]');
658
+ showError("");
659
+ const data = {};
660
+ for (const f of formFields) {
661
+ const control = controls.get(f.name);
662
+ const el = control ? null : body.querySelector(`[name="${f.name}"]`);
663
+ if (!control && !el) continue;
664
+ let v;
665
+ if (control) {
666
+ v = control.submit ? control.submit() : control.read();
667
+ if (v === void 0) v = f.type === "select" ? "" : null;
668
+ } else if (f.type === "checkbox") {
669
+ v = !!el.checked;
670
+ } else if (f.type === "number") {
671
+ v = el.value === "" ? null : Number(el.value);
672
+ } else {
673
+ v = el.value;
674
+ }
675
+ if (f.type === "select" && f.create) {
676
+ const spec = _createSpec(f);
677
+ const match = comboOption(f, v);
678
+ if (match) v = match.value;
679
+ else if (v) {
680
+ if (submitBtn) submitBtn.disabled = true;
681
+ let created;
682
+ let failMsg = null;
683
+ try {
684
+ created = await spec.onCreate(v);
685
+ } catch (err) {
686
+ created = null;
687
+ failMsg = err?.message || String(err);
688
+ }
689
+ if (submitBtn) submitBtn.disabled = false;
690
+ if (created == null) {
691
+ const hint = spec.hint || f.label.toLowerCase();
692
+ showError(failMsg ? `Couldn't create ${hint} "${v}": ${failMsg}` : `Couldn't create ${hint} "${v}". See the toast for details.`);
693
+ return;
694
+ }
695
+ v = created;
696
+ }
697
+ }
698
+ data[f.name] = v;
699
+ }
700
+ let firstBadName = null;
701
+ for (const f of formFields) {
702
+ clearFieldError(f.name);
703
+ if (rowHidden(f.name)) {
704
+ if (unavailable(f, data[f.name]) != null) data[f.name] = f.type === "radio" ? null : "";
705
+ continue;
706
+ }
707
+ const v = data[f.name];
708
+ let msg = f.required && _missing(f, v) ? "Required." : null;
709
+ if (!msg && _outOfRange(f, v)) msg = _rangeMessage(f);
710
+ if (!msg) {
711
+ const reason = unavailable(f, v);
712
+ if (reason != null) msg = reason ? `Not available: ${reason}` : "Not available.";
713
+ }
714
+ if (!msg) msg = controls.get(f.name)?.error?.() ?? null;
715
+ if (!msg && typeof f.validator === "function") {
716
+ try {
717
+ msg = f.validator(v, data);
718
+ } catch (err) {
719
+ msg = err?.message || String(err);
720
+ }
721
+ }
722
+ if (msg) {
723
+ setFieldError(f.name, msg);
724
+ if (!firstBadName) firstBadName = f.name;
725
+ }
726
+ }
727
+ if (firstBadName) {
728
+ controls.get(firstBadName)?.focus();
729
+ body.querySelector(`[data-picker="${firstBadName}"]`)?.focus();
730
+ body.querySelector(`[name="${firstBadName}"]:not([type="hidden"])`)?.focus();
731
+ return;
732
+ }
733
+ close(data);
734
+ });
735
+ requestAnimationFrame(() => {
736
+ const first = [...body.querySelectorAll('input:not([type="hidden"]), select, textarea')].find((el) => !el.closest("[data-field-row]")?.hidden);
737
+ first?.focus();
738
+ });
739
+ });
740
+ }
741
+ function openConfirm({
742
+ title,
743
+ message,
744
+ confirmLabel = "OK",
745
+ cancelLabel = "Cancel",
746
+ danger = false,
747
+ icon = null
748
+ } = {}) {
749
+ return new Promise((resolve) => {
750
+ const body = document.createElement("div");
751
+ body.className = "twm-modal__body-host";
752
+ const messageEl = document.createElement("div");
753
+ messageEl.className = "twm-modal__body twm-modal__body--text";
754
+ messageEl.textContent = message == null ? "" : String(message);
755
+ const actionsEl = document.createElement("div");
756
+ actionsEl.className = "twm-modal__actions";
757
+ const button = (action, className, label) => {
758
+ const btn = document.createElement("button");
759
+ btn.type = "button";
760
+ btn.className = className;
761
+ btn.dataset.action = action;
762
+ btn.textContent = label == null ? "" : String(label);
763
+ actionsEl.appendChild(btn);
764
+ };
765
+ button("cancel", "twm-btn", cancelLabel);
766
+ button("confirm", danger ? "twm-btn twm-btn--danger" : "twm-btn twm-btn--primary", confirmLabel);
767
+ body.append(messageEl, actionsEl);
768
+ let _resolved = false;
769
+ const win = new ManagedWindow({
770
+ container: _modalHost,
771
+ id: `twm-confirm-${_modalSeq++}`,
772
+ title: title || "Confirm",
773
+ icon: icon || (danger ? "warning" : "help"),
774
+ content: body,
775
+ modal: true,
776
+ canMinimize: false,
777
+ canMaximize: false,
778
+ canResize: false,
779
+ canDrag: true,
780
+ defaultWidth: 380,
781
+ defaultHeight: 180,
782
+ onClose: () => {
783
+ if (!_resolved) {
784
+ _resolved = true;
785
+ resolve(false);
786
+ }
787
+ }
788
+ });
789
+ win.show();
790
+ const close = (v) => {
791
+ if (_resolved) return;
792
+ _resolved = true;
793
+ resolve(v);
794
+ try {
795
+ win.close({ force: true });
796
+ } catch {
797
+ }
798
+ };
799
+ body.querySelector('[data-action="cancel"]').addEventListener("click", () => close(false));
800
+ body.querySelector('[data-action="confirm"]').addEventListener("click", () => close(true));
801
+ requestAnimationFrame(() => {
802
+ body.querySelector(`[data-action="${danger ? "cancel" : "confirm"}"]`)?.focus();
803
+ });
804
+ });
805
+ }
806
+ function openModal({
807
+ title = "Dialog",
808
+ icon = "info",
809
+ content = null,
810
+ actions = null,
811
+ width = 640,
812
+ height = 480,
813
+ onMount = null,
814
+ backdropBlur = void 0,
815
+ backdropOpacity = void 0,
816
+ maximizable = false,
817
+ onMaximizeChange = null
818
+ } = {}) {
819
+ return new Promise((resolve) => {
820
+ const body = document.createElement("div");
821
+ body.className = "twm-modal__body-host";
822
+ const bodyInner = document.createElement("div");
823
+ bodyInner.className = "twm-modal__body twm-modal__body--rich";
824
+ if (content instanceof HTMLElement) bodyInner.appendChild(content);
825
+ body.appendChild(bodyInner);
826
+ const acts = Array.isArray(actions) && actions.length > 0 ? actions : [{ label: "Close", value: null }];
827
+ const actionsEl = document.createElement("div");
828
+ actionsEl.className = "twm-modal__actions";
829
+ acts.forEach((a, i) => {
830
+ const btn = document.createElement("button");
831
+ btn.type = "button";
832
+ let cls = "twm-btn";
833
+ if (a.primary) cls += " twm-btn--primary";
834
+ if (a.danger) cls += " twm-btn--danger";
835
+ btn.className = cls;
836
+ if (a.icon) {
837
+ const glyph = document.createElement("span");
838
+ glyph.className = "material-symbols-outlined twm-btn__glyph";
839
+ glyph.textContent = String(a.icon);
840
+ glyph.setAttribute("aria-hidden", "true");
841
+ const text = document.createElement("span");
842
+ text.className = "twm-btn__label";
843
+ text.textContent = String(a.label || "");
844
+ btn.append(glyph, text);
845
+ } else {
846
+ btn.textContent = String(a.label || "");
847
+ }
848
+ btn.dataset.actionIdx = String(i);
849
+ if (a.disabled) btn.disabled = true;
850
+ if (a.value !== void 0 && a.value !== null) {
851
+ btn.dataset.actionValue = String(a.value);
852
+ }
853
+ actionsEl.appendChild(btn);
854
+ });
855
+ body.appendChild(actionsEl);
856
+ let _resolved = false;
857
+ let _onMax = null;
858
+ const _dropMaxListener = () => {
859
+ if (!_onMax) return;
860
+ window.removeEventListener("managed-window-maximized", _onMax);
861
+ _onMax = null;
862
+ };
863
+ const win = new ManagedWindow({
864
+ container: _modalHost,
865
+ id: `twm-modal-${_modalSeq++}`,
866
+ title,
867
+ icon,
868
+ content: body,
869
+ modal: true,
870
+ backdropBlur,
871
+ backdropOpacity,
872
+ canMinimize: false,
873
+ // C29. OPT-IN, and `false` is still the default — a confirmation
874
+ // and a two-field form have nothing to do with the extra room, and
875
+ // a button that grows a dialog nobody wanted grown is noise in the
876
+ // one place a user looks for the X. `onMaximize` is deliberately
877
+ // NOT set: see the C29 note in the header — with nothing claiming
878
+ // the gesture, `toggleMaximize` runs the rectangle, which is the
879
+ // only thing maximise can mean for a window with no tile.
880
+ canMaximize: !!maximizable,
881
+ canResize: true,
882
+ canDrag: true,
883
+ defaultWidth: width,
884
+ defaultHeight: height,
885
+ // Esc, the X and the backdrop all land here without passing through
886
+ // `close`, so the listener is dropped here as well as there — the
887
+ // three dismissals a user reaches for most are exactly the ones
888
+ // that would otherwise leak it.
889
+ onClose: () => {
890
+ _dropMaxListener();
891
+ if (!_resolved) {
892
+ _resolved = true;
893
+ resolve(null);
894
+ }
895
+ }
896
+ });
897
+ win.show();
898
+ if (maximizable && typeof onMaximizeChange === "function") {
899
+ _onMax = (e) => {
900
+ if (e.detail?.id !== win.id) return;
901
+ try {
902
+ onMaximizeChange(!!e.detail.maximized, bodyInner);
903
+ } catch (err) {
904
+ console.warn("[modal] onMaximizeChange threw", err);
905
+ }
906
+ };
907
+ window.addEventListener("managed-window-maximized", _onMax);
908
+ }
909
+ const close = (value) => {
910
+ if (_resolved) return;
911
+ _resolved = true;
912
+ _dropMaxListener();
913
+ resolve(value);
914
+ try {
915
+ win.close({ force: true });
916
+ } catch {
917
+ }
918
+ };
919
+ actionsEl.addEventListener("click", (e) => {
920
+ const btn = e.target.closest("[data-action-idx]");
921
+ if (!btn) return;
922
+ const idx = Number(btn.dataset.actionIdx);
923
+ const a = acts[idx];
924
+ if (!a) return;
925
+ if (a.keepOpen) {
926
+ try {
927
+ a.onClick?.(close);
928
+ } catch (err) {
929
+ console.warn(err);
930
+ }
931
+ return;
932
+ }
933
+ close(a.value);
934
+ });
935
+ const controls = {
936
+ // NOT `CSS.escape`. It is a browser global that jsdom does not
937
+ // provide, and this file is mounted under jsdom by four render
938
+ // tests — so reaching for it turns "the dialog is valid" into a
939
+ // ReferenceError in every one of them, and into nothing at all in a
940
+ // headless consumer. An attribute selector needs `"` and `\`
941
+ // escaped and nothing else.
942
+ actionButton: (value) => actionsEl.querySelector(
943
+ `[data-action-value="${String(value).replace(/["\\]/g, "\\$&")}"]`
944
+ ),
945
+ setActionEnabled(value, enabled) {
946
+ const btn = controls.actionButton(value);
947
+ if (btn) btn.disabled = !enabled;
948
+ return !!btn;
949
+ }
950
+ };
951
+ if (typeof onMount === "function") {
952
+ requestAnimationFrame(() => {
953
+ try {
954
+ onMount(bodyInner, controls);
955
+ } catch (err) {
956
+ console.warn(err);
957
+ }
958
+ });
959
+ }
960
+ requestAnimationFrame(() => {
961
+ const field = bodyInner.querySelector(
962
+ 'input:not([type="hidden"]):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])'
963
+ );
964
+ if (field) {
965
+ field.focus();
966
+ try {
967
+ field.setSelectionRange?.(field.value.length, field.value.length);
968
+ } catch {
969
+ }
970
+ return;
971
+ }
972
+ const idx = acts.findIndex((a) => a.primary);
973
+ const which = idx >= 0 ? idx : acts.length - 1;
974
+ actionsEl.querySelector(`[data-action-idx="${which}"]`)?.focus();
975
+ });
976
+ });
977
+ }
978
+ function _estimateHeight(fields) {
979
+ const height = (f) => {
980
+ if (f.section) return 32;
981
+ const hint = f.hint ? 16 : 0;
982
+ if (f.type === "textarea") return 32 + (Number(f.rows) || 4) * 18 + hint;
983
+ if (f.type === "checkbox" || f.type === "toggle") return 32 + hint;
984
+ if (f.type === "colorList") {
985
+ const n = Array.isArray(f.default) ? f.default.length : 0;
986
+ return 46 + Math.max(n, 1) * 40 + 40 + hint;
987
+ }
988
+ if (f.type === "color") return 46 + (Array.isArray(f.presets) && f.presets.length ? 28 : 0) + hint;
989
+ if (f.type === "radio") {
990
+ const opts = flatChoices(f.options);
991
+ return 12 + opts.reduce((h, o) => h + 22 + (o.description || o.reason ? 16 : 0), 0) + hint;
992
+ }
993
+ return 46 + hint;
994
+ };
995
+ let body = 0;
996
+ for (const f of fields) {
997
+ body += Array.isArray(f?.row) ? Math.max(0, ..._formFields(f.row).map(height)) : height(f);
998
+ }
999
+ const base = 36 + 46 + 28 + body;
1000
+ const max = Math.floor(window.innerHeight * 0.8);
1001
+ return Math.max(200, Math.min(base, max));
1002
+ }
1003
+ function _estimateWidth(fields) {
1004
+ const wide = fields.some((f) => f && (f.type === "textarea" || f.type === "colorList" || f.section || Array.isArray(f.row)));
1005
+ return wide ? 640 : 480;
1006
+ }
1007
+ function _formFields(entries) {
1008
+ return entries.flatMap((e) => Array.isArray(e?.row) ? e.row : [e]).filter((f) => f && f.section == null);
1009
+ }
1010
+ function _bounded(f) {
1011
+ return f.min != null || f.max != null;
1012
+ }
1013
+ function _outOfRange(f, v) {
1014
+ if (f.type !== "number" || !_bounded(f) || v == null || Number.isNaN(v)) return false;
1015
+ return f.min != null && v < Number(f.min) || f.max != null && v > Number(f.max);
1016
+ }
1017
+ function _rangeMessage(f) {
1018
+ if (f.min != null && f.max != null) return `Must be between ${f.min} and ${f.max}.`;
1019
+ return f.min != null ? `Must be at least ${f.min}.` : `Must be at most ${f.max}.`;
1020
+ }
1021
+ function _offered(options) {
1022
+ return flatChoices(options).filter((o) => !o.disabled);
1023
+ }
1024
+ function _datalistOptions(options) {
1025
+ return _offered(options).map((o) => `<option value="${_esc(o.value)}"></option>`).join("");
1026
+ }
1027
+ function _mountControl(host, f, value, onChange) {
1028
+ const slot = host.querySelector(`[data-control-slot="${f.name}"]`);
1029
+ if (!slot) return null;
1030
+ if (f.type === "toggle") {
1031
+ const toggle = new ToggleField({
1032
+ name: f.name,
1033
+ checked: value === true || value === "true",
1034
+ ariaLabel: f.label || f.name,
1035
+ onText: f.onText ?? null,
1036
+ offText: f.offText ?? null,
1037
+ disabled: !!f.disabled,
1038
+ compact: !!f.compact
1039
+ });
1040
+ slot.appendChild(toggle.render());
1041
+ return {
1042
+ read: () => toggle.get(),
1043
+ write: (v) => toggle.set(v),
1044
+ focus: () => toggle.input?.focus(),
1045
+ unavailable: () => null,
1046
+ dispose: () => toggle.dispose()
1047
+ };
1048
+ }
1049
+ if (f.type === "color" || f.type === "colorList") {
1050
+ const c2 = f.type === "color" ? new ColorField({ name: f.name, value: value ?? "", presets: f.presets, label: f.label }) : new ColorListField({ name: f.name, value: value ?? [], presets: f.presets });
1051
+ slot.replaceWith(c2.render());
1052
+ const list2 = f.type === "colorList";
1053
+ return {
1054
+ read: () => list2 ? c2.getTexts() : c2.getText(),
1055
+ submit: () => c2.getValue(),
1056
+ write: (v) => c2.setValue(v),
1057
+ focus: () => c2.focus(),
1058
+ unavailable: () => null,
1059
+ error: () => {
1060
+ if (c2.getValue() !== null) return null;
1061
+ if (!list2) return "Not a hex colour: use #rgb or #rrggbb.";
1062
+ const bad = c2.invalidIndexes().map((i) => i + 1).join(", ");
1063
+ return `Not a hex colour (row ${bad}): use #rgb or #rrggbb.`;
1064
+ },
1065
+ dispose: () => c2.dispose()
1066
+ };
1067
+ }
1068
+ const common = { keepMissing: false, loose: true };
1069
+ const disabledReason = (entry) => entry?.disabled ? entry.reason || "" : null;
1070
+ if (f.type === "select") {
1071
+ const c2 = selectBox(f.options, value, { ...common, valueAttr: "value", className: "" });
1072
+ c2.box.name = f.name;
1073
+ if (f.required) c2.box.required = true;
1074
+ slot.replaceWith(c2.box);
1075
+ return {
1076
+ // No option selected reads '', as an empty select always has (and
1077
+ // as it submits); a selected option reads as its own value.
1078
+ read: () => c2.entry() ? c2.entry().value : "",
1079
+ // A value the list does not offer enabled clears the select, as
1080
+ // writing an unknown `.value` always has (it then submits '').
1081
+ write: (v) => {
1082
+ if (!c2.select(v)) c2.box.selectedIndex = -1;
1083
+ },
1084
+ focus: () => c2.box.focus(),
1085
+ unavailable: () => disabledReason(c2.entry()),
1086
+ setOptions: c2.setOptions
1087
+ };
1088
+ }
1089
+ const list = (v) => Array.isArray(v) ? v : v == null || v === "" ? [] : [v];
1090
+ if (f.type === "checklist") {
1091
+ const c2 = toggleChips({
1092
+ ...common,
1093
+ options: f.options,
1094
+ value: list(value),
1095
+ label: f.label || f.name,
1096
+ onChange: () => onChange(f.name)
1097
+ });
1098
+ slot.replaceWith(c2.el);
1099
+ return {
1100
+ read: c2.valueOf,
1101
+ write: (v) => c2.setValue(list(v)),
1102
+ focus: () => c2.el.querySelector("button:not(:disabled)")?.focus(),
1103
+ unavailable: () => null,
1104
+ // a disabled chip cannot be pressed, by hand or by set
1105
+ setOptions: c2.setOptions
1106
+ };
1107
+ }
1108
+ const c = choiceList({
1109
+ ...common,
1110
+ options: f.options,
1111
+ value,
1112
+ label: f.label || f.name,
1113
+ name: `twm-form-${_modalSeq}-${f.name}`,
1114
+ onChange: () => onChange(f.name)
1115
+ });
1116
+ slot.replaceWith(c.el);
1117
+ return {
1118
+ read: () => c.valueOf() ?? null,
1119
+ write: (v) => {
1120
+ c.select(v);
1121
+ },
1122
+ focus: c.focus,
1123
+ unavailable: () => disabledReason(c.entry()),
1124
+ setOptions: c.setOptions
1125
+ };
1126
+ }
1127
+ function _missing(f, v) {
1128
+ if (f.type === "picker") return !v;
1129
+ if (f.type === "checklist") return !Array.isArray(v) || v.length === 0;
1130
+ if (f.type === "radio") return v == null;
1131
+ if (f.type === "color") return v === "";
1132
+ if (f.type === "colorList") return Array.isArray(v) && v.length === 0;
1133
+ return false;
1134
+ }
1135
+ function _createSpec(f) {
1136
+ if (!f || !f.create) return null;
1137
+ if (f.create === true) return { onCreate: async (v) => v, hint: null };
1138
+ return {
1139
+ onCreate: typeof f.create.onCreate === "function" ? f.create.onCreate : async (v) => v,
1140
+ hint: f.create.hint ?? null
1141
+ };
1142
+ }
1143
+ function _pickerLabel(f, value) {
1144
+ if (typeof f.displayFor !== "function") return value == null ? "" : String(value);
1145
+ try {
1146
+ return String(f.displayFor(value) ?? "");
1147
+ } catch (err) {
1148
+ console.warn("[modal] displayFor threw", err);
1149
+ return String(value ?? "");
1150
+ }
1151
+ }
1152
+ function _wirePicker(host, f) {
1153
+ const hidden = host.querySelector(`input[type="hidden"][name="${f.name}"]`);
1154
+ const display = host.querySelector(`[data-picker-display="${f.name}"]`);
1155
+ const btn = host.querySelector(`[data-picker="${f.name}"]`);
1156
+ if (!hidden || !display || !btn || typeof f.pick !== "function") return;
1157
+ let busy = false;
1158
+ const open = async () => {
1159
+ if (busy) return;
1160
+ busy = true;
1161
+ let picked;
1162
+ try {
1163
+ picked = await f.pick(hidden.value);
1164
+ } catch (err) {
1165
+ console.warn("[modal] pick threw", err);
1166
+ picked = null;
1167
+ } finally {
1168
+ busy = false;
1169
+ }
1170
+ if (picked == null) return;
1171
+ const isPair = typeof picked === "object";
1172
+ hidden.value = String((isPair ? picked.value : picked) ?? "");
1173
+ display.value = isPair && picked.label != null ? String(picked.label) : _pickerLabel(f, hidden.value);
1174
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
1175
+ };
1176
+ btn.addEventListener("click", open);
1177
+ display.addEventListener("click", open);
1178
+ }
1179
+ function _wireComboboxHint(host, f, optionsOf) {
1180
+ const input = host.querySelector(`input[name="${f.name}"]`);
1181
+ const hintEl = host.querySelector(`.twm-combobox__hint[data-for="${f.name}"]`);
1182
+ if (!input || !hintEl) return;
1183
+ if (f.create === true) return;
1184
+ const label = _createSpec(f).hint || "new entry";
1185
+ const update = () => {
1186
+ const v = input.value.trim();
1187
+ const known = new Set(flatChoices(optionsOf()).map((o) => String(o.value)));
1188
+ if (v && !known.has(v)) {
1189
+ hintEl.textContent = `\u21B3 will create ${label} \u201C${v}\u201D`;
1190
+ hintEl.hidden = false;
1191
+ } else {
1192
+ hintEl.hidden = true;
1193
+ }
1194
+ };
1195
+ input.addEventListener("input", update);
1196
+ update();
1197
+ return update;
1198
+ }
1199
+ function _renderEntry(f, defaults) {
1200
+ if (f && f.section != null) {
1201
+ const attr = f.name ? ` data-field-row="${f.name}"` : "";
1202
+ return `<div class="twm-modal__section"${attr}>${_esc(f.section)}</div>`;
1203
+ }
1204
+ if (Array.isArray(f?.row)) {
1205
+ const cells = _formFields(f.row).map((g) => _renderField(g, defaults[g.name] ?? g.default));
1206
+ return `<div class="twm-modal__field-group">${cells.join("")}</div>`;
1207
+ }
1208
+ return _renderField(f, defaults[f.name] ?? f.default);
1209
+ }
1210
+ function _renderField(f, value) {
1211
+ const required = f.required ? "required" : "";
1212
+ const rowMod = f.type === "textarea" || f.type === "checklist" || f.type === "radio" ? " twm-modal__row--multiline" : "";
1213
+ const hintHtml = f.hint ? `<div class="twm-modal__hint--field">${_esc(f.hint)}</div>` : "";
1214
+ const errHtml = `<div class="twm-modal__field-error" data-field-error="${f.name}" hidden></div>`;
1215
+ const wrap = (inner, tag = "label") => `
1216
+ <${tag} class="twm-modal__row${rowMod}" data-field-row="${f.name}">
1217
+ <span>${_esc(f.label || "")}</span>
1218
+ ${inner}
1219
+ ${hintHtml}
1220
+ ${errHtml}
1221
+ </${tag}>
1222
+ `;
1223
+ const slot = `<span data-control-slot="${f.name}"></span>`;
1224
+ if (f.type === "checkbox") {
1225
+ const checked = value === true || value === "true" ? "checked" : "";
1226
+ return `
1227
+ <label class="twm-modal__row" data-field-row="${f.name}">
1228
+ <span></span>
1229
+ <span class="twm-modal__check">
1230
+ <input name="${f.name}" type="checkbox" ${checked}>
1231
+ <span>${_esc(f.label || "")}</span>
1232
+ </span>
1233
+ ${hintHtml}
1234
+ ${errHtml}
1235
+ </label>
1236
+ `;
1237
+ }
1238
+ if (f.type === "checklist" || f.type === "radio") return wrap(slot, "div");
1239
+ if (f.type === "toggle") {
1240
+ return wrap(`<span class="twm-modal__toggle" data-control-slot="${f.name}"></span>`, "div");
1241
+ }
1242
+ if (f.type === "textarea") {
1243
+ const v2 = value == null ? "" : String(value);
1244
+ const rows = f.rows != null ? `rows="${Number(f.rows) || 4}"` : 'rows="4"';
1245
+ const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
1246
+ return wrap(`<textarea name="${f.name}" ${rows} ${placeholder2} ${required}>${_esc(v2)}</textarea>`);
1247
+ }
1248
+ if (f.type === "color" || f.type === "colorList") {
1249
+ if (f.type === "color") return wrap(slot);
1250
+ return `
1251
+ <div class="twm-modal__row twm-modal__row--multiline" data-field-row="${f.name}" role="group" aria-label="${_esc(f.label || "")}">
1252
+ <span>${_esc(f.label || "")}</span>
1253
+ ${slot}
1254
+ ${hintHtml}
1255
+ ${errHtml}
1256
+ </div>
1257
+ `;
1258
+ }
1259
+ const v = value == null ? "" : String(value);
1260
+ if (f.type === "picker") {
1261
+ const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
1262
+ return wrap(`
1263
+ <div class="twm-picker">
1264
+ <input type="hidden" name="${f.name}" value="${_esc(v)}">
1265
+ <input class="twm-picker__display" data-picker-display="${f.name}" type="text"
1266
+ value="${_esc(_pickerLabel(f, v))}" ${placeholder2} readonly>
1267
+ <button type="button" class="twm-btn twm-picker__btn" data-picker="${f.name}"
1268
+ aria-label="Search" title="Search">
1269
+ <span class="material-symbols-outlined" aria-hidden="true">search</span>
1270
+ </button>
1271
+ </div>
1272
+ `);
1273
+ }
1274
+ if (f.type === "select" && f.create) {
1275
+ const shown = findChoice(flatChoices(f.options), value, true)?.disabled ? "" : v;
1276
+ const dlId = `ea-dl-${f.name}-${_modalSeq}`;
1277
+ const opts = _datalistOptions(f.options);
1278
+ const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : 'placeholder="type to pick or create\u2026"';
1279
+ return wrap(`
1280
+ <div class="twm-combobox">
1281
+ <input name="${f.name}" type="text" list="${dlId}"
1282
+ value="${_esc(shown)}" ${placeholder2} ${required}
1283
+ autocomplete="off">
1284
+ <datalist id="${dlId}" data-datalist-for="${f.name}">${opts}</datalist>
1285
+ <span class="twm-combobox__hint" data-for="${f.name}" hidden></span>
1286
+ </div>
1287
+ `);
1288
+ }
1289
+ if (f.type === "select") return wrap(slot);
1290
+ const step = f.step != null ? `step="${f.step}"` : "";
1291
+ const placeholder = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
1292
+ const type = f.type === "number" || f.type === "password" ? f.type : "text";
1293
+ let bounds = "";
1294
+ if (f.type === "number" && f.min != null) bounds += ` min="${_esc(f.min)}"`;
1295
+ if (f.type === "number" && f.max != null) bounds += ` max="${_esc(f.max)}"`;
1296
+ return wrap(`<input name="${f.name}" type="${type}" value="${_esc(v)}" ${step}${bounds} ${placeholder} ${required}>`);
1297
+ }
1298
+ function _esc(s) {
1299
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1300
+ }
1301
+
1302
+ export {
1303
+ SortableList,
1304
+ ColorListField,
1305
+ setModalHost,
1306
+ modalHost,
1307
+ openForm,
1308
+ openConfirm,
1309
+ openModal
1310
+ };
1311
+ //# sourceMappingURL=chunk-D3COLZZ2.js.map