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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,27 +1,16 @@
1
1
  /**
2
- * THE KIT'S SMALL DOM HELPERS, AND ITS ONE POPOVER.
2
+ * THE KIT'S SMALL DOM HELPERS, AND ITS POPOVER.
3
3
  *
4
4
  * Everything here builds elements; nothing here reads the network, storage or
5
- * a consumer's vocabulary. The popover is the step picker's and *Insert a
6
- * value*'s shell (36 §3.5, §3.7):
7
- *
8
- * - it is appended to FlexDesk's modal host (`modalHost()`, C25) or to
9
- * `document.body` — the same question a modal and a context menu ask, so a
10
- * consumer spanning two browser windows gets it in the window it set;
11
- * - it is `position: fixed`, placed under its anchor (above when there is no
12
- * room below) and CLAMPED to the viewport;
13
- * - it is dismissed by a press outside it, by Escape, or by `close()`, and
14
- * **it returns focus to its anchor** when it closes with focus inside it —
15
- * a picker that drops focus on `<body>` drops it where an editor's keys
16
- * never arrive — except when focus has just left it by Tab, which the
17
- * pickers close it for (`'blur'`): that focus is going where it was sent;
18
- * - there is ONE at a time: opening a second closes the first, and opening one
19
- * from the anchor that holds the open one closes it (a toggle);
20
- * - `closeFlowPopovers()` closes whichever is open, which is what an editor's
21
- * `destroy()` calls so that nothing it opened outlives it.
5
+ * a consumer's vocabulary. The popover, the step picker's and *Insert a value*'s
6
+ * shell (36 §3.5, §3.7), is FlexDesk's `openPopover`, which says how it is placed,
7
+ * dismissed and toggled and that it returns focus to its anchor: a picker that
8
+ * drops focus on `<body>` drops it where an editor's keys never arrive. What the
9
+ * kit adds is `closeFlowPopovers()`, which closes whichever of its own is open:
10
+ * what an editor's `destroy()` calls so that nothing it opened outlives it.
22
11
  */
23
12
 
24
- import { modalHost } from '../../ui/components/modal.js';
13
+ import { openPopover as openAnchoredPopover } from '../../ui/components/popover.js';
25
14
 
26
15
  /** The category tones (36 §3.1, §3.12); the stylesheet maps each to tokens. */
27
16
  export const TONES = Object.freeze(['violet', 'teal', 'amber', 'grey', 'blue', 'indigo']);
