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
@@ -12,5 +12,6 @@ export { createCanvasEditor, CANVAS_ACTIONS } from './editor.js';
12
12
  export { CANVAS_STRINGS } from './strings.js';
13
13
  export {
14
14
  CANVAS_NODE, layoutGraph as arrangeCanvasGraph, portAnchor as canvasPortAnchor, nodeBottom as canvasNodeBottom,
15
- isReturnEdge as isCanvasReturnEdge,
15
+ nodeWidth as canvasNodeWidth,
16
+ isReturnEdge as isCanvasReturnEdge, pathPoints as canvasPathPoints, pathMidpoint as canvasPathMidpoint,
16
17
  } from './layout.js';
@@ -6,10 +6,13 @@
6
6
  * ARITHMETIC over these, never by measuring, so the
7
7
  * stylesheet must agree with them — `flow_canvas_css` reads
8
8
  * the sheet and holds it to them.
9
- * portAnchor where a port's line starts or ends
10
- * nodeBottom where a node's drawn box ends
9
+ * nodeWidth a node's width: its type's own `size.w`, else the box's
10
+ * portAnchor where a port's line starts or ends (a folded node's: its header)
11
+ * nodeBottom where a node's drawn box ends (a folded node's: its header)
11
12
  * isReturnEdge is this line a loop's body going back round?
12
13
  * layoutGraph ARRANGE: every step's place, left to right in run order
14
+ * pathPoints a drawn line's path, as the points along it
15
+ * pathMidpoint the point halfway along a drawn line, where its label sits
13
16
  */
14
17
 
15
18
  import { arrange } from '../../canvas/edge_router.js';
@@ -22,26 +25,42 @@ const FOOT = 12;
22
25
  const inputsOf = (catalogue, node) => (catalogue?.inputs ? catalogue.inputs(node?.type) : []);
23
26
  const outputsOf = (catalogue, node) => (catalogue?.outputs ? catalogue.outputs(node?.type) : []);
24
27
  const roleOf = (catalogue, node) => (catalogue?.role ? catalogue.role(node?.type) : 'step');
28
+ const sizeOf = (catalogue, node) => (catalogue?.size ? catalogue.size(node?.type) : null);
29
+ /** Is `node` drawn folded to its header? The box's own `folded(node)` decides
30
+ * when it has one (the editor's, which folds only with `collapsible`); else
31
+ * the node's `collapsed: true`. */
32
+ const foldedOf = (node, box) => (typeof box?.folded === 'function' ? Boolean(box.folded(node))
33
+ : node?.collapsed === true);
34
+
35
+ /** `node`'s width: its type's own `size.w` when it has one, else the box's. */
36
+ export function nodeWidth(catalogue, node, box = CANVAS_NODE) {
37
+ return sizeOf(catalogue, node)?.w || box.width;
38
+ }
25
39
 
26
40
  /**
27
41
  * The point a line meets `node`'s port `portName`: an input on the node's
28
42
  * left edge, an output on its right, one row of ports down from the header
29
43
  * per port before it — in the order the type DECLARES its ports, the order
30
44
  * they are drawn in. A port the type does not have anchors at the first row.
45
+ * A node folded to its header has its ports folded away: every line meets
46
+ * the header's middle, on its side.
31
47
  */
32
48
  export function portAnchor(catalogue, node, portName, direction, box = CANVAS_NODE) {
49
+ const x = node.position.x + (direction === 'input' ? 0 : nodeWidth(catalogue, node, box));
50
+ const side = direction === 'input' ? 'left' : 'right';
51
+ if (foldedOf(node, box)) return { x, y: node.position.y + box.header / 2, side };
33
52
  const list = direction === 'input' ? inputsOf(catalogue, node) : outputsOf(catalogue, node);
34
53
  const i = Math.max(0, list.findIndex((p) => p.name === portName));
35
- return {
36
- x: node.position.x + (direction === 'input' ? 0 : box.width),
37
- y: node.position.y + box.header + box.row * i + box.row / 2,
38
- side: direction === 'input' ? 'left' : 'right',
39
- };
54
+ return { x, y: node.position.y + box.header + box.row * i + box.row / 2, side };
40
55
  }
41
56
 
