flexdesk 0.17.17 → 0.18.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 (300) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +0 -0
  3. package/README.md +1 -0
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +0 -0
  6. package/charts.js +0 -0
  7. package/core.js +0 -0
  8. package/css/flexdesk.css +7499 -5851
  9. package/css/reset.css +1 -1
  10. package/css/tokens.css +23 -1
  11. package/dist/canvas.js +0 -0
  12. package/dist/canvas.js.map +0 -0
  13. package/dist/charts.js +10 -6
  14. package/dist/charts.js.map +1 -1
  15. package/dist/chunk-3PHCPZHT.js +0 -0
  16. package/dist/chunk-3PHCPZHT.js.map +0 -0
  17. package/dist/chunk-5W4EEKSK.js +778 -0
  18. package/dist/chunk-5W4EEKSK.js.map +7 -0
  19. package/dist/chunk-DNJ3N4T4.js +82 -0
  20. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +0 -0
  22. package/dist/chunk-FOOS3T5L.js.map +0 -0
  23. package/dist/chunk-GOQLR5WR.js +0 -0
  24. package/dist/chunk-GOQLR5WR.js.map +0 -0
  25. package/dist/{chunk-JKZLNKG6.js → chunk-HKPB6OFA.js} +90 -25
  26. package/dist/{chunk-JKZLNKG6.js.map → chunk-HKPB6OFA.js.map} +2 -2
  27. package/dist/chunk-HNU6MM2B.js +0 -0
  28. package/dist/chunk-HNU6MM2B.js.map +0 -0
  29. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  30. package/dist/chunk-JYWURG5T.js +0 -0
  31. package/dist/chunk-JYWURG5T.js.map +0 -0
  32. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  33. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  34. package/dist/chunk-N7HC3ADV.js +0 -0
  35. package/dist/chunk-N7HC3ADV.js.map +0 -0
  36. package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
  37. package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
  38. package/dist/chunk-PAI2ZUWV.js +260 -0
  39. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  40. package/dist/chunk-Q7BALEPX.js +527 -0
  41. package/dist/chunk-Q7BALEPX.js.map +7 -0
  42. package/dist/chunk-QXAOTGCC.js +40 -0
  43. package/dist/chunk-QXAOTGCC.js.map +7 -0
  44. package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
  45. package/dist/chunk-RKVYIXDC.js.map +7 -0
  46. package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
  47. package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
  48. package/dist/chunk-UKP22RUG.js +219 -0
  49. package/dist/chunk-UKP22RUG.js.map +7 -0
  50. package/dist/chunk-VLOAI3HS.js +43 -0
  51. package/dist/chunk-VLOAI3HS.js.map +7 -0
  52. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  53. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  54. package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
  55. package/dist/chunk-ZL3VKINP.js.map +7 -0
  56. package/dist/editor.js +3 -2
  57. package/dist/editor.js.map +1 -1
  58. package/dist/flexdesk.css +7499 -5851
  59. package/dist/flow.js +29 -97
  60. package/dist/flow.js.map +3 -3
  61. package/dist/git.js +1356 -0
  62. package/dist/git.js.map +7 -0
  63. package/dist/host.js +0 -0
  64. package/dist/host.js.map +0 -0
  65. package/dist/reset.css +1 -1
  66. package/dist/tile_registry-6WZPTUZV.js +0 -0
  67. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  68. package/dist/tiles.js +20 -15
  69. package/dist/tiles.js.map +2 -2
  70. package/dist/tokens.css +23 -1
  71. package/dist/widgets.js +3969 -1189
  72. package/dist/widgets.js.map +4 -4
  73. package/dist/wm.js +121 -33
  74. package/dist/wm.js.map +2 -2
  75. package/docs/README.md +9 -4
  76. package/docs/best-practices.md +2 -2
  77. package/docs/components/chat-shell.md +198 -0
  78. package/docs/components/command-palette.md +0 -0
  79. package/docs/components/conversation.md +10 -0
  80. package/docs/components/data-table-grid.md +0 -0
  81. package/docs/components/data-table.md +0 -0
  82. package/docs/components/editor.md +0 -0
  83. package/docs/components/filter-builder.md +246 -0
  84. package/docs/components/flow-canvas-editor.md +0 -0
  85. package/docs/components/flow-editors.md +0 -0
  86. package/docs/components/flow-lane-editor.md +0 -0
  87. package/docs/components/flow-outline-editor.md +0 -0
  88. package/docs/components/forms-and-dialogs.md +22 -0
  89. package/docs/components/git.md +355 -0
  90. package/docs/components/input-fields.md +167 -2
  91. package/docs/components/keymap.md +8 -0
  92. package/docs/components/markdown-and-outline.md +0 -0
  93. package/docs/components/menus.md +120 -3
  94. package/docs/components/panels-and-desktops.md +16 -5
  95. package/docs/components/record-list-and-fact-table.md +0 -0
  96. package/docs/components/settings-view.md +0 -0
  97. package/docs/components/shell.md +104 -1
  98. package/docs/components/wizard.md +0 -0
  99. package/docs/components/workspace-picker.md +149 -0
  100. package/docs/concepts/building-and-packaging.md +2 -2
  101. package/docs/concepts/domain-neutrality.md +0 -0
  102. package/docs/concepts/host-port.md +0 -0
  103. package/docs/concepts/packages-and-entry-points.md +14 -13
  104. package/docs/concepts/peer-dependencies.md +0 -0
  105. package/docs/concepts/state-guard.md +3 -3
  106. package/docs/quickstart.md +1 -1
  107. package/editor.js +0 -0
  108. package/flow.js +0 -0
  109. package/git.js +13 -0
  110. package/host.js +0 -0
  111. package/package.json +6 -1
  112. package/src/canvas/canvas_adapter.js +0 -0
  113. package/src/canvas/edge_router.js +0 -0
  114. package/src/canvas/graph/connector_router.js +0 -0
  115. package/src/canvas/graph/history_service.js +0 -0
  116. package/src/canvas/graph/node_platform.js +0 -0
  117. package/src/canvas/index.js +0 -0
  118. package/src/canvas/layered_layout.js +0 -0
  119. package/src/charts/data_series_plot_window.js +0 -0
  120. package/src/charts/downsample.js +0 -0
  121. package/src/charts/plot_config.js +0 -0
  122. package/src/charts/plotly_wrapper.js +0 -0
  123. package/src/core/event_bus.js +0 -0
  124. package/src/core/logging.js +0 -0
  125. package/src/core/settings.js +0 -0
  126. package/src/core/state_guard.js +0 -0
  127. package/src/core/state_machine.js +0 -0
  128. package/src/editor/editor_pane.js +0 -0
  129. package/src/editor/monaco_editor_factory.js +0 -0
  130. package/src/editor/monaco_loader.js +0 -0
  131. package/src/editor/notebook_search.js +0 -0
  132. package/src/editor/notebook_undo_manager.js +0 -0
  133. package/src/editor/split_pane_container.js +0 -0
  134. package/src/flow/canvas_editor/connections.js +0 -0
  135. package/src/flow/canvas_editor/editor.js +0 -0
  136. package/src/flow/canvas_editor/index.js +0 -0
  137. package/src/flow/canvas_editor/layout.js +0 -0
  138. package/src/flow/canvas_editor/strings.js +0 -0
  139. package/src/flow/kit/always_before.js +0 -0
  140. package/src/flow/kit/catalogue.js +0 -0
  141. package/src/flow/kit/chip_input.js +0 -0
  142. package/src/flow/kit/dom.js +28 -119
  143. package/src/flow/kit/findings.js +0 -0
  144. package/src/flow/kit/history.js +0 -0
  145. package/src/flow/kit/index.js +0 -0
  146. package/src/flow/kit/keys.js +0 -0
  147. package/src/flow/kit/logic_graph.js +0 -0
  148. package/src/flow/kit/references.js +0 -0
  149. package/src/flow/kit/settings_panel.js +0 -0
  150. package/src/flow/kit/settings_schema.js +0 -0
  151. package/src/flow/kit/step_picker.js +1 -1
  152. package/src/flow/kit/strings.js +0 -0
  153. package/src/flow/kit/value_picker.js +1 -1
  154. package/src/flow/kit/widgets.js +0 -0
  155. package/src/flow/lanes/editor.js +0 -0
  156. package/src/flow/lanes/index.js +0 -0
  157. package/src/flow/lanes/layout.js +0 -0
  158. package/src/flow/lanes/pipeline.js +0 -0
  159. package/src/flow/lanes/preview.js +0 -0
  160. package/src/flow/lanes/strings.js +0 -0
  161. package/src/flow/outline/editor.js +0 -0
  162. package/src/flow/outline/index.js +0 -0
  163. package/src/flow/outline/mapping.js +0 -0
  164. package/src/flow/outline/menu.js +1 -1
  165. package/src/flow/outline/operations.js +0 -0
  166. package/src/flow/outline/recognise.js +0 -0
  167. package/src/flow/outline/strings.js +0 -0
  168. package/src/git/diff_viewer.js +212 -0
  169. package/src/git/diff_window.js +51 -0
  170. package/src/git/git_graph.js +401 -0
  171. package/src/git/git_ops.js +61 -0
  172. package/src/git/git_panel.js +755 -0
  173. package/src/git/index.js +9 -0
  174. package/src/git/line_diff.js +202 -0
  175. package/src/help/help_modal.js +0 -0
  176. package/src/help/help_registry.js +0 -0
  177. package/src/host/external_links.js +0 -0
  178. package/src/host/file_picker.js +0 -0
  179. package/src/host/host.js +0 -0
  180. package/src/host/photino_host.js +0 -0
  181. package/src/host/photino_host.md +0 -0
  182. package/src/host/pywebview_host.js +0 -0
  183. package/src/tiles/chart_tiles.js +0 -0
  184. package/src/tiles/config_schema.js +0 -0
  185. package/src/tiles/figure_tile.js +0 -0
  186. package/src/tiles/layout_persistence.js +0 -0
  187. package/src/tiles/list_tiles.js +0 -0
  188. package/src/tiles/scope_picker.js +0 -0
  189. package/src/tiles/tile_base.js +5 -4
  190. package/src/tiles/tile_config_form.js +0 -0
  191. package/src/tiles/tile_registry.js +0 -0
  192. package/src/tiling/bottom_panel_tabs.js +0 -0
  193. package/src/tiling/command_palette.js +0 -0
  194. package/src/tiling/content_registry.js +0 -0
  195. package/src/tiling/desktops.js +10 -8
  196. package/src/tiling/entity_sources.js +0 -0
  197. package/src/tiling/keymap.js +6 -5
  198. package/src/tiling/kind_taxonomy.js +15 -2
  199. package/src/tiling/landing_table.js +0 -0
  200. package/src/tiling/loading_overlay.js +0 -0
  201. package/src/tiling/nav_panel.js +0 -0
  202. package/src/tiling/page_factory.js +0 -0
  203. package/src/tiling/panel_availability.js +0 -0
  204. package/src/tiling/panel_keys.js +0 -0
  205. package/src/tiling/shell.js +124 -17
  206. package/src/tiling/tab_strip.js +0 -0
  207. package/src/tiling/tile_renderer.js +2 -3
  208. package/src/tiling/tile_tab_menu.js +0 -0
  209. package/src/tiling/zoom.js +0 -0
  210. package/src/ui/base/component_base.js +0 -0
  211. package/src/ui/base/controller_base.js +0 -0
  212. package/src/ui/base/page_base.js +0 -0
  213. package/src/ui/components/about_dialog.js +0 -0
  214. package/src/ui/components/action_dropdown.js +4 -78
  215. package/src/ui/components/alert_log.js +0 -0
  216. package/src/ui/components/alerts_pane.js +0 -0
  217. package/src/ui/components/anchored_layer.js +255 -0
  218. package/src/ui/components/attribute_list_editor.js +0 -0
  219. package/src/ui/components/autocomplete_field.js +0 -0
  220. package/src/ui/components/cell_editor_host.js +0 -0
  221. package/src/ui/components/cell_editors.js +0 -0
  222. package/src/ui/components/cell_grid.js +0 -0
  223. package/src/ui/components/cell_paste.js +0 -0
  224. package/src/ui/components/cell_selection.js +0 -0
  225. package/src/ui/components/chat_shell.js +920 -0
  226. package/src/ui/components/chat_state_machine.js +223 -0
  227. package/src/ui/components/choice_controls.js +45 -2
  228. package/src/ui/components/color_field.js +3 -2
  229. package/src/ui/components/color_list_field.js +3 -2
  230. package/src/ui/components/column_fit.js +0 -0
  231. package/src/ui/components/column_groups.js +0 -0
  232. package/src/ui/components/column_order.js +0 -0
  233. package/src/ui/components/combobox.js +422 -0
  234. package/src/ui/components/conversation_view.js +17 -2
  235. package/src/ui/components/crosstab.js +0 -0
  236. package/src/ui/components/data_table.js +2 -2
  237. package/src/ui/components/detail_header.js +0 -0
  238. package/src/ui/components/document_outline.js +0 -0
  239. package/src/ui/components/drag_reorder.js +0 -0
  240. package/src/ui/components/fact_table.js +0 -0
  241. package/src/ui/components/fill_handle.js +0 -0
  242. package/src/ui/components/filter_builder.js +766 -0
  243. package/src/ui/components/filter_model.js +614 -0
  244. package/src/ui/components/gallery_picker.js +0 -0
  245. package/src/ui/components/header_actions.js +0 -0
  246. package/src/ui/components/inline_renamer.js +0 -0
  247. package/src/ui/components/job_progress.js +0 -0
  248. package/src/ui/components/managed_window.js +24 -47
  249. package/src/ui/components/menu_bar.js +101 -24
  250. package/src/ui/components/modal.js +79 -109
  251. package/src/ui/components/option_list.js +160 -0
  252. package/src/ui/components/popover.js +94 -0
  253. package/src/ui/components/progress.js +0 -0
  254. package/src/ui/components/recent_items.js +169 -0
  255. package/src/ui/components/record_list.js +0 -0
  256. package/src/ui/components/row_activation.js +0 -0
  257. package/src/ui/components/section_stack.js +0 -0
  258. package/src/ui/components/select.js +322 -0
  259. package/src/ui/components/series.js +0 -0
  260. package/src/ui/components/settings_view.js +0 -0
  261. package/src/ui/components/slide_out_panel.js +0 -0
  262. package/src/ui/components/slider_field.js +0 -0
  263. package/src/ui/components/sortable_list.js +0 -0
  264. package/src/ui/components/staged_edits.js +0 -0
  265. package/src/ui/components/staged_history.js +0 -0
  266. package/src/ui/components/table_filter_chips.js +0 -0
  267. package/src/ui/components/table_state_store.js +0 -0
  268. package/src/ui/components/table_structure.js +0 -0
  269. package/src/ui/components/table_values.js +0 -0
  270. package/src/ui/components/tabs.js +0 -0
  271. package/src/ui/components/tag_input.js +77 -8
  272. package/src/ui/components/toast.js +0 -0
  273. package/src/ui/components/toggle_field.js +0 -0
  274. package/src/ui/components/tsv.js +0 -0
  275. package/src/ui/components/window_taskbar.js +0 -0
  276. package/src/ui/components/workspace_picker.js +1155 -0
  277. package/src/ui/controllers/window_chrome_controller.js +4 -1
  278. package/src/ui/utils/auto_scrollbars.js +0 -0
  279. package/src/ui/utils/fire_event.js +17 -0
  280. package/src/ui/utils/focusable.js +25 -0
  281. package/src/ui/utils/markdown.js +0 -0
  282. package/src/ui/utils/modal_host.js +27 -0
  283. package/src/ui/utils/native_context_menu.js +0 -0
  284. package/src/ui/utils/overlay_scrollbar.js +0 -0
  285. package/src/ui/utils/raf_resize_observer.js +0 -0
  286. package/src/ui/utils/scroll_lock.js +0 -0
  287. package/src/ui/utils/strip_scroll.js +0 -0
  288. package/src/ui/utils/tooltip_service.js +0 -0
  289. package/tiles.js +0 -0
  290. package/widgets.js +12 -2
  291. package/wm.js +0 -0
  292. package/css/base.css +0 -10527
  293. package/css/overrides.css +0 -2596
  294. package/dist/chunk-7JI7HHSY.js +0 -107
  295. package/dist/chunk-7JI7HHSY.js.map +0 -7
  296. package/dist/chunk-MIW7D6WC.js +0 -526
  297. package/dist/chunk-MIW7D6WC.js.map +0 -7
  298. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  299. package/dist/chunk-XJNADRGR.js.map +0 -7
  300. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -77,7 +77,7 @@