@@ -81,118 +70,38 @@ export function uid(stem = 'flow') {
81
70
 
82
71
  // ── the popover ──────────────────────────────────────────────────────────
83
72
 
84
- const EDGE = 8;
85
- const GAP = 4;
86
- let ACTIVE = null;
73
+ /** The kit's open popovers, oldest first. */
74
+ const OPEN = [];
87
75
 
88
- /** Close whichever kit popover is open. */
76
+ /** Close every kit popover that is open. */
89
77
  export function closeFlowPopovers() {
90
- ACTIVE?.close('closed');
78
+ OPEN[0]?.close('closed');
91
79
  }
92
80
 
93
- /** The open popover's element, or null — for a test. */
81
+ /** The open popover's element (the innermost's), or null — for a test. */
94
82
  export function openFlowPopover() {
95
- return ACTIVE ? ACTIVE.el : null;
83
+ return OPEN.length ? OPEN[OPEN.length - 1].el : null;
96
84
  }
97
85
 
98
86
  /**
99
- * Put `content` in a popover under `anchor`.
87
+ * FlexDesk's popover (`openPopover` in `ui/components/popover.js`), wearing the
88
+ * kit's class and remembered so that an editor can close what it opened.
100
89
  *
101
- * @param {object} o
102
- * @param {HTMLElement} o.anchor what opened it; focus returns here
103
- * @param {HTMLElement} o.content the popover's body
104
- * @param {string} o.label its accessible name (`role="dialog"`)
105
- * @param {string} [o.className] an extra block class
106
- * @param {(reason: string) => void} [o.onClose] after it is gone
107
- * @param {HTMLElement} [o.focus] what takes focus on open
108
- * @returns {{el: HTMLElement, close: (reason?: string) => void, reposition: () => void, toggled: boolean}}
109
- * `toggled` is true when the call only CLOSED the popover its anchor held.
90
+ * @param {object} o What `openPopover` takes.
91
+ * @returns {{el: HTMLElement|null, close: (reason?: string) => void, reposition: () => void, toggled: boolean}}
92
+ * `el` is null when nothing opened; `toggled` is true when the call only
93
+ * CLOSED the popover its anchor held.
110
94
  */
111
- export function openPopover({ anchor, content, label, className = '', onClose = null, focus = null }) {
112
- if (ACTIVE && anchor && ACTIVE.anchor === anchor) {
113
- ACTIVE.close('toggle');
114
- return { el: null, close() {}, reposition() {}, toggled: true };
115
- }
116
- ACTIVE?.close('replaced');
117
-
118
- const pop = el('div', `twm-flow-popover${className ? ` ${className}` : ''}`);
119
- pop.setAttribute('role', 'dialog');
120
- pop.setAttribute('aria-label', label || '');
121
- pop.tabIndex = -1;
122
- pop.appendChild(content);
123
- (modalHost() || document.body).appendChild(pop);
124
-
125
- let closed = false;
126
- const place = () => {
127
- if (closed) return;
128
- // The CLIENT width: a page scrollbar is not room to draw in.
129
- const view = { w: document.documentElement.clientWidth || window.innerWidth || 1024,
130
- h: document.documentElement.clientHeight || window.innerHeight || 768 };
131
- const a = anchor?.getBoundingClientRect?.() || { left: 0, top: 0, bottom: 0, right: 0 };
132
- const box = pop.getBoundingClientRect();
133
- // Under the anchor, its left edge on the anchor's; an anchor near the
134
- // right edge gets the popover's RIGHT edge on its own instead.
135
- let left = a.left;
136
- if (left + box.width > view.w - EDGE && a.right - box.width >= EDGE) left = a.right - box.width;
137
- let top = a.bottom + GAP;
138
- if (top + box.height > view.h - EDGE && a.top - GAP - box.height >= EDGE) top = a.top - GAP - box.height;
139
- left = Math.max(EDGE, Math.min(left, view.w - box.width - EDGE));
140
- top = Math.max(EDGE, Math.min(top, view.h - box.height - EDGE));
141
- pop.style.left = `${Math.round(left)}px`;
142
- pop.style.top = `${Math.round(top)}px`;
143
- };
144
-
145
- const onPress = (ev) => {
146
- if (pop.contains(ev.target)) return;
147
- if (anchor && anchor.contains?.(ev.target)) return; // the anchor's own click toggles
148
- handle.close('outside');
149
- };
150
- const onKey = (ev) => {
151
- if (ev.key !== 'Escape') return;
152
- ev.preventDefault();
153
- ev.stopPropagation();
154
- handle.close('escape');
155
- };
156
- const onResize = () => place();
157
- // Placed before its content is drawn — a picker fills its list after this
158
- // returns — so it is placed again whenever its size changes, or it would
159
- // run past the window's edge.
160
- const sized = typeof ResizeObserver === 'function' ? new ResizeObserver(() => place()) : null;
161
-
162
- const handle = {
163
- el: pop,
164
- anchor,
165
- toggled: false,
166
- reposition: place,
167
- close(reason = 'closed') {
168
- if (closed) return;
169
- closed = true;
170
- const held = pop.contains(document.activeElement) || document.activeElement === document.body;
171
- document.removeEventListener('pointerdown', onPress, true);
172
- document.removeEventListener('mousedown', onPress, true);
173
- pop.removeEventListener('keydown', onKey);
174
- window.removeEventListener('resize', onResize);
175
- sized?.disconnect();
176
- pop.remove();
177
- if (ACTIVE === handle) ACTIVE = null;
178
- // Focus that LEFT the popover (a Tab out of it) is going somewhere the
179
- // reader sent it; taking it back to the anchor would fight that move.
180
- if (held && reason !== 'blur' && anchor?.isConnected) anchor.focus?.({ preventScroll: true });
95
+ export function openPopover({ className = '', onClose = null, ...rest }) {
96
+ const handle = openAnchoredPopover({
97
+ ...rest,
98
+ className: `twm-flow-popover${className ? ` ${className}` : ''}`,
99
+ onClose(reason) {
100
+ OPEN.splice(OPEN.indexOf(handle), 1);
181
101
  onClose?.(reason);
182
102
  },
183
- };
184
- ACTIVE = handle;
185
- // The press that OPENED it is still being dispatched; listen from the next turn.
186
- setTimeout(() => {
187
- if (closed) return;
188
- document.addEventListener('pointerdown', onPress, true);
189
- document.addEventListener('mousedown', onPress, true);
190
- }, 0);
191
- pop.addEventListener('keydown', onKey);
192
- window.addEventListener('resize', onResize);
193
- sized?.observe(pop);
194
- place();
195
- (focus || pop).focus?.({ preventScroll: true });
103
+ });
104
+ if (handle.el) OPEN.push(handle);
196
105
  return handle;
197
106
  }
198
107
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -202,7 +202,7 @@ export function openStepPicker({ anchor, entries = [], categories = [], where =
202
202
  anchor, content: root, label: title ?? say(S, 'addStep'), className: 'twm-flow-popover--picker',
203
203
  focus: search, onClose: () => settle(result),
204
204
  });
205
- if (handle.toggled) settle(null);
205
+ if (!handle.el) settle(null);
206
206
  else filter();
207
207
  done.close = () => handle.close('closed');
208
208
  return done;
File without changes
@@ -231,7 +231,7 @@ export function openValuePicker({ anchor, groups = [], note = null, onPick = nul
231
231
  anchor, content: root, label: title ?? say(S, 'insertValue'), className: 'twm-flow-popover--values',
232
232
  focus: search, onClose: () => settle(result),
233
233
  });
234
- if (handle.toggled) settle(null);
234
+ if (!handle.el) settle(null);
235
235
  else paint();
236
236
  done.close = () => handle.close('closed');
237
237
  return done;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -150,5 +150,5 @@ export function openStepMenu({ anchor, label, items, strings = null, onClose = n
150
150
  anchor, content: root, label: label || '', className: 'twm-flow-popover--menu',
151
151
  focus: enabled(root)[0] || root, onClose: () => onClose?.(),
152
152
  });
153
- return { el: root, close: () => handle.close('closed'), toggled: handle.toggled };
153
+ return { el: handle.el ? root : null, close: () => handle.close('closed'), toggled: handle.toggled };
154
154
  }
File without changes
File without changes
File without changes
@@ -0,0 +1,212 @@
1
+ /**
2
+ * diff_viewer.js — what changed, drawn three ways. Usable alone: it needs no
3
+ * adapter and knows nothing of git beyond reading a unified diff.
4
+ *
5
+ * FIELDS named values, `Priority low → high`. The consumer parses them;
6
+ * they arrive as `{ name, from, to }`
7
+ * TEXT line by line, with old and new line numbers and no diff headers,
8
+ * from `before`/`after` (compared here) or else from `diff` (read)
9
+ * RAW the `diff` text as it came
10
+ *
11
+ * const viewer = createDiffViewer(el, { before, after, diff, fields });
12
+ * viewer.update({ before, after });
13
+ * viewer.setView('raw');
14
+ * viewer.destroy();
15
+ *
16
+ * A view is offered only when its data is there, and the buttons that switch
17
+ * between views only when there are two or more. Which view shows: the one set
18
+ * by `view`, `setView` or a click while it stays on offer, else Fields when a
19
+ * field changed, else Text, else Raw. `setView(null)` hands the choice back.
20
+ */
21
+
22
+ import { computeLineDiff, parseUnifiedDiff } from './line_diff.js';
23
+
24
+ const DEFAULT_LABELS = Object.freeze({
25
+ fields: 'Fields',
26
+ text: 'Text',
27
+ raw: 'Raw',
28
+ noChanges: 'No differences.',
29
+ noFields: 'No field changes.',
30
+ tooLong: 'Too long to compare here.',
31
+ gap: (hidden) => `${hidden} unchanged line${hidden === 1 ? '' : 's'}`,
32
+ });
33
+
34
+ function el(tag, className, text) {
35
+ const n = document.createElement(tag);
36
+ if (className) n.className = className;
37
+ if (text !== undefined) n.textContent = text;
38
+ return n;
39
+ }
40
+
41
+ /** A field's value as drawn: an absent or empty one reads as a dash. */
42
+ const shownField = (v) => {
43
+ const s = v === null || v === undefined ? '' : String(v);
44
+ return s === '' ? '—' : s;
45
+ };
46
+
47
+ function lineRow(line, labels) {
48
+ const row = el('div', `twm-diff__line twm-diff__line--${line.kind}`);
49
+ if (line.kind === 'gap' || line.kind === 'hunk' || line.kind === 'meta' || line.kind === 'file') {
50
+ row.textContent = line.kind === 'gap' ? labels.gap(line.hidden) : line.text;
51
+ return row;
52
+ }
53
+ row.append(
54
+ el('span', 'twm-diff__n', line.oldLine === null ? '' : String(line.oldLine)),
55
+ el('span', 'twm-diff__n', line.newLine === null ? '' : String(line.newLine)),
56
+ el('span', 'twm-diff__sign', line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ''),
57
+ el('span', 'twm-diff__text', line.text),
58
+ );
59
+ return row;
60
+ }
61
+
62
+ /**
63
+ * @param {HTMLElement} host
64
+ * @param {Object} [options]
65
+ * @param {string} [options.before] - The old text. With `after`, drawn by comparing the two.
66
+ * @param {string} [options.after] - The new text.
67
+ * @param {string} [options.diff] - A unified diff: the Raw view, and the Text view when there is no `before`/`after`.
68
+ * @param {Array<{name: string, from?: *, to?: *}>} [options.fields] - The Fields view's rows.
69
+ * @param {number} [options.beforeStart=1] - The line number of `before`'s first line.
70
+ * @param {number} [options.afterStart=1] - The line number of `after`'s first line.
71
+ * @param {number} [options.context=3] - Unchanged lines kept around each change.
72
+ * @param {number} [options.maxLines] - Past this many differing lines a side, the texts are not compared.
73
+ * @param {'fields'|'text'|'raw'} [options.view] - The view to show while it is on offer.
74
+ * @param {Object} [options.labels] - Words to replace: `fields`, `text`, `raw`, `noChanges`, `noFields`, `tooLong`, `gap(hidden)`.
75
+ * @param {(view: string) => void} [options.onViewChange] - Told when a click changes the view.
76
+ * @returns {{ el: HTMLElement, update(data: Object): void, setView(view: string|null): void,
77
+ * view(): string|null, showMessage(text: string): void, destroy(): void }}
78
+ */
79
+ export function createDiffViewer(host, options = {}) {
80
+ const { context = 3, maxLines, onViewChange = null } = options;
81
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
82
+
83
+ const root = el('div', 'twm-diff');
84
+ const switcher = el('div', 'twm-diff__views');
85
+ switcher.setAttribute('role', 'group');
86
+ switcher.setAttribute('aria-label', 'Diff view');
87
+ const body = el('div', 'twm-diff__body');
88
+ root.append(switcher, body);
89
+ host.appendChild(root);
90
+
91
+ let data = {};
92
+ let chosen = options.view ?? null;
93
+ let textLines; // this data's Text view; undefined until first drawn
94
+ let current = null;
95
+
96
+ const hasPair = () => typeof data.before === 'string' || typeof data.after === 'string';
97
+
98
+ const linesOfText = () => {
99
+ if (textLines !== undefined) return textLines;
100
+ textLines = hasPair()
101
+ ? computeLineDiff(data.before, data.after, {
102
+ context,
103
+ ...(maxLines === undefined ? {} : { maxLines }),
104
+ beforeStart: data.beforeStart ?? 1,
105
+ afterStart: data.afterStart ?? 1,
106
+ })
107
+ : parseUnifiedDiff(data.diff);
108
+ return textLines;
109
+ };
110
+
111
+ const offered = () => {
112
+ const out = [];
113
+ if (Array.isArray(data.fields)) out.push('fields');
114
+ if (hasPair() || (typeof data.diff === 'string' && linesOfText().length > 0)) out.push('text');
115
+ if (typeof data.diff === 'string') out.push('raw');
116
+ return out;
117
+ };
118
+
119
+ const pick = (views) => {
120
+ if (chosen && views.includes(chosen)) return chosen;
121
+ if (views.includes('fields') && data.fields.length > 0) return 'fields';
122
+ if (views.includes('text')) return 'text';
123
+ return views[0] ?? null;
124
+ };
125
+
126
+ const message = (text) => {
127
+ body.replaceChildren(el('div', 'twm-diff__message', text));
128
+ };
129
+
130
+ const drawFields = () => {
131
+ if (data.fields.length === 0) { message(labels.noFields); return; }
132
+ const grid = el('div', 'twm-diff__fields');
133
+ for (const field of data.fields) {
134
+ const values = el('span', 'twm-diff__field-values');
135
+ const arrow = el('span', 'material-symbols-outlined twm-diff__field-arrow', 'arrow_forward');
136
+ arrow.setAttribute('aria-hidden', 'true');
137
+ values.append(
138
+ el('span', 'twm-diff__field-from', shownField(field.from)),
139
+ arrow,
140
+ el('span', 'twm-diff__field-to', shownField(field.to)),
141
+ );
142
+ grid.append(el('span', 'twm-diff__field-name', String(field.name)), values);
143
+ }
144
+ body.replaceChildren(grid);
145
+ };
146
+
147
+ const drawText = () => {
148
+ const lines = linesOfText();
149
+ if (lines === null) {
150
+ message(typeof data.diff === 'string' ? `${labels.tooLong} See ${labels.raw}.` : labels.tooLong);
151
+ return;
152
+ }
153
+ if (lines.length === 0) { message(labels.noChanges); return; }
154
+ const list = el('div', 'twm-diff__lines');
155
+ list.append(...lines.map((line) => lineRow(line, labels)));
156
+ body.replaceChildren(list);
157
+ };
158
+
159
+ const drawRaw = () => {
160
+ if (data.diff === '') { message(labels.noChanges); return; }
161
+ body.replaceChildren(el('pre', 'twm-diff__raw', data.diff));
162
+ };
163
+
164
+ const draw = () => {
165
+ const views = offered();
166
+ current = pick(views);
167
+ switcher.replaceChildren(...(views.length > 1 ? views.map((v) => {
168
+ const b = el('button', 'twm-diff__view', labels[v]);
169
+ b.type = 'button';
170
+ b.setAttribute('aria-pressed', String(v === current));
171
+ b.addEventListener('click', () => {
172
+ chosen = v;
173
+ draw();
174
+ onViewChange?.(v);
175
+ });
176
+ return b;
177
+ }) : []));
178
+ switcher.hidden = views.length <= 1;
179
+ if (current === 'fields') drawFields();
180
+ else if (current === 'text') drawText();
181
+ else if (current === 'raw') drawRaw();
182
+ else message(labels.noChanges);
183
+ };
184
+
185
+ const update = (next) => {
186
+ data = next ?? {};
187
+ textLines = undefined;
188
+ draw();
189
+ };
190
+
191
+ update(options);
192
+
193
+ return {
194
+ el: root,
195
+ update,
196
+ setView(view) {
197
+ chosen = view;
198
+ draw();
199
+ },
200
+ view: () => current,
201
+ /** Words in place of the diff, while it loads or when it could not be read. The next update replaces them. */
202
+ showMessage(text) {
203
+ switcher.replaceChildren();
204
+ switcher.hidden = true;
205
+ current = null;
206
+ message(text);
207
+ },
208
+ destroy() {
209
+ root.remove();
210
+ },
211
+ };
212
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * diff_window.js — a diff in a window of its own: FlexDesk's modal around a
3
+ * createDiffViewer, showing "Loading…" until the data arrives.
4
+ */
5
+
6
+ import { openModal } from '../ui/components/modal.js';
7
+ import { createDiffViewer } from './diff_viewer.js';
8
+
9
+ /**
10
+ * @param {Object} options
11
+ * @param {string} options.title
12
+ * @param {() => Promise<Object>|Object} options.load - Answers what `createDiffViewer`'s `update` takes, or
13
+ * `{ ok: false, error }`; a throw is shown the same way.
14
+ * @param {string} [options.icon='difference']
15
+ * @param {number} [options.width=880]
16
+ * @param {number} [options.height=600]
17
+ * @param {Object} [options.labels] - As for createDiffViewer.
18
+ * @returns {Promise<null>} Settles when the window closes.
19
+ */
20
+ export function openDiffWindow({ title, load, icon = 'difference', width = 880, height = 600, labels } = {}) {
21
+ const content = document.createElement('div');
22
+ content.className = 'twm-diff-window';
23
+ const viewer = createDiffViewer(content, { labels });
24
+ viewer.showMessage('Loading…');
25
+ const closed = openModal({ title, icon, content, width, height, maximizable: true });
26
+ Promise.resolve()
27
+ .then(load)
28
+ .then((data) => {
29
+ if (data?.ok === false) viewer.showMessage(data.error || 'The diff could not be read.');
30
+ else viewer.update(data);
31
+ })
32
+ .catch((e) => viewer.showMessage(e?.message || String(e)));
33
+ return closed;
34
+ }
35
+
36
+ /**
37
+ * The diff of one file, from an adapter's `diffFile`, in a window.
38
+ * @param {Object} options
39
+ * @param {Object} options.adapter - A git adapter with `diffFile` (docs/components/git.md).
40
+ * @param {string} options.path
41
+ * @param {string} [options.status]
42
+ * @param {string} [options.renamedFrom] - The path it had before, for a rename.
43
+ * @param {string} [options.ref] - A commit: the file's change in that commit, not the uncommitted one.
44
+ * @returns {Promise<null>}
45
+ */
46
+ export function openGitDiffWindow({ adapter, path, status, renamedFrom, ref }) {
47
+ return openDiffWindow({
48
+ title: renamedFrom ? `${renamedFrom} → ${path}` : path,
49
+ load: () => adapter.diffFile({ path, status, ...(renamedFrom ? { renamedFrom } : {}), ...(ref ? { ref } : {}) }),
50
+ });
51
+ }