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
@@ -34,33 +34,26 @@
34
34
  * each. `line` on a series is merged over the built `trace.line`, so
35
35
  * `line: {shape: 'hv', dash: 'dash'}` is as good as `interpolation`/`lineStyle`.
36
36
  *
37
- * ══ WHY THE DRAW HAS ITS OWN TRY/CATCH, AND A SEQUENCE ══════════════════
37
+ * ══ THE DRAW IS FigureTile's ═══════════════════════════════════════════
38
38
  *
39
- * The draw is `await ensurePlotly()` deep, so `render()` returns long before
40
- * anything can throw. `_draw` therefore reports its own failures (Plotly
41
- * absent included — a clear error state, never an unhandled rejection), and
42
- * `_drawSeq` discards a draw that lands after a newer one.
39
+ * `ChartTile` is a `FigureTile` (figure_tile.js, 0.17.0): it answers
40
+ * `figure(data)` with the traces and layout `_plan` builds, and the base awaits
41
+ * Plotly, draws, discards a stale draw, purges on every state change, relayouts
42
+ * after a resize, downloads the PNG and opens Expand. What stays here is what a
43
+ * CHART is: the data shorthands, the chart types, the note under the figure.
43
44
  *
44
- * EVERY STATE CHANGE INVALIDATES THE FIGURE. `showEmpty`, `showLoading` and
45
- * `showError` replace the content with `innerHTML`, which detaches the Plotly div
46
- * without telling Plotly. So each of them here first purges the figure, drops the
47
- * reference, marks it not drawn, and bumps `_drawSeq` — and a draw still in
48
- * flight when `setData(empty)` arrives finds itself stale and purges its own
49
- * figure instead of marking it drawn.
50
- *
51
- * `scrollZoom` is off per call: a tile sits on a scrolling board, and the wheel
52
- * over a chart must scroll past it rather than zoom it.
45
+ * The menu is Download PNG alone, as 0.13.0 shipped it: `TileBase`'s two
46
+ * documentation entries belong to an application with documentation.
53
47
  */
54
48
 
55
49
  import { TileBase } from './tile_base.js';
50
+ import { FigureTile } from './figure_tile.js';
56
51
  import { registerWidget, getWidget } from './tile_registry.js';
57
52
  import {
58
53
  buildTrace, buildXAxisLayout, buildYAxisLayout, buildLegendLayout,
59
54
  buildHistogramTrace, buildOverlayShapes,
60
55
  } from '../charts/chart_types.js';
61
- import {
62
- createChart, resize as resizePlot, purge, ensurePlotly, getSeriesColor,
63
- } from '../charts/plotly_wrapper.js';
56
+ import { getSeriesColor } from '../charts/plotly_wrapper.js';
64
57
  import { strideDownsampleShared } from '../charts/downsample.js';
65
58
 
66
59
  /** Part-of-whole types: one series, no axes. */
@@ -136,7 +129,7 @@ function pointCount(s) {
136
129
  return 0;
137
130
  }
138
131
 