77
77
  * }
78
78
  *
79
79
  * Dependent-property pick-or-create — a `select` field that carries a
80
- * `create` spec renders as a **type-ahead combobox** instead of a
80
+ * `create` spec renders as a `createCombobox` instead of a
81
81
  * dropdown: the user picks an existing option OR types a new value.
82
82
  * On submit, a value that isn't an existing option is created via
83
83
  * `create.onCreate(value)` before the form resolves:
@@ -115,29 +115,11 @@ import { ToggleField } from '../../ui/components/toggle_field.js';
115
115
  import { ColorField } from './color_field.js';
116
116
  import { ColorListField } from './color_list_field.js';
117
117
  import { choiceList, findChoice, flatChoices, selectBox, toggleChips } from './choice_controls.js';
118
+ import { createCombobox } from './combobox.js';
119
+ import { modalHost, setModalHost } from '../utils/modal_host.js';
120
+ import { fireEvent } from '../utils/fire_event.js';
118
121
 
119
- /**
120
- * C25. WHERE A DIALOG OPENS, when the application spans more than one window.
121
- *
122
- * Every modal here is a `ManagedWindow` with no `container`, so it mounts into
123
- * `document.body` — and "document" is the document this MODULE was loaded in.
124
- * A consumer that opens a second browser window with `window.open` and builds
125
- * its DOM from the opener's realm (which is how a pop-out grid works: same
126
- * modules, same JS context, a different document) therefore gets its dialogs in
127
- * the window it popped OUT of. The user presses "Add column" on their second
128
- * monitor and a dialog appears on the first, behind whatever is there.
129
- *
130
- * Passing a container at each call site was the other option and it is worse:
131
- * twenty-one of them across eleven files, every one of which would have to be
132
- * given a document it has no other reason to know about, and any one missed is
133
- * this bug again with no way to see it from here.
134
- *
135
- * So the HOST is ambient and the consumer sets it when its focus moves. Null —
136
- * every consumer today — means `document.body`, exactly as before.
137
- */
138
- let _modalHost = null;
139
- export function setModalHost(el) { _modalHost = el || null; }
140
- export function modalHost() { return _modalHost; }
122
+ export { setModalHost, modalHost };
141
123
 