42
57
  /** Where `node`'s drawn box ends — computed, because jsdom and a node not yet
43
- * laid out both answer 0 for `offsetHeight`. */
58
+ * laid out both answer 0 for `offsetHeight`. A type with its own `size.h` is
59
+ * drawn exactly that tall; a node folded to its header, as tall as that. */
44
60
  export function nodeBottom(catalogue, node, box = CANVAS_NODE) {
61
+ if (foldedOf(node, box)) return node.position.y + box.header;
62
+ const h = sizeOf(catalogue, node)?.h;
63
+ if (h) return node.position.y + h;
45
64
  const rows = Math.max(inputsOf(catalogue, node).length, outputsOf(catalogue, node).length, 1);
46
65
  return node.position.y + box.header + box.row * rows + FOOT;
47
66
  }
@@ -74,13 +93,17 @@ export function isReturnEdge(connection, target, catalogue, loopEntry = 'in') {
74
93
  * start step(s) that follows each step's outputs in the order its type
75
94
  * declares them — so a condition's first arm sits above its second
76
95
  * whatever the two steps are called.
96
+ * - **each node's own box.** A type's `size` (or `heightOf(node)`, a height
97
+ * the editor measured) is the node's height, a folded node's (`folded`, as
98
+ * for `portAnchor`) is its header's, and the widest node sets the column
99
+ * spacing; with neither, every node is the box's.
77
100
  * - **a loop's return lines left out.** The line from a loop's body back into
78
101
  * the loop is the loop, not a dependency; followed, it would pull the body
79
102
  * left of the loop that runs it. Any other cycle is broken by `arrange`.
80
103
  */
81
104
  export function layoutGraph(graph, catalogue, { width = CANVAS_NODE.width, header = CANVAS_NODE.header,
82
105
  row = CANVAS_NODE.row, gap = 96, rowGap = 40,
83
- loopEntry = 'in' } = {}) {
106
+ loopEntry = 'in', heightOf = null, folded = undefined } = {}) {
84
107
  const nodes = graph?.nodes || [];
85
108
  const connections = graph?.connections || [];
86
109
  const byId = new Map(nodes.map((n) => [n.id, n]));
@@ -109,10 +132,94 @@ export function layoutGraph(graph, catalogue, { width = CANVAS_NODE.width, heade
109
132
  for (const n of nodes) if (!order.has(n.id)) order.set(n.id, order.size);
110
133
 
111
134
  const rows = (n) => Math.max(inputsOf(catalogue, n).length, outputsOf(catalogue, n).length);
135
+ const box = { width, header, row, ...(folded !== undefined ? { folded } : {}) };
136
+ const height = (n) => {
137
+ if (foldedOf(n, box)) return header + 24;
138
+ const own = sizeOf(catalogue, n)?.h || (typeof heightOf === 'function' ? heightOf(n) : null);
139
+ return own ? own + 2 * FOOT : header + row * rows(n) + 24;
140
+ };
141
+ const widest = Math.max(width, ...nodes.map((n) => nodeWidth(catalogue, n, box)));
112
142
  return arrange(
113
- nodes.map((n) => ({ id: n.id, label: String(order.get(n.id)).padStart(6, '0'),
114
- height: header + row * rows(n) + 24 })),
143
+ nodes.map((n) => ({ id: n.id, label: String(order.get(n.id)).padStart(6, '0'), height: height(n) })),
115
144
  connections.filter((c) => !isReturn(c)).map((c) => ({ from: c.target, to: c.source })),
116
- { columnGap: width + gap, rowGap },
145
+ { columnGap: widest + gap, rowGap },
117
146
  );
118
147
  }
148
+
149
+ /** Points per curve segment of a sampled path: enough that a label's place
150
+ * and a crossing test agree with what the browser draws to within a pixel. */
151
+ const CURVE_STEPS = 24;
152
+
153
+ /**
154
+ * The points along an SVG path `d`, as the kernel writes them: absolute `M`,
155
+ * `L`, `C`, `S` and `Z`, numbers separated by spaces or commas. Each curve is
156
+ * sampled at `steps` points; a straight run is its two ends. Computed rather
157
+ * than asked of the browser (`getPointAtLength`), because jsdom has no
158
+ * geometry and a label must sit in the same place in both.
159
+ */
160
+ export function pathPoints(d, { steps = CURVE_STEPS } = {}) {
161
+ const tokens = String(d || '').match(/-?\d*\.?\d+(?:[eE][-+]?\d+)?|[A-Za-z]/g) || [];
162
+ const points = [];
163
+ let at = null;
164
+ let lastControl = null;
165
+ let opened = null;
166
+ let i = 0;
167
+ const num = () => Number(tokens[i++]);
168
+ const cubic = (c1, c2, end) => {
169
+ for (let k = 1; k <= steps; k += 1) {
170
+ const t = k / steps;
171
+ const u = 1 - t;
172
+ points.push({
173
+ x: u * u * u * at.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * end.x,
174
+ y: u * u * u * at.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * end.y,
175
+ });
176
+ }
177
+ lastControl = c2;
178
+ at = end;
179
+ };
180
+ while (i < tokens.length) {
181
+ const command = tokens[i++];
182
+ if (command === 'M') {
183
+ at = { x: num(), y: num() };
184
+ opened = at;
185
+ points.push(at);
186
+ lastControl = null;
187
+ } else if (command === 'Z') {
188
+ at = opened;
189
+ points.push(at);
190
+ lastControl = null;
191
+ } else if (command === 'L') {
192
+ at = { x: num(), y: num() };
193
+ points.push(at);
194
+ lastControl = null;
195
+ } else if (command === 'C') {
196
+ const c1 = { x: num(), y: num() };
197
+ const c2 = { x: num(), y: num() };
198
+ cubic(c1, c2, { x: num(), y: num() });
199
+ } else if (command === 'S') {
200
+ const c1 = lastControl ? { x: 2 * at.x - lastControl.x, y: 2 * at.y - lastControl.y } : at;
201
+ const c2 = { x: num(), y: num() };
202
+ cubic(c1, c2, { x: num(), y: num() });
203
+ } else {
204
+ throw new Error(`pathPoints reads M, L, C, S and Z only, not "${command}".`);
205
+ }
206
+ }
207
+ return points;
208
+ }
209
+
210
+ /** The point halfway along `d`, by length. */
211
+ export function pathMidpoint(d) {
212
+ const points = pathPoints(d);
213
+ if (points.length < 2) return points[0] ? { ...points[0] } : { x: 0, y: 0 };
214
+ const lengths = [0];
215
+ for (let k = 1; k < points.length; k += 1) {
216
+ lengths.push(lengths[k - 1] + Math.hypot(points[k].x - points[k - 1].x, points[k].y - points[k - 1].y));
217
+ }
218
+ const half = lengths[lengths.length - 1] / 2;
219
+ let k = 1;
220
+ while (k < lengths.length - 1 && lengths[k] < half) k += 1;
221
+ const span = lengths[k] - lengths[k - 1];
222
+ const t = span > 0 ? (half - lengths[k - 1]) / span : 0;
223
+ return { x: points[k - 1].x + (points[k].x - points[k - 1].x) * t,
224
+ y: points[k - 1].y + (points[k].y - points[k - 1].y) * t };
225
+ }
@@ -7,14 +7,28 @@
7
7
  * through `strings` and the `dataPortRead` option.
8
8
  */
9
9
 
10
+ /** `1 step`, `2 notes`, …; a count of none is left out of what is said. */
11
+ const counted = (steps, notes) => [[steps, 'step'], [notes, 'note']].filter(([n]) => n > 0)
12
+ .map(([n, w]) => `${n} ${w}${n === 1 ? '' : 's'}`).join(' and ');
13
+
10
14
  export const CANVAS_STRINGS = Object.freeze({
11
15
  palette: 'Steps',
12
16
  canvasLabel: 'Flow canvas',
13
17
  toolbarLabel: 'Flow',
14
18
  arrange: 'Arrange',
15
19
  arrangeTitle: 'Lay the steps out left to right, in the order a run takes them',
20
+ arrangeLayeredTitle: 'Lay the steps out left to right, following their connections',
16
21
  fit: 'Fit',
17
22
  fitTitle: 'Show the whole flow',
23
+ zoomOut: 'Zoom out',
24
+ zoomOutTitle: 'Make the flow smaller, one step',
25
+ zoomIn: 'Zoom in',
26
+ zoomInTitle: 'Make the flow larger, one step',
27
+ zoomReset: '100%',
28
+ zoomResetTitle: 'Show the flow at its natural size',
29
+ zoomAtMin: 'This is as small as the flow is drawn.',
30
+ zoomAtMax: 'This is as large as the flow is drawn.',
31
+ minimapTitle: 'Click to move the view there',
18
32
  nothingToUndo: 'There is nothing to undo.',
19
33
  nothingToRedo: 'There is nothing to redo.',
20
34
  arrangeNeedsTwo: 'Arrange needs two steps or more.',
@@ -28,10 +42,76 @@ export const CANVAS_STRINGS = Object.freeze({
28
42
  returnLine: 'Back to the loop for its next pass',
29
43
  connectedReturn: 'Goes round the loop again.',
30
44
  unknownType: (type) => `There is no step type "${type}".`,
45
+ renderFailed: (name, why) => `${name} could not draw its body${why ? `: ${why}` : '.'}`,
46
+ updateFailed: (name, why) => `${name} could not bring its body up to date${why ? `: ${why}` : '.'}`,
31
47
  stepId: (id) => `Step id: ${id}`,
48
+ // `renameInPlace`
49
+ nameEmpty: 'A step needs a name: the one it had is kept.',
50
+ // `collapsible`
51
+ collapseNode: 'Fold this step to its header',
52
+ expandNode: 'Show this step\'s ports again',
32
53
  connection: 'Connection',
33
54
  connectionLine: (e) => `${e.source} · ${e.sourcePort} → ${e.target} · ${e.targetPort}`,
34
55
  removeConnection: 'Remove connection',
35
56
  portCount: (n) => `×${n}`,
36
57
  portNotTaken: 'not taken',
58
+
59
+ // ── under the `connections` option ───────────────────────────────
60
+ connectTypes: ({ from, fromStep, to, toStep, accepts, provides }) => `${to} on ${toStep} takes `
61
+ + `${accepts.join(' or ')}, and ${from} on ${fromStep} gives ${provides.join(' or ')}.`,
62
+ connectRefused: 'These two cannot be connected.',
63
+ connectDrag: 'Drop it on an input to connect, or on the canvas to add a step there. Esc cancels.',
64
+ dropOnInput: 'Drop the line on an input to connect it.',
65
+ dropPickerTitle: 'Add a step and connect it',
66
+ dropNothingFits: 'No step has an input this output can connect to.',
67
+ offerAccept: 'Accept',
68
+ offerDismiss: 'Not now',
69
+
70
+ // `multiSelect`
71
+ alignLeft: 'Align left edges',
72
+ alignCenter: 'Align centres',
73
+ alignRight: 'Align right edges',
74
+ alignTop: 'Align top edges',
75
+ alignMiddle: 'Align middles',
76
+ alignBottom: 'Align bottom edges',
77
+ distributeHorizontal: 'Distribute horizontally',
78
+ distributeVertical: 'Distribute vertically',
79
+ alignNeedsTwo: 'Pick two steps or more to align them.',
80
+ distributeNeedsThree: 'Pick three steps or more to distribute them.',
81
+ stepsPicked: (n) => `${n} steps picked`,
82
+ stepsPickedLine: 'Drag one to move them all. The arrow keys nudge them.',
83
+ removeSteps: (n) => (n === 1 ? 'Remove 1 step' : `Remove ${n} steps`),
84
+ // `groups`
85
+ group: 'Group',
86
+ groupTitle: 'Group the picked steps (Ctrl+G)',
87
+ ungroup: 'Ungroup',
88
+ ungroupTitle: 'Ungroup the groups the picked steps are in (Ctrl+Shift+G)',
89
+ groupNeedsOne: 'Pick the steps to group first.',
90
+ ungroupNeedsGroup: 'None of the picked steps is in a group.',
91
+ groupUntitled: 'Group',
92
+ groupName: 'Group name',
93
+ // `notes`
94
+ note: 'Note',
95
+ noteTitle: 'Add a note to the canvas',
96
+ noteKind: 'Note',
97
+ noteText: 'Note text',
98
+ noteEmpty: 'Empty note — double-click to write in it',
99
+ removeNote: 'Remove note',
100
+ itemsPicked: (steps, notes) => `${counted(steps, notes)} picked`,
101
+ removeItems: (n) => (n === 1 ? 'Remove 1 item' : `Remove ${n} items`),
102
+ // `clipboard`
103
+ copyLabel: (title, n) => (n > 1 ? `${title} (copy ${n})` : `${title} (copy)`),
104
+ copied: (n) => (n === 1 ? 'Copied 1 step.' : `Copied ${n} steps.`),
105
+ cutDone: (n) => (n === 1 ? 'Cut 1 step.' : `Cut ${n} steps.`),
106
+ // a copy or a cut that took notes too (`notes`)
107
+ copiedItems: (steps, notes) => `Copied ${counted(steps, notes)}.`,
108
+ cutDoneItems: (steps, notes) => `Cut ${counted(steps, notes)}.`,
109
+ nothingPicked: 'Pick a step first.',
110
+ cannotCopy: 'None of the picked steps can be copied.',
111
+ cannotCut: 'None of the picked steps can be removed, so none can be cut.',
112
+ nothingToPaste: 'There is nothing to paste.',
113
+ pasteNotSteps: 'What is on the clipboard is not steps of a flow.',
114
+ notAddable: (name) => `‘${name}’ cannot be added to this flow.`,
115
+ clipboardFailed: 'The clipboard could not be reached.',
116
+ routingFallback: (n) => `More than ${n} steps: the lines are drawn as plain curves, not routed round the steps.`,
37
117
  });
File without changes
@@ -11,6 +11,29 @@
11
11
  *
12
12
  * Any other key a type carries is kept and ignored.
13
13
  *
14
+ * ══ A TYPE MAY DRAW ITS OWN BODY ═══════════════════════════════════════
15
+ *
16
+ * Four optional keys are read by the canvas editor, and only by it:
17
+ *
18
+ * render(body, node, ctx) fills the node's body, below its ports, once
19
+ * update(body, node, ctx) brings that body up to date on every repaint
20
+ * size: {w, h} the node's box in px (either may be left out)
21
+ * accent a tone (`dom.js`'s TONES) the node is marked in
22
+ *
23
+ * A type without them is drawn as every other step is. A `size` that is not
24
+ * positive numbers, an `accent` that is not a tone, a `render` or `update`
25
+ * that is not a function: each is a line in `problems`, and that key is not
26
+ * used.
27
+ *
28
+ * A fifth, `inline: ['key', …]`, names settings of the type's `config_schema`
29
+ * the canvas editor draws ON the node too, by the same widget registry the
30
+ * settings panel uses. A name the schema does not have, or an `inline` that is
31
+ * not a list of names, is a line in `problems`, and that name is not drawn.
32
+ *
33
+ * A sixth, `rank: 'source' | 'sink'`, is the hint the canvas editor's
34
+ * `layout: 'layered'` Arrange gives its steps: the first column or the last.
35
+ * Any other value is a line in `problems`, and it is not used.
36
+ *
14
37
  * ══ DECLARED PORT ORDER IS MEANING ═════════════════════════════════════
15
38
  *
16
39
  * A lane reads an operation's first FLOW input as its own lane and the rest as
@@ -33,7 +56,7 @@
33
56
  * consumer's catalogue with one bad entry still opens an editor.
34
57
  */
35
58
 
36
- import { checkSettingsSchema } from './settings_schema.js';
59
+ import { checkSettingsSchema, fieldsFromSchema } from './settings_schema.js';
37
60
  import { TONES } from './dom.js';
38
61
 
39
62
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
@@ -47,6 +70,41 @@ function cleanStem(stem) {
47
70
  return String(stem ?? '').replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 40) || 'step';
48
71
  }
49
72
 
73
+ const positive = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : null);
74
+
75
+ /** A type's `size` as `{w, h}` (either null), or null — with the problem, if any. */
76
+ function readSize(raw) {
77
+ if (raw === undefined || raw === null) return { size: null, problem: null };
78
+ const refused = { size: null, problem: 'size must be {w, h} in positive pixels; it is not used.' };
79
+ if (!isPlainObject(raw)) return refused;
80
+ const w = raw.w === undefined ? null : positive(raw.w);
81
+ const h = raw.h === undefined ? null : positive(raw.h);
82
+ if ((raw.w !== undefined && w === null) || (raw.h !== undefined && h === null) || (w === null && h === null)) {
83
+ return refused;
84
+ }
85
+ return { size: Object.freeze({ w, h }), problem: null };
86
+ }
87
+
88
+ /** A type's `inline` names, those its schema has, in the order given — each
89
+ * one it does not have a line in `problems`. */
90
+ function readInline(raw, schema, id, problems) {
91
+ if (raw === undefined || raw === null) return Object.freeze([]);
92
+ if (!Array.isArray(raw)) {
93
+ problems.push({ type_id: id, message: `"${id}"'s inline must be a list of setting names; it is not used.` });
94
+ return Object.freeze([]);
95
+ }
96
+ const known = new Set(fieldsFromSchema(schema).map((f) => f.key));
97
+ const keys = [];
98
+ for (const key of raw) {
99
+ if (typeof key === 'string' && known.has(key)) {
100
+ if (!keys.includes(key)) keys.push(key);
101
+ } else {
102
+ problems.push({ type_id: id, message: `"${id}"'s inline names "${String(key)}", which its settings do not have; it is not drawn.` });
103
+ }
104
+ }
105
+ return Object.freeze(keys);
106
+ }
107
+
50
108
  function normalisePort(p) {
51
109
  return {
52
110
  ...p,
@@ -69,6 +127,10 @@ export function createStepCatalogue(types, { categories = [], idBase = defaultId
69
127
  const problems = [];
70
128
  const byId = new Map();
71
129
  const order = [];
130
+ const sizes = new Map();
131
+ const accents = new Map();
132
+ const inlines = new Map();
133
+ const ranks = new Map();
72
134
 
73
135
  for (const raw of Array.isArray(types) ? types : []) {
74
136
  if (!isPlainObject(raw)) {
@@ -108,6 +170,24 @@ export function createStepCatalogue(types, { categories = [], idBase = defaultId
108
170
  });
109
171
  byId.set(id, type);
110
172
  order.push(type);
173
+ const { size, problem } = readSize(raw.size);
174
+ if (problem) problems.push({ type_id: id, message: `"${id}"'s ${problem}` });
175
+ if (size) sizes.set(id, size);
176
+ if (raw.accent !== undefined && raw.accent !== null) {
177
+ if (TONES.includes(raw.accent)) accents.set(id, raw.accent);
178
+ else problems.push({ type_id: id, message: `"${id}"'s accent "${raw.accent}" is not a tone; it is not used.` });
179
+ }
180
+ const inline = readInline(raw.inline, schema, id, problems);
181
+ if (inline.length) inlines.set(id, inline);
182
+ if (raw.rank !== undefined && raw.rank !== null) {
183
+ if (raw.rank === 'source' || raw.rank === 'sink') ranks.set(id, raw.rank);
184
+ else problems.push({ type_id: id, message: `"${id}"'s rank "${String(raw.rank)}" is not 'source' or 'sink'; it is not used.` });
185
+ }
186
+ for (const hook of ['render', 'update']) {
187
+ if (raw[hook] !== undefined && typeof raw[hook] !== 'function') {
188
+ problems.push({ type_id: id, message: `"${id}"'s ${hook} is not a function; it is not used.` });
189
+ }
190
+ }
111
191
  }
112
192
 
113
193
  const cats = (Array.isArray(categories) ? categories : []).filter(isPlainObject).map((c) => Object.freeze({
@@ -129,11 +209,11 @@ export function createStepCatalogue(types, { categories = [], idBase = defaultId
129
209
  extra.push(Object.freeze({ id: '', label: '', tone: 'grey', layout: 'list' }));
130
210
  }
131
211
  const allCats = Object.freeze([...cats, ...extra]);
132
- const rank = (t) => {
212
+ const categoryIndex = (t) => {
133
213
  const id = t.category === null ? '' : t.category;
134
214
  return allCats.findIndex((c) => c.id === id);
135
215
  };
136
- const sorted = [...order].sort((a, b) => (rank(a) - rank(b))
216
+ const sorted = [...order].sort((a, b) => (categoryIndex(a) - categoryIndex(b))
137
217
  || String(a.label).localeCompare(String(b.label)));
138
218
 
139
219
  const portsOf = (typeId, direction, flow) => {
@@ -160,6 +240,20 @@ export function createStepCatalogue(types, { categories = [], idBase = defaultId
160
240
  const t = byId.get(typeId);
161
241
  return (t && allCats.find((c) => c.id === (t.category ?? ''))?.tone) || 'grey';
162
242
  },
243
+ /** The type's own box, `{w, h}` (either null), or null when it has none. */
244
+ size: (typeId) => sizes.get(typeId) ?? null,
245
+ /** The type's accent tone, or null when it has none. */
246
+ accent: (typeId) => accents.get(typeId) ?? null,
247
+ /** The setting names the type draws on its node (`inline`), in order; `[]` when none. */
248
+ inline: (typeId) => inlines.get(typeId) ?? Object.freeze([]),
249
+ /** The type's layered-layout hint, 'source' or 'sink', or null when it has none. */
250
+ rank: (typeId) => ranks.get(typeId) ?? null,
251
+ /** The type's `{render, update}` (`update` may be null), or null when it has no `render`. */
252
+ body: (typeId) => {
253
+ const t = byId.get(typeId);
254
+ const render = typeof t?.render === 'function' ? t.render : null;
255
+ return render ? { render, update: typeof t.update === 'function' ? t.update : null } : null;
256
+ },
163
257
  inputs: (typeId, { flow = false } = {}) => portsOf(typeId, 'input', flow),
164
258
  outputs: (typeId, { flow = false } = {}) => portsOf(typeId, 'output', flow),
165
259
  port: (typeId, name, direction) => byId.get(typeId)?.ports
File without changes
File without changes
File without changes
File without changes
@@ -20,9 +20,9 @@ export {
20
20
  } from './references.js';
21
21
  export { createChipInput } from './chip_input.js';
22
22
  export {
23
- createWidgetRegistry, GENERIC_WIDGETS, DEFAULT_WIDGET_REFERENCES, renderField, shownValue, typedValue,
24
- fitToText,
23
+ createWidgetRegistry, GENERIC_WIDGETS, DEFAULT_WIDGET_REFERENCES, renderField, typedValue, fitToText,
25
24
  } from './widgets.js';
25
+ export { shownValue } from '../../ui/components/choice_controls.js';
26
26
  export { createSettingsPanel } from './settings_panel.js';
27
27
  export { openStepPicker } from './step_picker.js';
28
28
  export { openValuePicker } from './value_picker.js';
@@ -34,5 +34,5 @@ export { FlowHistory } from './history.js';
34
34
  export { bindFlowKeys, ownsUndo, isTextField, isOwnControl } from './keys.js';
35
35
  export {
36
36
  ID_OK, LOGIC_GRAPH_STRINGS, emptyGraph, normalise, serialise, nextId, addNode, removeNode, moveNode,
37
- canConnect, connect, disconnect, sameEdge, inputNamesOf,
37
+ canConnect, connect, addConnection, disconnect, sameEdge, inputNamesOf,
38
38
  } from './logic_graph.js';
@@ -4,6 +4,8 @@
4
4
  * Ctrl/⌘+Z · Ctrl/⌘+Y, Ctrl/⌘+Shift+Z undo · redo
5
5
  * Delete remove the selection
6
6
  * Ctrl/⌘+D · C · X · V duplicate · copy · cut · paste
7
+ * Ctrl/⌘+A pick everything
8
+ * Ctrl/⌘+G · Ctrl/⌘+Shift+G group · ungroup the selection
7
9
  * F2 rename
8
10
  * Alt+↑ · Alt+↓ · Alt+← move up · down · out of its block
9
11
  * ↑ ↓ ← → move the selection
@@ -66,7 +68,8 @@ export function isTextField(target) {
66
68
  *
67
69
  * @param {HTMLElement} root
68
70
  * @param {object} handlers undo, redo, remove, duplicate, copy, cut, paste,
69
- * rename, moveUp, moveDown, moveOut, up, down, left, right, open, escape
71
+ * selectAll, group, ungroup, rename, moveUp, moveDown, moveOut, up, down, left, right,
72
+ * open, escape
70
73
  * @returns {() => void} unbind
71
74
  */
72
75
  export function bindFlowKeys(root, handlers = {}) {
@@ -100,8 +103,11 @@ export function bindFlowKeys(root, handlers = {}) {
100
103
  if (lower === 'c') { run('copy', ev); return; }
101
104
  if (lower === 'x') { run('cut', ev); return; }
102
105
  if (lower === 'v') { run('paste', ev); return; }
106
+ if (lower === 'a') { run('selectAll', ev); return; }
107
+ if (lower === 'g') { run('group', ev); return; }
103
108
  return;
104
109
  }
110
+ if (mod && lower === 'g') { run('ungroup', ev); return; }
105
111
  if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
106
112
  if (k === 'ArrowUp') run('moveUp', ev);
107
113
  else if (k === 'ArrowDown') run('moveDown', ev);