flexdesk 0.17.7 → 0.17.9

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 (284) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +0 -0
  3. package/README.md +0 -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/base.css +21 -1
  9. package/css/flexdesk.css +30 -3
  10. package/css/overrides.css +0 -0
  11. package/css/reset.css +0 -0
  12. package/dist/canvas.js +0 -0
  13. package/dist/canvas.js.map +0 -0
  14. package/dist/charts.js +7 -6
  15. package/dist/charts.js.map +2 -2
  16. package/dist/{chunk-RMTRWWEO.js → chunk-2YTM3GJE.js} +67 -26
  17. package/dist/chunk-2YTM3GJE.js.map +7 -0
  18. package/dist/chunk-3PHCPZHT.js +0 -0
  19. package/dist/chunk-3PHCPZHT.js.map +0 -0
  20. package/dist/{chunk-UURZ37SD.js → chunk-7U4IB65H.js} +27 -9
  21. package/dist/chunk-7U4IB65H.js.map +7 -0
  22. package/dist/{chunk-D3COLZZ2.js → chunk-BE2PU3LL.js} +2 -2
  23. package/dist/{chunk-D3COLZZ2.js.map → chunk-BE2PU3LL.js.map} +0 -0
  24. package/dist/chunk-CVMCZZX2.js +0 -0
  25. package/dist/chunk-CVMCZZX2.js.map +0 -0
  26. package/dist/{chunk-UWO43BZU.js → chunk-F35XSQWA.js} +16 -20
  27. package/dist/chunk-F35XSQWA.js.map +7 -0
  28. package/dist/chunk-FOOS3T5L.js +0 -0
  29. package/dist/chunk-FOOS3T5L.js.map +0 -0
  30. package/dist/chunk-GOQLR5WR.js +0 -0
  31. package/dist/chunk-GOQLR5WR.js.map +0 -0
  32. package/dist/chunk-HNU6MM2B.js +0 -0
  33. package/dist/chunk-HNU6MM2B.js.map +0 -0
  34. package/dist/chunk-JKZLNKG6.js +0 -0
  35. package/dist/chunk-JKZLNKG6.js.map +0 -0
  36. package/dist/chunk-JYWURG5T.js +0 -0
  37. package/dist/chunk-JYWURG5T.js.map +0 -0
  38. package/dist/{chunk-RKNFBX5K.js → chunk-MIW7D6WC.js} +16 -4
  39. package/dist/{chunk-RKNFBX5K.js.map → chunk-MIW7D6WC.js.map} +2 -2
  40. package/dist/chunk-N7HC3ADV.js +0 -0
  41. package/dist/chunk-N7HC3ADV.js.map +0 -0
  42. package/dist/{chunk-LWV23YVE.js → chunk-NIJEJAZ7.js} +42 -18
  43. package/dist/chunk-NIJEJAZ7.js.map +7 -0
  44. package/dist/{chunk-HYJYYEPK.js → chunk-REJ5HLIM.js} +2 -2
  45. package/dist/{chunk-HYJYYEPK.js.map → chunk-REJ5HLIM.js.map} +0 -0
  46. package/dist/{chunk-WVFGV5FT.js → chunk-WUFLGETN.js} +39 -2
  47. package/dist/{chunk-WVFGV5FT.js.map → chunk-WUFLGETN.js.map} +3 -3
  48. package/dist/core.js +0 -0
  49. package/dist/core.js.map +0 -0
  50. package/dist/editor.js +2 -2
  51. package/dist/editor.js.map +0 -0
  52. package/dist/flexdesk.css +30 -3
  53. package/dist/flow.js +3 -3
  54. package/dist/flow.js.map +0 -0
  55. package/dist/host.js +0 -0
  56. package/dist/host.js.map +0 -0
  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 +63 -20
  61. package/dist/tiles.js.map +2 -2
  62. package/dist/widgets.js +6 -6
  63. package/dist/widgets.js.map +0 -0
  64. package/dist/wm.js +5 -5
  65. package/dist/wm.js.map +0 -0
  66. package/docs/README.md +0 -0
  67. package/docs/best-practices.md +0 -0
  68. package/docs/components/charts.md +95 -0
  69. package/docs/components/command-palette.md +0 -0
  70. package/docs/components/conversation.md +0 -0
  71. package/docs/components/data-table-grid.md +0 -0
  72. package/docs/components/data-table.md +0 -0
  73. package/docs/components/editor.md +0 -0
  74. package/docs/components/flow-canvas-editor.md +0 -0
  75. package/docs/components/flow-editors.md +0 -0
  76. package/docs/components/flow-lane-editor.md +0 -0
  77. package/docs/components/flow-outline-editor.md +0 -0
  78. package/docs/components/forms-and-dialogs.md +0 -0
  79. package/docs/components/input-fields.md +0 -0
  80. package/docs/components/keymap.md +0 -0
  81. package/docs/components/markdown-and-outline.md +0 -0
  82. package/docs/components/menus.md +41 -0
  83. package/docs/components/notifications.md +0 -0
  84. package/docs/components/panels-and-desktops.md +0 -0
  85. package/docs/components/record-list-and-fact-table.md +0 -0
  86. package/docs/components/settings-view.md +0 -0
  87. package/docs/components/shell.md +0 -0
  88. package/docs/components/slide-out-panel.md +0 -0
  89. package/docs/components/tiles.md +25 -0
  90. package/docs/components/wizard.md +0 -0
  91. package/docs/concepts/building-and-packaging.md +0 -0
  92. package/docs/concepts/domain-neutrality.md +0 -0
  93. package/docs/concepts/host-port.md +0 -0
  94. package/docs/concepts/packages-and-entry-points.md +0 -0
  95. package/docs/concepts/peer-dependencies.md +0 -0
  96. package/docs/concepts/state-guard.md +0 -0
  97. package/docs/concepts/styling.md +0 -0
  98. package/docs/quickstart.md +0 -0
  99. package/editor.js +0 -0
  100. package/flow.js +0 -0
  101. package/host.js +0 -0
  102. package/package.json +1 -1
  103. package/src/canvas/canvas_adapter.js +0 -0
  104. package/src/canvas/edge_router.js +0 -0
  105. package/src/canvas/graph/connector_router.js +0 -0
  106. package/src/canvas/graph/history_service.js +0 -0
  107. package/src/canvas/graph/node_platform.js +0 -0
  108. package/src/canvas/index.js +0 -0
  109. package/src/canvas/layered_layout.js +0 -0
  110. package/src/charts/chart_types.js +3 -2
  111. package/src/charts/data_series_plot_window.js +0 -0
  112. package/src/charts/downsample.js +0 -0
  113. package/src/charts/plot_config.js +0 -0
  114. package/src/charts/plot_config_editor.js +8 -2
  115. package/src/charts/plot_grid.js +16 -4
  116. package/src/charts/plot_popout_window.js +2 -1
  117. package/src/charts/plotly_wrapper.js +0 -0
  118. package/src/charts/series_transforms.js +114 -20
  119. package/src/core/event_bus.js +0 -0
  120. package/src/core/logging.js +0 -0
  121. package/src/core/settings.js +0 -0
  122. package/src/core/state_guard.js +0 -0
  123. package/src/core/state_guard_config.js +0 -0
  124. package/src/core/state_machine.js +0 -0
  125. package/src/editor/editor_pane.js +0 -0
  126. package/src/editor/monaco_editor_factory.js +0 -0
  127. package/src/editor/monaco_loader.js +0 -0
  128. package/src/editor/notebook_search.js +0 -0
  129. package/src/editor/notebook_tab_bar.js +26 -7
  130. package/src/editor/notebook_undo_manager.js +0 -0
  131. package/src/editor/split_pane_container.js +0 -0
  132. package/src/flow/canvas_editor/connections.js +0 -0
  133. package/src/flow/canvas_editor/editor.js +0 -0
  134. package/src/flow/canvas_editor/index.js +0 -0
  135. package/src/flow/canvas_editor/layout.js +0 -0
  136. package/src/flow/canvas_editor/strings.js +0 -0
  137. package/src/flow/kit/always_before.js +0 -0
  138. package/src/flow/kit/catalogue.js +0 -0
  139. package/src/flow/kit/chip_input.js +0 -0
  140. package/src/flow/kit/dom.js +0 -0
  141. package/src/flow/kit/findings.js +0 -0
  142. package/src/flow/kit/history.js +0 -0
  143. package/src/flow/kit/index.js +0 -0
  144. package/src/flow/kit/keys.js +0 -0
  145. package/src/flow/kit/logic_graph.js +0 -0
  146. package/src/flow/kit/references.js +0 -0
  147. package/src/flow/kit/settings_panel.js +0 -0
  148. package/src/flow/kit/settings_schema.js +0 -0
  149. package/src/flow/kit/step_picker.js +0 -0
  150. package/src/flow/kit/strings.js +0 -0
  151. package/src/flow/kit/value_picker.js +0 -0
  152. package/src/flow/kit/widgets.js +0 -0
  153. package/src/flow/lanes/editor.js +0 -0
  154. package/src/flow/lanes/index.js +0 -0
  155. package/src/flow/lanes/layout.js +0 -0
  156. package/src/flow/lanes/pipeline.js +0 -0
  157. package/src/flow/lanes/preview.js +0 -0
  158. package/src/flow/lanes/strings.js +0 -0
  159. package/src/flow/outline/editor.js +0 -0
  160. package/src/flow/outline/index.js +0 -0
  161. package/src/flow/outline/mapping.js +0 -0
  162. package/src/flow/outline/menu.js +0 -0
  163. package/src/flow/outline/operations.js +0 -0
  164. package/src/flow/outline/recognise.js +0 -0
  165. package/src/flow/outline/strings.js +0 -0
  166. package/src/help/help_modal.js +0 -0
  167. package/src/help/help_registry.js +0 -0
  168. package/src/host/external_links.js +0 -0
  169. package/src/host/file_picker.js +0 -0
  170. package/src/host/host.js +0 -0
  171. package/src/host/photino_host.js +0 -0
  172. package/src/host/photino_host.md +0 -0
  173. package/src/host/pywebview_host.js +0 -0
  174. package/src/tiles/chart_tiles.js +0 -0
  175. package/src/tiles/config_schema.js +0 -0
  176. package/src/tiles/figure_tile.js +0 -0
  177. package/src/tiles/layout_persistence.js +0 -0
  178. package/src/tiles/list_tiles.js +0 -0
  179. package/src/tiles/scope_picker.js +0 -0
  180. package/src/tiles/tile_base.js +67 -16
  181. package/src/tiles/tile_config_form.js +0 -0
  182. package/src/tiles/tile_grid.js +0 -0
  183. package/src/tiles/tile_registry.js +0 -0
  184. package/src/tiling/bottom_panel_tabs.js +0 -0
  185. package/src/tiling/command_palette.js +0 -0
  186. package/src/tiling/content_registry.js +0 -0
  187. package/src/tiling/desktops.js +0 -0
  188. package/src/tiling/entity_sources.js +0 -0
  189. package/src/tiling/keymap.js +0 -0
  190. package/src/tiling/kind_taxonomy.js +0 -0
  191. package/src/tiling/landing_table.js +0 -0
  192. package/src/tiling/loading_overlay.js +0 -0
  193. package/src/tiling/nav_panel.js +0 -0
  194. package/src/tiling/page_factory.js +0 -0
  195. package/src/tiling/panel_availability.js +0 -0
  196. package/src/tiling/panel_keys.js +0 -0
  197. package/src/tiling/shell.js +0 -0
  198. package/src/tiling/tab_strip.js +0 -0
  199. package/src/tiling/tile_breadcrumb.js +0 -0
  200. package/src/tiling/tile_renderer.js +0 -0
  201. package/src/tiling/tile_tab_menu.js +0 -0
  202. package/src/tiling/tile_tree.js +0 -0
  203. package/src/tiling/wm.js +0 -0
  204. package/src/tiling/zoom.js +0 -0
  205. package/src/ui/base/component_base.js +0 -0
  206. package/src/ui/base/controller_base.js +0 -0
  207. package/src/ui/base/page_base.js +0 -0
  208. package/src/ui/components/about_dialog.js +0 -0
  209. package/src/ui/components/action_dropdown.js +19 -6
  210. package/src/ui/components/alert_log.js +0 -0
  211. package/src/ui/components/alerts_pane.js +0 -0
  212. package/src/ui/components/attribute_list_editor.js +0 -0
  213. package/src/ui/components/autocomplete_field.js +0 -0
  214. package/src/ui/components/cell_editor_host.js +0 -0
  215. package/src/ui/components/cell_editors.js +0 -0
  216. package/src/ui/components/cell_grid.js +0 -0
  217. package/src/ui/components/cell_paste.js +0 -0
  218. package/src/ui/components/cell_selection.js +0 -0
  219. package/src/ui/components/choice_controls.js +0 -0
  220. package/src/ui/components/color_field.js +0 -0
  221. package/src/ui/components/color_list_field.js +0 -0
  222. package/src/ui/components/column_fit.js +0 -0
  223. package/src/ui/components/column_groups.js +0 -0
  224. package/src/ui/components/column_order.js +0 -0
  225. package/src/ui/components/computing_status_window.js +0 -0
  226. package/src/ui/components/confirm_dialog.js +0 -0
  227. package/src/ui/components/context_menu.js +20 -20
  228. package/src/ui/components/conversation_view.js +0 -0
  229. package/src/ui/components/crosstab.js +0 -0
  230. package/src/ui/components/data_table.js +45 -19
  231. package/src/ui/components/detail_header.js +0 -0
  232. package/src/ui/components/document_outline.js +0 -0
  233. package/src/ui/components/drag_reorder.js +0 -0
  234. package/src/ui/components/fact_table.js +0 -0
  235. package/src/ui/components/fill_handle.js +0 -0
  236. package/src/ui/components/gallery_picker.js +0 -0
  237. package/src/ui/components/header_actions.js +0 -0
  238. package/src/ui/components/inline_renamer.js +0 -0
  239. package/src/ui/components/job_progress.js +0 -0
  240. package/src/ui/components/managed_window.js +0 -0
  241. package/src/ui/components/menu_bar.js +0 -0
  242. package/src/ui/components/modal.js +0 -0
  243. package/src/ui/components/notification_history.js +0 -0
  244. package/src/ui/components/progress.js +0 -0
  245. package/src/ui/components/record_list.js +0 -0
  246. package/src/ui/components/row_activation.js +0 -0
  247. package/src/ui/components/section_stack.js +0 -0
  248. package/src/ui/components/series.js +0 -0
  249. package/src/ui/components/settings_view.js +0 -0
  250. package/src/ui/components/slide_out_panel.js +0 -0
  251. package/src/ui/components/slider_field.js +0 -0
  252. package/src/ui/components/sortable_list.js +0 -0
  253. package/src/ui/components/staged_edits.js +0 -0
  254. package/src/ui/components/staged_history.js +0 -0
  255. package/src/ui/components/table_filter_chips.js +0 -0
  256. package/src/ui/components/table_state_store.js +0 -0
  257. package/src/ui/components/table_structure.js +0 -0
  258. package/src/ui/components/table_values.js +0 -0
  259. package/src/ui/components/tabs.js +0 -0
  260. package/src/ui/components/tag_input.js +0 -0
  261. package/src/ui/components/toast.js +0 -0
  262. package/src/ui/components/toggle_field.js +0 -0
  263. package/src/ui/components/tree_view.js +0 -0
  264. package/src/ui/components/tsv.js +0 -0
  265. package/src/ui/components/window_taskbar.js +0 -0
  266. package/src/ui/components/wizard.js +0 -0
  267. package/src/ui/controllers/window_chrome_controller.js +0 -0
  268. package/src/ui/notification_center.js +0 -0
  269. package/src/ui/utils/auto_scrollbars.js +0 -0
  270. package/src/ui/utils/markdown.js +0 -0
  271. package/src/ui/utils/menu_viewport.js +81 -0
  272. package/src/ui/utils/native_context_menu.js +0 -0
  273. package/src/ui/utils/overlay_scrollbar.js +0 -0
  274. package/src/ui/utils/raf_resize_observer.js +0 -0
  275. package/src/ui/utils/scroll_lock.js +0 -0
  276. package/src/ui/utils/strip_scroll.js +0 -0
  277. package/src/ui/utils/tooltip_service.js +0 -0
  278. package/tiles.js +0 -0
  279. package/widgets.js +0 -0
  280. package/wm.js +0 -0
  281. package/dist/chunk-LWV23YVE.js.map +0 -7
  282. package/dist/chunk-RMTRWWEO.js.map +0 -7
  283. package/dist/chunk-UURZ37SD.js.map +0 -7
  284. package/dist/chunk-UWO43BZU.js.map +0 -7