142
124
  let _modalSeq = 1;
143
125
 
@@ -198,7 +180,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
198
180
  `;
199
181
 
200
182
  const win = new ManagedWindow({
201
- container: _modalHost,
183
+ container: modalHost(),
202
184
  id: `twm-modal-${_modalSeq++}`,
203
185
  title: title || 'Dialog',
204
186
  icon: 'edit_note',
@@ -237,12 +219,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
237
219
  // The options each field offers NOW — `setOptions` replaces them, the
238
220
  // caller's field object is never written to.
239
221
  const optionsOf = new Map(formFields.map((f) => [f.name, f.options]));
240
- const comboHints = new Map();
241
222
  for (const f of formFields) {
242
- if (f.type === 'select' && f.create) {
243
- const update = _wireComboboxHint(body, f, () => optionsOf.get(f.name));
244
- if (update) comboHints.set(f.name, update);
245
- }
246
223
  if (f.type === 'picker') _wirePicker(body, f);
247
224
  }
248
225
  const rowHidden = (name) => !!body.querySelector(`[data-field-row="${name}"]`)?.hidden;
@@ -281,13 +258,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
281
258
  * read a combobox through this, so they agree (`'2'` is option `2`). */
282
259
  const comboOption = (f, text) => findChoice(flatChoices(optionsOf.get(f.name)), text, true);
283
260
  /** A disabled option's reason ('' with none) when one is the value, else null. */
284
- const unavailable = (f, value) => {
285
- const c = controls.get(f.name);
286
- if (c) return c.unavailable();
287
- if (f.type !== 'select' || !f.create || value == null || value === '') return null;
288
- const o = comboOption(f, value);
289
- return o?.disabled ? (o.reason || '') : null;
290
- };
261
+ const unavailable = (f) => controls.get(f.name)?.unavailable() ?? null;
291
262
 
292
263
  // Clear per-field errors on input so the user sees the row recover.
293
264
  for (const f of formFields) {
@@ -337,11 +308,6 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
337
308
  const el = body.querySelector(`[name="${name}"]`);
338
309
  if (!el) return undefined;
339
310
  if (el.type === 'checkbox') return !!el.checked;
340
- const f = fieldOf(name);
341
- if (f?.type === 'select' && f.create) {
342
- const match = comboOption(f, el.value);
343
- return match ? match.value : el.value;
344
- }
345
311
  return el.value;
346
312
  },
347
313
  set(name, value) {
@@ -350,29 +316,17 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
350
316
  if (!el) return;
351
317
  if (el.type === 'checkbox') { el.checked = !!value; return; }
352
318
  const f = fieldOf(name);
353
- // A combobox does not take a disabled option's text either.
354
- const shut = f?.type === 'select' && f.create && comboOption(f, value)?.disabled;
355
- el.value = value == null || shut ? '' : String(value);
356
- comboHints.get(name)?.();
319
+ el.value = value == null ? '' : String(value);
357
320
  if (f?.type === 'picker') {
358
321
  const display = body.querySelector(`[data-picker-display="${name}"]`);
359
322
  if (display) display.value = _pickerLabel(f, el.value);
360
323
  }
361
324
  },
362
325
  setOptions(name, options) {
363
- const f = fieldOf(name);
364
- if (!f) return;
365
326
  const c = controls.get(name);
366
- if (c?.setOptions) {
367
- optionsOf.set(name, options);
368
- c.setOptions(options);
369
- return;
370
- }
371
- if (f.type !== 'select' || !f.create) return;
327
+ if (!c?.setOptions) return;
372
328
  optionsOf.set(name, options);
373
- const list = body.querySelector(`[data-datalist-for="${name}"]`);
374
- if (list) list.innerHTML = _datalistOptions(options);
375
- comboHints.get(name)?.();
329
+ c.setOptions(options);
376
330
  },
377
331
  };
378
332
  const fire = (name) => {
@@ -444,7 +398,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
444
398
  if (rowHidden(f.name)) {
445
399
  // Not asked about — but a disabled option is still never
446
400
  // the value: a hidden row holding one submits no value.
447
- if (unavailable(f, data[f.name]) != null) data[f.name] = f.type === 'radio' ? null : '';
401
+ if (unavailable(f) != null) data[f.name] = f.type === 'radio' ? null : '';
448
402
  continue;
449
403
  }
450
404
  const v = data[f.name];
@@ -454,7 +408,7 @@ export function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK'
454
408
  let msg = f.required && _missing(f, v) ? 'Required.' : null;
455
409
  if (!msg && _outOfRange(f, v)) msg = _rangeMessage(f);
456
410
  if (!msg) {
457
- const reason = unavailable(f, v);
411
+ const reason = unavailable(f);
458
412
  if (reason != null) msg = reason ? `Not available: ${reason}` : 'Not available.';
459
413
  }
460
414
  if (!msg) msg = controls.get(f.name)?.error?.() ?? null;
@@ -527,7 +481,7 @@ export function openConfirm({
527
481
 
528
482
  let _resolved = false;
529
483
  const win = new ManagedWindow({
530
- container: _modalHost,
484
+ container: modalHost(),
531
485
  id: `twm-confirm-${_modalSeq++}`,
532
486
  title: title || 'Confirm',
533
487
  icon: icon || (danger ? 'warning' : 'help'),
@@ -601,6 +555,9 @@ export function openConfirm({
601
555
  * existing caller has always got.
602
556
  * - `onMaximizeChange` `(maximized, contentEl) => void`, called on each
603
557
  * flip. Only useful with `maximizable`.
558
+ * - `beforeClose` `() => boolean | Promise<boolean>`: asked when Esc or
559
+ * the X would dismiss; answering false keeps the modal open. An
560
+ * action button does not ask it.
604
561
  *
605
562
  * ══ C29. WHY A DIALOG'S MAXIMISE IS THE GEOMETRIC ONE ═══════════════════
606
563
  *
@@ -642,6 +599,7 @@ export function openModal({
642
599
  backdropOpacity = undefined,
643
600
  maximizable = false,
644
601
  onMaximizeChange = null,
602
+ beforeClose = null,
645
603
  } = {}) {
646
604
  return new Promise((resolve) => {
647
605
  const body = document.createElement('div');
@@ -726,7 +684,7 @@ export function openModal({
726
684
  _onMax = null;
727
685
  };
728
686
  const win = new ManagedWindow({
729
- container: _modalHost,
687
+ container: modalHost(),
730
688
  id: `twm-modal-${_modalSeq++}`,
731
689
  title,
732
690
  icon,
@@ -747,6 +705,7 @@ export function openModal({
747
705
  canDrag: true,
748
706
  defaultWidth: width,
749
707
  defaultHeight: height,
708
+ beforeClose,
750
709
  // Esc, the X and the backdrop all land here without passing through
751
710
  // `close`, so the listener is dropped here as well as there — the
752
711
  // three dismissals a user reaches for most are exactly the ones
@@ -953,14 +912,68 @@ function _offered(options) {
953
912
  return flatChoices(options).filter((o) => !o.disabled);
954
913
  }
955
914
 
956
- function _datalistOptions(options) {
957
- return _offered(options).map((o) => `<option value="${_esc(o.value)}"></option>`).join('');
915
+ /**
916
+ * A `select` with `create`: `createCombobox` over the enabled options. The box holds
917
+ * the text; `read` is the value of the option that text names, else the text. A
918
+ * disabled option is not offered, and a default or `write` naming one leaves the box
919
+ * empty; typed all the same, it is `unavailable`.
920
+ */
921
+ function _mountCombobox(slot, f, value) {
922
+ let options = f.options;
923
+ const named = (text) => findChoice(flatChoices(options), text, true);
924
+ const shown = (v) => (v == null || named(v)?.disabled ? '' : String(v));
925
+
926
+ const hint = document.createElement('span');
927
+ hint.className = 'twm-combobox__hint';
928
+ hint.hidden = true;
929
+ // With `create: true` the typed value simply IS the value — there is no
930
+ // entity being made, so promising to "create" one is a lie that reads as
931
+ // a warning.
932
+ const noun = f.create === true ? null : _createSpec(f).hint || 'new entry';
933
+ const updateHint = () => {
934
+ const v = c.value.trim();
935
+ const isNew = noun && v && !flatChoices(options).some((o) => String(o.value) === v);
936
+ if (isNew) hint.textContent = `↳ will create ${noun} “${v}”`;
937
+ hint.hidden = !isNew;
938
+ };
939
+
940
+ const c = createCombobox({
941
+ options: _offered(options),
942
+ value: shown(value),
943
+ name: f.name,
944
+ placeholder: f.placeholder ?? 'type to pick or create…',
945
+ // Enter keeps what was typed: this field exists to accept a value nobody listed.
946
+ autoHighlight: false,
947
+ onChange: () => updateHint(),
948
+ });
949
+ if (f.required) c.input.required = true;
950
+ c.el.appendChild(hint);
951
+ slot.replaceWith(c.el);
952
+ updateHint();
953
+ return {
954
+ read() {
955
+ const o = named(c.value);
956
+ return o ? o.value : c.value;
957
+ },
958
+ write(v) { c.value = shown(v); updateHint(); },
959
+ focus: c.focus,
960
+ unavailable() {
961
+ const o = c.value === '' ? undefined : named(c.value);
962
+ return o?.disabled ? (o.reason || '') : null;
963
+ },
964
+ setOptions(next) {
965
+ options = next;
966
+ c.setOptions(_offered(next));
967
+ updateHint();
968
+ },
969
+ dispose: c.destroy,
970
+ };
958
971
  }
959
972
 
960
973
  /**
961
974
  * Mount a field that is a component rather than markup — a `toggle`, a
962
- * `select` (not a combobox), `checklist` or `radio` drawn by
963
- * `choice_controls.js`, or a `color` / `colorList` — into the
975
+ * `select`, `checklist` or `radio` drawn by `choice_controls.js`, a `select`
976
+ * with `create` drawn by `createCombobox`, or a `color` / `colorList` — into the
964
977
  * `data-control-slot` its row left, and hand back what the form needs of it:
965
978
  * `{ read(), write(v), focus(), unavailable(), submit?(), error?(),
966
979
  * setOptions?(options), dispose?() }`. `unavailable()` is the reason (or '')
@@ -1018,6 +1031,7 @@ function _mountControl(host, f, value, onChange) {
1018
1031
  dispose: () => c.dispose(),
1019
1032
  };
1020
1033
  }
1034
+ if (f.type === 'select' && f.create) return _mountCombobox(slot, f, value);
1021
1035
  const common = { keepMissing: false, loose: true };
1022
1036
  const disabledReason = (entry) => (entry?.disabled ? (entry.reason || '') : null);
1023
1037
  if (f.type === 'select') {
@@ -1113,7 +1127,7 @@ function _wirePicker(host, f) {
1113
1127
  ? String(picked.label) : _pickerLabel(f, hidden.value);
1114
1128
  // A hidden input fires no `change` of its own; this is the one the
1115
1129
  // field-error clear and `onFieldChange` listen for.
1116
- hidden.dispatchEvent(new Event('change', { bubbles: true }));
1130
+ fireEvent(hidden, 'change', { bubbles: true });
1117
1131
  };
1118
1132
  btn.addEventListener('click', open);
1119
1133
  // The read-only box is the obvious thing to click; make it work too.
@@ -1121,33 +1135,6 @@ function _wirePicker(host, f) {
1121
1135
  }
1122
1136
 
1123
1137
 
1124
- /** Show / hide the "↳ will create …" hint as the user types into a
1125
- * create-capable combobox. */
1126
- function _wireComboboxHint(host, f, optionsOf) {
1127
- const input = host.querySelector(`input[name="${f.name}"]`);
1128
- const hintEl = host.querySelector(`.twm-combobox__hint[data-for="${f.name}"]`);
1129
- if (!input || !hintEl) return;
1130
- // With `create: true` the typed value simply IS the value — there is no
1131
- // entity being made, so promising to "create" one is a lie that reads as
1132
- // a warning.
1133
- if (f.create === true) return;
1134
- const label = _createSpec(f).hint || 'new entry';
1135
- const update = () => {
1136
- const v = input.value.trim();
1137
- const known = new Set(flatChoices(optionsOf()).map((o) => String(o.value)));
1138
- if (v && !known.has(v)) {
1139
- hintEl.textContent = `↳ will create ${label} “${v}”`;
1140
- hintEl.hidden = false;
1141
- } else {
1142
- hintEl.hidden = true;
1143
- }
1144
- };
1145
- input.addEventListener('input', update);
1146
- update();
1147
- return update;
1148
- }
1149
-
1150
-
1151
1138
  function _renderEntry(f, defaults) {
1152
1139
  if (f && f.section != null) {
1153
1140
  // A named section is addressable by `setVisible`, so a heading can be
@@ -1247,23 +1234,6 @@ function _renderField(f, value) {
1247
1234
  </div>
1248
1235
  `);
1249
1236
  }
1250
- if (f.type === 'select' && f.create) {
1251
- // A default naming a disabled option is not taken, as in a select.
1252
- const shown = findChoice(flatChoices(f.options), value, true)?.disabled ? '' : v;
1253
- const dlId = `ea-dl-${f.name}-${_modalSeq}`;
1254
- const opts = _datalistOptions(f.options);
1255
- const placeholder = f.placeholder
1256
- ? `placeholder="${_esc(f.placeholder)}"` : 'placeholder="type to pick or create…"';
1257
- return wrap(`
1258
- <div class="twm-combobox">
1259
- <input name="${f.name}" type="text" list="${dlId}"
1260
- value="${_esc(shown)}" ${placeholder} ${required}
1261
- autocomplete="off">
1262
- <datalist id="${dlId}" data-datalist-for="${f.name}">${opts}</datalist>
1263
- <span class="twm-combobox__hint" data-for="${f.name}" hidden></span>
1264
- </div>
1265
- `);
1266
- }
1267
1237
  if (f.type === 'select') return wrap(slot);
1268
1238
  const step = f.step != null ? `step="${f.step}"` : '';
1269
1239
  const placeholder = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : '';
@@ -0,0 +1,160 @@
1
+ /**
2
+ * THE ROWS `createSelect`, `createCombobox` AND `createTagInput`'s SUGGESTIONS
3
+ * DRAW: a `role="listbox"` of options, each with a glyph, a label, a second line,
4
+ * a hint at the end of the row and, when disabled, its reason; `{ group, options }`
5
+ * sections under a heading; one active row that the arrow keys walk.
6
+ *
7
+ * Focus never comes here. The text box or trigger that holds it names the active
8
+ * row with `aria-activedescendant` (`onActive`), and a press on a row is
9
+ * prevented from taking focus, so that a list row can be chosen with the box
10
+ * still holding the caret, and no blur closes the list under the pointer.
11
+ *
12
+ * `aria-selected` says which row IS the current value; the active row is a class.
13
+ * They are different things in a select, where the highlight walks away from the
14
+ * value and comes back. Every word is built with `textContent`: these lists carry
15
+ * text users wrote.
16
+ *
17
+ * An internal part: not exported from `@flexdesk/widgets`.
18
+ */
19
+
20
+ let SEQ = 0;
21
+
22
+ function node(tag, className, text) {
23
+ const n = document.createElement(tag);
24
+ if (className) n.className = className;
25
+ if (text !== undefined && text !== null) n.textContent = String(text);
26
+ return n;
27
+ }
28
+
29
+ /** `text` with the first run of `needle` in a `<mark>`, so it is plain why a row is offered. */
30
+ function marked(target, text, needle) {
31
+ const at = needle ? text.toLowerCase().indexOf(needle.toLowerCase()) : -1;
32
+ if (at < 0) { target.textContent = text; return; }
33
+ target.append(text.slice(0, at), node('mark', 'twm-option-list__match', text.slice(at, at + needle.length)),
34
+ text.slice(at + needle.length));
35
+ }
36
+
37
+ /**
38
+ * @param {object} o
39
+ * @param {(option: object) => void} o.onPick A row was clicked. Never a disabled one.
40
+ * @param {(id: string|null) => void} [o.onActive] The active row's id changed.
41
+ */
42
+ export function createOptionList({ onPick, onActive = null }) {
43
+ const id = `twm-option-list-${++SEQ}`;
44
+ const el = node('div', 'twm-option-list');
45
+ el.id = id;
46
+ el.setAttribute('role', 'listbox');
47
+
48
+ /** Every row drawn: `{ option, row }`. */
49
+ let rows = [];
50
+ let active = -1;
51
+
52
+ const paintActive = () => {
53
+ rows.forEach(({ row }, i) => row.classList.toggle('twm-option-list__row--active', i === active));
54
+ const row = rows[active]?.row;
55
+ // jsdom does not implement it, and a test harness is no reason to drop it.
56
+ row?.scrollIntoView?.({ block: 'nearest' });
57
+ onActive?.(row ? row.id : null);
58
+ };
59
+
60
+ const rowFor = (option, { glyphs, mark, isCurrent }) => {
61
+ const row = node('div', 'twm-option-list__row');
62
+ row.id = `${id}-${rows.length}`;
63
+ row.setAttribute('role', 'option');
64
+ row.setAttribute('aria-selected', String(!!isCurrent?.(option)));
65
+ if (option.disabled) row.setAttribute('aria-disabled', 'true');
66
+ row.title = option.description ? `${option.label} — ${option.description}` : option.label;
67
+ if (glyphs) {
68
+ const glyph = node('span', 'twm-option-list__glyph material-symbols-outlined', option.icon || '');
69
+ glyph.setAttribute('aria-hidden', 'true');
70
+ row.appendChild(glyph);
71
+ }
72
+ const words = node('span', 'twm-option-list__words');
73
+ const label = node('span', 'twm-option-list__label');
74
+ marked(label, option.label, mark);
75
+ words.appendChild(label);
76
+ if (option.description) words.appendChild(node('span', 'twm-option-list__description', option.description));
77
+ if (option.disabled && option.reason) words.appendChild(node('span', 'twm-choice__reason', option.reason));
78
+ row.appendChild(words);
79
+ if (option.hint) row.appendChild(node('span', 'twm-option-list__hint', option.hint));
80
+ return row;
81
+ };
82
+
83
+ const enabled = () => rows.map((r, i) => (r.option.disabled ? -1 : i)).filter((i) => i >= 0);
84
+
85
+ // A press on the list leaves focus where it is. See the header.
86
+ el.addEventListener('mousedown', (ev) => ev.preventDefault());
87
+ el.addEventListener('click', (ev) => {
88
+ const row = ev.target.closest?.('[role="option"]');
89
+ const hit = rows.find((r) => r.row === row);
90
+ if (hit && !hit.option.disabled) onPick(hit.option);
91
+ });
92
+
93
+ return {
94
+ el,
95
+ id,
96
+ /**
97
+ * Draw `sections` (`[{ heading, items }]`, as `choiceSections` gives them).
98
+ * @param {object} [opts]
99
+ * @param {string} [opts.mark] The text to mark in each label.
100
+ * @param {(option: object) => boolean} [opts.isCurrent] Is this the current value?
101
+ */
102
+ setRows(sections, { mark = '', isCurrent = null } = {}) {
103
+ rows = [];
104
+ active = -1;
105
+ el.replaceChildren();
106
+ const glyphs = sections.some((s) => s.items.some((o) => o.icon));
107
+ for (const { heading, items } of sections) {
108
+ let parent = el;
109
+ if (heading != null) {
110
+ parent = node('div', 'twm-option-list__section');
111
+ parent.setAttribute('role', 'group');
112
+ parent.setAttribute('aria-label', heading);
113
+ const title = node('div', 'twm-choice__group', heading);
114
+ title.setAttribute('aria-hidden', 'true');
115
+ parent.appendChild(title);
116
+ el.appendChild(parent);
117
+ }
118
+ for (const option of items) {
119
+ const row = rowFor(option, { glyphs, mark, isCurrent });
120
+ rows.push({ option, row });
121
+ parent.appendChild(row);
122
+ }
123
+ }
124
+ paintActive();
125
+ },
126
+ /** One line of text in place of the rows: nothing matched, still loading, a failure. */
127
+ setMessage(text) {
128
+ rows = [];
129
+ active = -1;
130
+ el.replaceChildren(node('div', 'twm-option-list__message', text));
131
+ paintActive();
132
+ },
133
+ /** How many rows are drawn. */
134
+ get size() { return rows.length; },
135
+ /** The drawn options, in order. */
136
+ options: () => rows.map((r) => r.option),
137
+ /** The active row's option, or null. */
138
+ activeOption: () => rows[active]?.option ?? null,
139
+ /** Make row `index` active; -1, or a row that is disabled, makes none active. */
140
+ setActive(index) {
141
+ active = rows[index] && !rows[index].option.disabled ? index : -1;
142
+ paintActive();
143
+ },
144
+ /** The next row that may be chosen, by `step`, wrapping. From none: the first or the last. */
145
+ move(step) {
146
+ const stops = enabled();
147
+ if (!stops.length) return;
148
+ const at = stops.indexOf(active);
149
+ const next = at < 0 ? (step > 0 ? 0 : stops.length - 1) : (at + step + stops.length) % stops.length;
150
+ active = stops[next];
151
+ paintActive();
152
+ },
153
+ /** The first (`step` 1) or last (`step` -1) row that may be chosen. */
154
+ moveToEnd(step) {
155
+ const stops = enabled();
156
+ active = stops.length ? stops[step > 0 ? 0 : stops.length - 1] : -1;
157
+ paintActive();
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A POPOVER — a panel of rich content hung off the element that opened it. (0.18.0)
3
+ *
4
+ * `role="dialog"`, on an anchored layer (`anchored_layer.js`, which says how it is
5
+ * placed and what closes it). Focus moves into it on open and comes back to the
6
+ * anchor on close. Opening one from the anchor that holds an open one closes it,
7
+ * a toggle; opening one anywhere else closes the open one, unless that one owns
8
+ * the new anchor (`anchored_layer.js`).
9
+ *
10
+ * TAB STAYS INSIDE, cycling the panel's controls, as a modal's and a `CategoryPicker`'s
11
+ * do. The panel is mounted at the end of the page, so past its edge lies whatever comes
12
+ * next in the document, which is not the anchor's neighbour and, over a modal, is behind
13
+ * it. Escape is the way out.
14
+ */
15
+ import { openAnchoredLayer } from './anchored_layer.js';
16
+ import { collectFocusable } from '../utils/focusable.js';
17
+
18
+ /** The open popovers, to find the one an anchor holds. */
19
+ const OPEN = new Set();
20
+
21
+ /** What a call answers when nothing opened. */
22
+ const nothingOpened = (toggled) => ({ el: null, close() {}, closed: Promise.resolve(), reposition() {}, toggled });
23
+
24
+ /** Tab at the panel's edge goes round to the other edge. */
25
+ function keepTabInside(el) {
26
+ el.addEventListener('keydown', (ev) => {
27
+ if (ev.key !== 'Tab' || ev.defaultPrevented) return;
28
+ const controls = collectFocusable(el);
29
+ if (!controls.length) { ev.preventDefault(); return; }
30
+ const first = controls[0];
31
+ const last = controls[controls.length - 1];
32
+ const active = el.ownerDocument.activeElement;
33
+ if (ev.shiftKey ? (active === first || active === el) : active === last) {
34
+ ev.preventDefault();
35
+ (ev.shiftKey ? last : first).focus();
36
+ }
37
+ });
38
+ }
39
+
40
+ /**
41
+ * @param {object} o
42
+ * @param {HTMLElement} o.anchor What opened it; focus returns here.
43
+ * @param {HTMLElement} o.content The popover's body.
44
+ * @param {string} [o.label] Its accessible name.
45
+ * @param {string} [o.className] Extra classes on the panel.
46
+ * @param {HTMLElement} [o.focus] What takes focus on open; else the panel.
47
+ * @param {'start'|'end'} [o.align] Which anchor edge it lines up with first.
48
+ * @param {string} [o.inside] A selector for floating things of your own that
49
+ * are not "outside" it.
50
+ * @param {(reason: string) => void} [o.onClose] After it is gone.
51
+ * @returns {{el: HTMLElement|null, close: (reason?: string) => void, closed: Promise<void>,
52
+ * reposition: () => void, toggled: boolean}}
53
+ * `el` is null when nothing opened, and `onClose` is not called for it: `toggled`
54
+ * is true when the call only CLOSED the popover its anchor held, false when
55
+ * the anchor has left the document.
56
+ */
57
+ export function openPopover({ anchor, content, label = '', className = '', focus = null, align = 'start',
58
+ inside = null, onClose = null }) {
59
+ for (const open of OPEN) {
60
+ if (anchor && open.anchor === anchor) {
61
+ open.close('toggle');
62
+ return nothingOpened(true);
63
+ }
64
+ }
65
+
66
+ const el = document.createElement('div');
67
+ el.className = `twm-popover${className ? ` ${className}` : ''}`;
68
+ el.setAttribute('role', 'dialog');
69
+ if (label) el.setAttribute('aria-label', label);
70
+ el.tabIndex = -1;
71
+ el.appendChild(content);
72
+ keepTabInside(el);
73
+
74
+ let settle;
75
+ const closed = new Promise((resolve) => { settle = resolve; });
76
+ const layer = openAnchoredLayer({
77
+ anchor, el, align, gap: 4, inside,
78
+ onClose(reason) {
79
+ OPEN.delete(handle);
80
+ anchor?.classList?.remove('twm-is-open');
81
+ anchor?.setAttribute?.('aria-expanded', 'false');
82
+ onClose?.(reason);
83
+ settle();
84
+ },
85
+ });
86
+ if (!layer) return nothingOpened(false);
87
+ const handle = { el, anchor, closed, toggled: false, close: layer.close, reposition: layer.place };
88
+ OPEN.add(handle);
89
+ anchor?.classList?.add('twm-is-open');
90
+ if (anchor && !anchor.hasAttribute?.('aria-haspopup')) anchor.setAttribute?.('aria-haspopup', 'dialog');
91
+ anchor?.setAttribute?.('aria-expanded', 'true');
92
+ (focus || el).focus?.({ preventScroll: true });
93
+ return handle;
94
+ }
File without changes