139
- export class ChartTile extends TileBase {
132
+ export class ChartTile extends FigureTile {
140
133
  static TYPE = 'flexdesk.chart';
141
134
  static TITLE = 'Chart';
142
135
  static ICON = 'insert_chart';
@@ -144,19 +137,6 @@ export class ChartTile extends TileBase {
144
137
  static DEFAULT_SIZE = { w: 6, h: 4 };
145
138
  static SIZE_CONSTRAINTS = { minW: 3, minH: 3, maxW: 12, maxH: 10 };
146
139
 
147
- /**
148
- * @param {Object} options - TileBase's, plus:
149
- * @param {Function} [options.onPointClick] - `({customdata, point, tile})`
150
- * on a click on a mark. Also assignable later as `tile.onPointClick`.
151
- */
152
- constructor(options = {}) {
153
- super(options);
154
- this.onPointClick = typeof options.onPointClick === 'function' ? options.onPointClick : null;
155
- this._plotEl = null;
156
- this._drawn = false;
157
- this._drawSeq = 0;
158
- }
159
-
160
140
  /** Pure and static-only — it runs inside `TileBase`'s constructor. */
161
141
  getDefaultConfig() {
162
142
  return {
@@ -177,12 +157,12 @@ export class ChartTile extends TileBase {
177
157
  getConfigSchema() {
178
158
  return {
179
159
  fields: [
180
- { name: 'title', type: 'text', label: 'Tile title' },
181
- { name: 'chart_type', type: 'select', label: 'Chart type',
160
+ { key: 'title', type: 'text', label: 'Tile title' },
161
+ { key: 'chart_type', type: 'select', label: 'Chart type',
182
162
  options: CHART_TILE_TYPES.map((o) => ({ ...o })) },
183
- { name: 'unit', type: 'text', label: 'Unit',
184
- help: 'Shown with the value axis title, e.g. "pts" or "h".' },
185
- { name: 'legend', type: 'select', label: 'Legend',
163
+ { key: 'unit', type: 'text', label: 'Unit',
164
+ description: 'Shown with the value axis title, e.g. "pts" or "h".' },
165
+ { key: 'legend', type: 'select', label: 'Legend',
186
166
  visibleWhen: { field: 'chart_type', in: [...READS.legend] },
187
167
  options: [
188
168
  { value: 'bottom', label: 'Below' },
@@ -191,18 +171,18 @@ export class ChartTile extends TileBase {
191
171
  { value: 'left', label: 'Left' },
192
172
  { value: 'hidden', label: 'None' },
193
173
  ] },
194
- { name: 'x_title', type: 'text', label: 'X axis title',
174
+ { key: 'x_title', type: 'text', label: 'X axis title',
195
175
  visibleWhen: { field: 'chart_type', in: [...READS.axis_titles] } },
196
- { name: 'y_title', type: 'text', label: 'Y axis title',
176
+ { key: 'y_title', type: 'text', label: 'Y axis title',
197
177
  visibleWhen: { field: 'chart_type', in: [...READS.axis_titles] } },
198
- { name: 'y_scale', type: 'select', label: 'Y scale',
178
+ { key: 'y_scale', type: 'select', label: 'Y scale',
199
179
  visibleWhen: { field: 'chart_type', in: [...READS.y_scale] },
200
180
  options: [{ value: 'linear', label: 'Linear' }, { value: 'log', label: 'Logarithmic' }] },
201
- { name: 'y_min', type: 'number', label: 'Y minimum',
181
+ { key: 'y_min', type: 'number', label: 'Y minimum',
202
182
  visibleWhen: { field: 'chart_type', in: [...READS.y_range] } },
203
- { name: 'y_max', type: 'number', label: 'Y maximum',
183
+ { key: 'y_max', type: 'number', label: 'Y maximum',
204
184
  visibleWhen: { field: 'chart_type', in: [...READS.y_range] } },
205
- { name: 'show_markers', type: 'checkbox', label: 'Show point markers',
185
+ { key: 'show_markers', type: 'checkbox', label: 'Show point markers',
206
186
  visibleWhen: { field: 'chart_type', in: [...READS.show_markers] } },
207
187
  ],
208
188
  };
@@ -225,163 +205,31 @@ export class ChartTile extends TileBase {
225
205
  if (config) applyTitle(this);
226
206
  }
227
207
 
228
- /**
229
- * Hand this tile its series. Unlike `update()`, an empty or null value is
230
- * drawn — as the empty state — rather than ignored.
231
- */
232
- setData(data) {
233
- if (this._disposed) return;
234
- this.data = data ?? null;
235
- if (this.contentElement) this.render(this.data);
236
- }
237
-
238
- /**
239
- * Kick off the draw and return. Deliberately not `async`: nothing awaits
240
- * `render`, so a returned promise would be a rejection nobody catches.
241
- */
242
- render(data) {
243
- this._draw(data);
244
- }
245
-
246
- /** One relayout when a resize settles; nothing while a draw is in flight. */
247
- onResize() {
248
- if (!this._plotEl || !this._drawn) return;
249
- resizePlot(this._plotEl).catch(() => {});
208
+ /** `null` (the empty state) when there is nothing to draw. */
209
+ figure(data) {
210
+ const norm = normalizeChartData(data);
211
+ if (!norm) return null;
212
+ return { ...this._plan(norm), note: norm.note };
250
213
  }
251
214
 
215
+ /** Download PNG alone — see the header. */
252
216
  menuItems() {
253
- return [{ action: 'download-png', label: 'Download PNG', icon: 'image' }];
254
- }
255
-
256
- _onMenuAction(action) {
257
- if (action === 'download-png') this._downloadPng();
258
- }
259
-
260
- // ── the three states: each one invalidates the figure ───────────────
261
-
262
- showLoading() {
263
- this._invalidate();
264
- super.showLoading();
265
- }
266
-
267
- showEmpty(message) {
268
- this._invalidate();
269
- super.showEmpty(message);
270
- }
271
-
272
- showError(message) {
273
- this._invalidate();
274
- super.showError(message);
275
- }
276
-
277
- /** Release the figure BEFORE the element goes — `TileBase.dispose` nulls it. */
278
- dispose() {
279
- this._invalidate();
280
- super.dispose();
281
- }
282
-
283
- /** Any draw in flight is now stale, and the figure on screen is released. */
284
- _invalidate() {
285
- this._drawSeq += 1;
286
- this._releasePlot();
287
- }
288
-
289
- /** Hand the current figure back to Plotly and forget it. Idempotent, and
290
- * not awaited: a rejected purge (Plotly absent, already purged) must not
291
- * surface as an unhandled rejection. */
292
- _releasePlot() {
293
- const el = this._plotEl;
294
- this._plotEl = null;
295
- this._drawn = false;
296
- if (el) purge(el).catch(() => {});
297
- }
298
-
299
- // ── the draw ────────────────────────────────────────────────────────
300
-
301
- async _draw(data) {
302
- this._drawSeq += 1;
303
- const seq = this._drawSeq;
304
- const content = this.contentElement;
305
- if (!content) return;
306
-
307
- // Every path below replaces the content, so the figure there now is
308
- // about to be orphaned whichever way this goes.
309
- this._releasePlot();
310
-
311
- const norm = normalizeChartData(data);
312
- if (!norm) { this.showEmpty('No data to show.'); return; }
313
-
314
- let plan;
315
- try {
316
- plan = this._plan(norm);
317
- } catch (err) {
318
- this._failed(err);
319
- return;
320
- }
321
- if (!plan) return; // `_plan` has already said why
322
-
323
- const host = document.createElement('div');
324
- host.className = 'fd-chart-tile';
325
- const parts = [host];
326
- if (norm.note) {
327
- const note = document.createElement('div');
328
- note.className = 'fd-chart-tile__note';
329
- note.setAttribute('role', 'note');
330
- note.textContent = norm.note;
331
- note.title = norm.note;
332
- parts.push(note);
333
- }
334
- content.replaceChildren(...parts);
335
- this._plotEl = host;
336
- this._drawn = false;
337
-
338
- try {
339
- await createChart(host, plan.traces, plan.layout, {
340
- scrollZoom: false,
341
- displayModeBar: false,
342
- responsive: true,
343
- });
344
- } catch (err) {
345
- if (this._disposed || seq !== this._drawSeq) return;
346
- this._failed(err);
347
- return;
348
- }
349
- if (this._disposed || seq !== this._drawSeq) {
350
- // A newer draw or a state change has already replaced the content;
351
- // this figure is detached, and purging it releases its listeners.
352
- purge(host).catch(() => {});
353
- return;
354
- }
355
- this._drawn = true;
356
- this._wireClick(host);
217
+ return this._figureMenuItems();
357
218
  }
358
219
 
359
- /** Plotly's click, reduced to the point's own `customdata`. A pie's point
360
- * carries its slice index as `pointNumber` (older builds: `pointIndex`),
361
- * and its `customdata` is read off the trace by that index. */
362
- _wireClick(host) {
363
- if (typeof host.on !== 'function') return;
364
- host.on('plotly_click', (ev) => {
365
- const point = ev?.points?.[0];
366
- if (!point || typeof this.onPointClick !== 'function') return;
367
- const trace = point.data || {};
368
- let customdata = point.customdata;
369
- if (trace.type === 'pie' || customdata === undefined) {
370
- const i = point.pointNumber ?? point.pointIndex;
371
- if (Array.isArray(trace.customdata) && i != null) customdata = trace.customdata[i];
372
- }
373
- try {
374
- this.onPointClick({ customdata, point, tile: this });
375
- } catch (err) {
376
- console.error('[ChartTile] onPointClick threw:', err);
377
- }
378
- });
220
+ /** The figure, and the note on its own line under it. */
221
+ _figureContent(plotEl, fig) {
222
+ plotEl.classList.add('fd-chart-tile');
223
+ if (!fig.note) return [plotEl];
224
+ const note = document.createElement('div');
225
+ note.className = 'fd-chart-tile__note';
226
+ note.setAttribute('role', 'note');
227
+ note.textContent = fig.note;
228
+ note.title = fig.note;
229
+ return [plotEl, note];
379
230
  }
380
231
 
381
- /**
382
- * Turn normalised data into `{traces, layout}`, or put the tile in the
383
- * empty state and answer `null`.
384
- */
232
+ /** Turn normalised data into `{traces, layout}`. */
385
233
  _plan(norm) {
386
234
  const type = this.config.chart_type || 'line';
387
235
  const { series } = norm;
@@ -522,42 +370,6 @@ export class ChartTile extends TileBase {
522
370
  }
523
371
  return out;
524
372
  }
525
-
526
- async _downloadPng() {
527
- if (!this._plotEl || !this._drawn) return;
528
- try {
529
- const P = await ensurePlotly();
530
- const uri = await P.toImage(this._plotEl, { format: 'png', scale: 2 });
531
- const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
532
- // The native Save dialog when the host has one (a desktop window).
533
- // `saveFile` answers null when there is no dialog behind it, which
534
- // means "download it yourself", not a failed save.
535
- const dialogs = this.host?.dialogs;
536
- const result = dialogs
537
- ? await dialogs.saveFile({ data: uri.split(',')[1], filename, kind: 'png' })
538
- : null;
539
- if (result) {
540
- if (result.ok) this._notify('Download', `Chart saved to ${result.path}`, 'success');
541
- else if (!result.cancelled) this._notify('Download failed', result.error || 'The image could not be saved', 'error');
542
- return;
543
- }
544
- const a = document.createElement('a');
545
- a.href = uri;
546
- a.download = filename;
547
- a.click();
548
- } catch (err) {
549
- this._notify('Download failed', `The image could not be made: ${err?.message || err}`, 'error');
550
- }
551
- }
552
-
553
- _failed(err) {
554
- if (this._disposed) return;
555
- this.showError(`This chart could not be drawn: ${err?.message || err}`);
556
- this.eventBus?.emit?.('tile:render-failed', {
557
- tileId: this.id,
558
- message: err?.message || String(err),
559
- });
560
- }
561
373
  }
562
374
 
563
375
  /**
@@ -599,10 +411,10 @@ export class KpiTile extends TileBase {
599
411
  getConfigSchema() {
600
412
  return {
601
413
  fields: [
602
- { name: 'title', type: 'text', label: 'Tile title' },
603
- { name: 'label', type: 'text', label: 'Label' },
604
- { name: 'unit', type: 'text', label: 'Unit', help: 'Shown after the number, e.g. "pts".' },
605
- { name: 'format', type: 'select', label: 'Format',
414
+ { key: 'title', type: 'text', label: 'Tile title' },
415
+ { key: 'label', type: 'text', label: 'Label' },
416
+ { key: 'unit', type: 'text', label: 'Unit', description: 'Shown after the number, e.g. "pts".' },
417
+ { key: 'format', type: 'select', label: 'Format',
606
418
  options: [
607
419
  { value: 'number', label: 'Number' },
608
420
  { value: 'percent', label: 'Percentage (of a ratio)' },
@@ -610,15 +422,15 @@ export class KpiTile extends TileBase {
610
422
  { value: 'duration', label: 'Duration (seconds)' },
611
423
  { value: 'text', label: 'Text' },
612
424
  ] },
613
- { name: 'decimals', type: 'number', label: 'Decimals', min: 0, max: 6 },
614
- { name: 'currency', type: 'text', label: 'Currency code',
615
- help: 'ISO 4217, e.g. EUR. Left empty, the number is shown without a symbol.' },
616
- { name: 'prefix', type: 'text', label: 'Prefix' },
617
- { name: 'suffix', type: 'text', label: 'Suffix' },
618
- { name: 'delta_relative', type: 'checkbox', label: 'Show the change as a percentage' },
619
- { name: 'invert_delta', type: 'checkbox', label: 'A fall is good',
620
- help: 'For a cost, a queue length or a latency: a smaller number is the better one.' },
621
- { name: 'foot', type: 'text', label: 'Caption' },
425
+ { key: 'decimals', type: 'number', label: 'Decimals', min: 0, max: 6 },
426
+ { key: 'currency', type: 'text', label: 'Currency code',
427
+ description: 'ISO 4217, e.g. EUR. Left empty, the number is shown without a symbol.' },
428
+ { key: 'prefix', type: 'text', label: 'Prefix' },
429
+ { key: 'suffix', type: 'text', label: 'Suffix' },
430
+ { key: 'delta_relative', type: 'checkbox', label: 'Show the change as a percentage' },
431
+ { key: 'invert_delta', type: 'checkbox', label: 'A fall is good',
432
+ description: 'For a cost, a queue length or a latency: a smaller number is the better one.' },
433
+ { key: 'foot', type: 'text', label: 'Caption' },
622
434
  ],
623
435
  };
624
436
  }
@@ -834,7 +646,3 @@ function formatValue(value, { format = 'number', decimals = null, prefix = '', s
834
646
  })();
835
647
  return `${prefix || ''}${body}${suffix || ''}`;
836
648
  }
837
-
838
- function slug(text) {
839
- return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'chart';
840
- }
@@ -5,6 +5,8 @@
5
5
  * Defines field types, validation rules, and default values.
6
6
  */
7
7
 
8
+ import { isHexColor } from '../ui/components/color_field.js';
9
+
8
10
  /**
9
11
  * Field type definitions for widget configuration.
10
12
  */
@@ -54,6 +56,12 @@ export function validateField(value, field) {
54
56
  }
55
57
  break;
56
58
 
59
+ case FieldTypes.COLOR:
60
+ if (value != null && value !== '' && !isHexColor(value)) {
61
+ return { valid: false, error: `${field.label} must be a hex colour (#rgb or #rrggbb)` };
62
+ }
63
+ break;
64
+
57
65
  case FieldTypes.MULTISELECT:
58
66
  if (field.required && (!Array.isArray(value) || value.length === 0)) {
59
67
  return { valid: false, error: `${field.label} must have at least one selection` };