@@ -15,11 +15,44 @@
15
15
  * },
16
16
  * };
17
17
  *
18
- * `run(y, params)` resolves `{ [output]: number[] }`. It is called ONCE per
19
- * eligible trace (a 2-D line/scatter trace) for each transform with at least one
20
- * output switched on, and every switched-on output it returns is drawn as an
21
- * extra trace in the source trace's colour, named `<source name><suffix>`, with
22
- * the output's `lineStyle` (solid | dashed | dotted) and `lineWidth`.
18
+ * `run(y, params, context)` resolves `{ [output]: number[] }`. It is called
19
+ * ONCE per eligible trace (a 2-D line/scatter trace) for each transform with at
20
+ * least one output switched on, and every switched-on output it returns is
21
+ * drawn as an extra trace in the source trace's colour, named
22
+ * `<source name><suffix>`, with the output's `lineStyle` (solid | dashed |
23
+ * dotted) and `lineWidth`.
24
+ *
25
+ * `y` is the drawn values (after the subplot's NaN handling and whatever the
26
+ * embedder's `resolveSeries` did, such as a rebase). `context` (0.17.8) says
27
+ * which series they are:
28
+ *
29
+ * { series, // the subplot's series entry the trace was drawn from
30
+ * // (its variable, scenario, ...); null when unknown
31
+ * subplot, // the subplot config
32
+ * results, // the results the figure was drawn from, or null
33
+ * resolveSeries } // the `(series, results, subplot) => { y, x? }` the
34
+ * // figure was drawn with, or null
35
+ *
36
+ * so a transform can look the same series up elsewhere (a baseline scenario)
37
+ * and resolve it the same way. renderPlotGrid (and so PlotPopoutWindow) runs
38
+ * transforms on the same traces as before 0.17.8 (overlays included, derived
39
+ * traces never); a trace drawn from one of the subplot's series gets that
40
+ * series, any other (an overlay) gets `series: null`. A transform written as
41
+ * `run(y, params)` ignores the third argument and works as before.
42
+ *
43
+ * A parameter is a number field by default:
44
+ *
45
+ * window: { label: 'Window', default: 5, min: 1, step: 1 }
46
+ *
47
+ * or, declared `type: 'choice'` (0.17.8), a select over `options`: an array of
48
+ * `{ value, label }`, or a function `(subplot) => [{ value, label }]` called
49
+ * when the controls are drawn (and when the parameter needs its default):
50
+ *
51
+ * baseline: { label: 'Compare with', type: 'choice',
52
+ * options: () => scenarios.map((s) => ({ value: s.id, label: s.name })) }
53
+ *
54
+ * The chosen value is saved and passed to `run` like a number. Without a
55
+ * `default` and with nothing saved, a choice is its first enabled option.
23
56
  *
24
57
  * What is switched on lives in the subplot, under `subplot.transforms`:
25
58
  *
@@ -30,14 +63,38 @@
30
63
  */
31
64
 
32
65
  import { buildTrace } from './chart_types.js';
66
+ import { flatChoices, selectBox } from '../ui/components/choice_controls.js';
67
+
68
+ /** Whether a parameter spec is a choice (a select) rather than a number. */
69
+ const isChoice = (spec) => spec?.type === 'choice';
70
+
71
+ /** A choice parameter's options for `subplot`: its array, or what its function returns. */
72
+ function choiceOptions(spec, subplot) {
73
+ const options = typeof spec.options === 'function' ? spec.options(subplot) : spec.options;
74
+ return Array.isArray(options) ? options : [];
75
+ }
76
+
77
+ /**
78
+ * A choice parameter's value when none is declared or saved: the option
79
+ * selectBox selects for it, its first enabled one, `{ group, options }`
80
+ * entries flattened in order (the first of all when every one is disabled).
81
+ */
82
+ function firstChoice(spec, subplot) {
83
+ const flat = flatChoices(choiceOptions(spec, subplot));
84
+ return (flat.find((o) => !o.disabled) ?? flat[0])?.value;
85
+ }
33
86
 
34
87
  /** A transform's parameter values: its declared defaults under the subplot's own. */
35
- function resolveParams(transform, state) {
88
+ function resolveParams(transform, state, subplot) {
89
+ const saved = state?.params ?? {};
36
90
  const params = {};
37
91
  for (const [name, spec] of Object.entries(transform.params ?? {})) {
38
92
  params[name] = spec?.default;
93
+ if (isChoice(spec) && spec.default === undefined && saved[name] === undefined) {
94
+ params[name] = firstChoice(spec, subplot);
95
+ }
39
96
  }
40
- return { ...params, ...(state?.params ?? {}) };
97
+ return { ...params, ...saved };
41
98
  }
42
99
 
43
100
  /** The output names of `transform` that `state` switches on, in declared order. */
@@ -60,23 +117,34 @@ function isEligible(trace) {
60
117
  * @param {Object} [options]
61
118
  * @param {Function} [options.onError] - `({ transformId, traceName, error })`,
62
119
  * called for each skipped run.
120
+ * @param {Function} [options.seriesOf] - `(trace) => series | null`: the series
121
+ * config a trace was drawn from, passed as `context.series` (null for a
122
+ * trace it names none for, and for every trace without it). It does not
123
+ * choose the sources: every eligible trace is run on, as before.
124
+ * @param {Object} [options.results] - The results the figure was drawn from
125
+ * (`context.results`).
126
+ * @param {Function} [options.resolveSeries] - The resolver the figure was drawn
127
+ * with (`context.resolveSeries`).
63
128
  * @returns {Promise<Object[]>} The extra traces, in transform then trace order.
64
129
  */
65
- async function deriveSeriesTraces(traces, subplot, seriesTransforms, { onError } = {}) {
130
+ async function deriveSeriesTraces(traces, subplot, seriesTransforms,
131
+ { onError, seriesOf, results, resolveSeries } = {}) {
66
132
  const derived = [];
67
133
  if (!seriesTransforms) return derived;
68
- const sources = traces.filter(isEligible);
134
+ const sources = traces.filter(isEligible)
135
+ .map((trace) => ({ trace, series: typeof seriesOf === 'function' ? (seriesOf(trace) ?? null) : null }));
69
136
 
70
137
  for (const [transformId, transform] of Object.entries(seriesTransforms)) {
71
138
  const state = subplot?.transforms?.[transformId];
72
139
  const outputs = enabledOutputs(transform, state);
73
140
  if (!outputs.length || typeof transform?.run !== 'function') continue;
74
- const params = resolveParams(transform, state);
141
+ const params = resolveParams(transform, state, subplot);
75
142
 
76
- for (const source of sources) {
143
+ for (const { trace: source, series } of sources) {
144
+ const context = { series, subplot, results: results ?? null, resolveSeries: resolveSeries ?? null };
77
145
  let result;
78
146
  try {
79
- result = await transform.run(source.y, params);
147
+ result = await transform.run(source.y, params, context);
80
148
  } catch (error) {
81
149
  onError?.({ transformId, traceName: source.name, error });
82
150
  continue;
@@ -112,7 +180,8 @@ async function deriveSeriesTraces(traces, subplot, seriesTransforms, { onError }
112
180
 
113
181
  /**
114
182
  * The controls for `seriesTransforms` in a subplot's config: per transform, a
115
- * toggle for each output and a number field for each parameter. They edit
183
+ * toggle for each output and, for each parameter, a number field or, for a
184
+ * `type: 'choice'` parameter, a select (choice_controls' selectBox). They edit
116
185
  * `subplot.transforms` in place and call `onChange()` after every edit. With no
117
186
  * transforms (null or `{}`) nothing is rendered.
118
187
  *
@@ -173,22 +242,47 @@ function renderSeriesTransformControls(container, subplot, seriesTransforms, { o
173
242
  row.className = 'twm-series-transforms__row';
174
243
  const text = document.createElement('span');
175
244
  text.textContent = spec?.label ?? name;
245
+ const writeParam = (value) => {
246
+ const state = stateOf(transformId);
247
+ const params = { ...(state.params ?? {}) };
248
+ if (value === undefined) delete params[name];
249
+ else params[name] = value;
250
+ state.params = params;
251
+ onChange?.();
252
+ };
253
+ const saved = subplot.transforms?.[transformId]?.params?.[name];
254
+
255
+ if (isChoice(spec)) {
256
+ const current = saved ?? spec.default ?? firstChoice(spec, subplot);
257
+ const choice = selectBox(choiceOptions(spec, subplot), current, {
258
+ label: spec?.label ?? name,
259
+ valueAttr: 'value',
260
+ className: 'twm-series-transforms__param twm-series-transforms__select',
261
+ });
262
+ choice.box.addEventListener('change', () => {
263
+ const value = choice.valueOf();
264
+ // Redrawn with the chosen value, as settings do: a stale
265
+ // '(not found)' entry goes once an offered option is picked.
266
+ choice.setOptions(choiceOptions(spec, subplot), value);
267
+ writeParam(value);
268
+ });
269
+ paramInputs.push(choice.box);
270
+ row.append(text, choice.box);
271
+ group.appendChild(row);
272
+ continue;
273
+ }
274
+
176
275
  const input = document.createElement('input');
177
276
  input.type = 'number';
178
277
  input.className = 'twm-series-transforms__param';
179
278
  for (const attr of ['min', 'max', 'step']) {
180
279
  if (spec?.[attr] != null) input[attr] = String(spec[attr]);
181
280
  }
182
- const current = subplot.transforms?.[transformId]?.params?.[name] ?? spec?.default;
281
+ const current = saved ?? spec?.default;
183
282
  input.value = current == null ? '' : String(current);
184
283
  input.addEventListener('change', () => {
185
284
  const value = parseFloat(input.value);
186
- const state = stateOf(transformId);
187
- const params = { ...(state.params ?? {}) };
188
- if (Number.isFinite(value)) params[name] = value;
189
- else delete params[name];
190
- state.params = params;
191
- onChange?.();
285
+ writeParam(Number.isFinite(value) ? value : undefined);
192
286
  });
193
287
  paramInputs.push(input);
194
288
  row.append(text, input);
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
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { DragReorder } from '../ui/components/drag_reorder.js';
26
26
  import { revealInStrip, wheelStripSideways } from '../ui/utils/strip_scroll.js';
27
+ import { placeMenuInViewport, moveMenuFocus, focusMenuItem } from '../ui/utils/menu_viewport.js';
27
28
 
28
29
  /** The MIME type a dragged editor tab carries when the embedder names none. */
29
30
  export const DEFAULT_TAB_DRAG_MIME = 'application/x-flexdesk-tab';
@@ -64,6 +65,8 @@ export class NotebookTabBar {
64
65
 
65
66
  /** @type {HTMLElement|null} */
66
67
  #contextMenuEl = null;
68
+ /** BUG-0040. What had focus when the menu opened. @type {Element|null} */
69
+ #contextMenuReturnFocus = null;
67
70
 
68
71
  /** @type {Function|null} */
69
72
  #boundHideContextMenu = null;
@@ -417,6 +420,7 @@ export class NotebookTabBar {
417
420
 
418
421
  const menu = document.createElement('div');
419
422
  menu.className = 'nb-context-menu';
423
+ menu.setAttribute('role', 'menu');
420
424
 
421
425
  for (const item of cleaned) {
422
426
  if (item.separator) {
@@ -427,6 +431,8 @@ export class NotebookTabBar {
427
431
  }
428
432
 
429
433
  const btn = document.createElement('button');
434
+ btn.type = 'button';
435
+ btn.setAttribute('role', 'menuitem');
430
436
  btn.className = 'nb-context-menu__item';
431
437
  if (item.danger) btn.classList.add('nb-context-menu__item--danger');
432
438
  if (item.disabled) btn.classList.add('nb-context-menu__item--disabled');
@@ -445,15 +451,14 @@ export class NotebookTabBar {
445
451
  menu.appendChild(btn);
446
452
  }
447
453
 
454
+ this.#contextMenuReturnFocus = document.activeElement;
448
455
  document.body.appendChild(menu);
449
456
  this.#contextMenuEl = menu;
450
457
 
451
- // Position with viewport clamping
452
- const rect = menu.getBoundingClientRect();
453
- const left = Math.min(x, window.innerWidth - rect.width - 8);
454
- const top = Math.min(y, window.innerHeight - rect.height - 8);
455
- menu.style.left = `${Math.max(0, left)}px`;
456
- menu.style.top = `${Math.max(0, top)}px`;
458
+ // BUG-0040. Inside the window, at most its height, scrolling when
459
+ // longer; the keyboard starts on the first item it can use.
460
+ placeMenuInViewport(menu, x, y);
461
+ focusMenuItem(this.#contextMenuItems()[0]);
457
462
 
458
463
  // Animate in
459
464
  requestAnimationFrame(() => menu.classList.add('visible'));
@@ -468,15 +473,29 @@ export class NotebookTabBar {
468
473
 
469
474
  #hideContextMenu() {
470
475
  if (!this.#contextMenuEl) return;
476
+ const returnTo = this.#contextMenuReturnFocus;
477
+ const held = this.#contextMenuEl.contains(document.activeElement);
471
478
  this.#contextMenuEl.remove();
472
479
  this.#contextMenuEl = null;
480
+ this.#contextMenuReturnFocus = null;
473
481
  document.removeEventListener('click', this.#boundHideContextMenu, true);
474
482
  document.removeEventListener('contextmenu', this.#boundHideContextMenu, true);
475
483
  document.removeEventListener('keydown', this.#onKeyDown);
484
+ if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });
485
+ }
486
+
487
+ /** BUG-0040. The menu's items the keyboard can land on. */
488
+ #contextMenuItems() {
489
+ return this.#contextMenuEl
490
+ ? [...this.#contextMenuEl.querySelectorAll('.nb-context-menu__item:not(.nb-context-menu__item--disabled)')]
491
+ : [];
476
492
  }
477
493
 
478
494
  #onKeyDown = (e) => {
479
- if (e.key === 'Escape') this.#hideContextMenu();
495
+ if (e.key === 'Escape') { this.#hideContextMenu(); return; }
496
+ // BUG-0040. Up/Down/Home/End move between the items and scroll the
497
+ // focused one into view; Enter and Space are the buttons' own.
498
+ moveMenuFocus(e, this.#contextMenuItems());
480
499
  };
481
500
 
482
501
  #handleContextMenuAction(action, filePath) {
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
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
package/src/host/host.js CHANGED
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
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { openRawTracesWindow } from '../charts/plot_popout_window.js';
10
10
  import { formatVariableLabel } from './config_schema.js';
11
+ import { ActionDropdown } from '../ui/components/action_dropdown.js';
12
+ import { modalHost } from '../ui/components/modal.js';
11
13
  // The host is INJECTED, not fetched from a global.
12
14
  //
13
15
  // This imported `appHost()` from ui/js/ecoagent/app_host.js — a tile reaching into
@@ -403,6 +405,12 @@ export class TileBase {
403
405
  if (this._disposed) return;
404
406
  this._disposed = true;
405
407
 
408
+ // An open ⋯ menu lives on document.body; take it back first.
409
+ this._closeTileMenu();
410
+ this._menuButton = null;
411
+ this._menuDropdown = null;
412
+ this._menuHome = null;
413
+
406
414
  // Close expand window if open
407
415
  if (this._expandWindow?.isVisible) {
408
416
  this._expandWindow.close();
@@ -485,6 +493,13 @@ export class TileBase {
485
493
  * @private
486
494
  */
487
495
  _buildChrome() {
496
+ // A rebuilt header gets a new ⋯ menu; an open old one must not stay
497
+ // behind on document.body.
498
+ this._closeTileMenu();
499
+ this._menuButton = null;
500
+ this._menuDropdown = null;
501
+ this._menuHome = null;
502
+
488
503
  // Header with drag handle and controls
489
504
  const header = document.createElement('div');
490
505
  header.className = 'tile-header';
@@ -584,23 +599,13 @@ export class TileBase {
584
599
  const menuBtn = header.querySelector('.tile-menu-btn');
585
600
  const menuDropdown = header.querySelector('.tile-menu-dropdown');
586
601
  if (menuBtn && menuDropdown) {
602
+ this._menuButton = menuBtn;
603
+ this._menuDropdown = menuDropdown;
604
+ this._menuHome = menuDropdown.parentNode;
587
605
  menuBtn.addEventListener('click', (e) => {
588
606
  e.stopPropagation();
589
- const isOpen = !menuDropdown.hidden;
590
- menuDropdown.hidden = !menuDropdown.hidden;
591
- if (!isOpen) {
592
- // Close on outside click
593
- const closeMenu = (ev) => {
594
- if (!menuDropdown.contains(ev.target) && ev.target !== menuBtn) {
595
- menuDropdown.hidden = true;
596
- document.removeEventListener('pointerdown', closeMenu, true);
597
- }
598
- };
599
- // Delay to avoid immediate close from the same click
600
- requestAnimationFrame(() => {
601
- document.addEventListener('pointerdown', closeMenu, true);
602
- });
603
- }
607
+ if (menuDropdown.hidden) this._openTileMenu();
608
+ else this._closeTileMenu();
604
609
  });
605
610
  // One listener per rendered entry, dispatching by `data-action`
606
611
  // into `_onMenuAction`. Wiring each action by name here is what
@@ -609,7 +614,7 @@ export class TileBase {
609
614
  menuDropdown.querySelectorAll('.tile-menu-item').forEach((itemEl) => {
610
615
  itemEl.addEventListener('click', (e) => {
611
616
  e.stopPropagation();
612
- menuDropdown.hidden = true;
617
+ this._closeTileMenu();
613
618
  this._onMenuAction(itemEl.dataset.action);
614
619
  });
615
620
  });
@@ -654,6 +659,52 @@ export class TileBase {
654
659
  }
655
660
  }
656
661
 
662
+ /**
663
+ * Open the ⋯ menu. It is drawn on `document.body` (or the open modal's
664
+ * host, as `showContextMenu` is), not inside the tile: a tile clips its
665
+ * content, so a menu wider than a narrow tile lost its left part. It is
666
+ * placed with `ActionDropdown.position` — fixed, under the button (above
667
+ * when there is no room), right edges aligned, kept inside the window.
668
+ * @private
669
+ */
670
+ _openTileMenu() {
671
+ const btn = this._menuButton;
672
+ const menu = this._menuDropdown;
673
+ if (!btn || !menu || !menu.hidden) return;
674
+ (modalHost() || document.body).appendChild(menu);
675
+ menu.hidden = false;
676
+ ActionDropdown.position(btn, menu, { align: 'end' });
677
+ // Close on a press outside the menu and its button. Added a frame
678
+ // later so the press that opened it does not close it.
679
+ this._menuOutsideListener = (ev) => {
680
+ if (!menu.contains(ev.target) && !btn.contains(ev.target)) this._closeTileMenu();
681
+ };
682
+ const listener = this._menuOutsideListener;
683
+ requestAnimationFrame(() => {
684
+ if (this._menuOutsideListener === listener) {
685
+ document.addEventListener('pointerdown', listener, true);
686
+ }
687
+ });
688
+ }
689
+
690
+ /**
691
+ * Close the ⋯ menu and put it back in the tile's header.
692
+ * @private
693
+ */
694
+ _closeTileMenu() {
695
+ const menu = this._menuDropdown;
696
+ if (this._menuOutsideListener) {
697
+ document.removeEventListener('pointerdown', this._menuOutsideListener, true);
698
+ this._menuOutsideListener = null;
699
+ }
700
+ if (!menu) return;
701
+ menu.hidden = true;
702
+ for (const prop of ['position', 'top', 'bottom', 'left', 'right', 'maxHeight', 'overflowY']) {
703
+ menu.style[prop] = '';
704
+ }
705
+ if (this._menuHome && menu.parentNode !== this._menuHome) this._menuHome.appendChild(menu);
706
+ }
707
+
657
708
  /**
658
709
  * Handle config button click — highlight tile and emit config request.
659
710
  * @private
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
package/src/tiling/wm.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes