flexdesk 0.16.1 → 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 (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  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-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  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 +3146 -164
  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 +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  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 +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  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 +0 -0
  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-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,322 @@
1
+ /**
2
+ * figure_tile.js — a tile that draws ONE Plotly figure, and owns the lifecycle
3
+ * of drawing it.
4
+ *
5
+ * A subclass says WHAT to draw and nothing else. It overrides one method:
6
+ *
7
+ * class SpreadTile extends FigureTile {
8
+ * static TYPE = 'myapp.spread';
9
+ * static TITLE = 'Spread';
10
+ * figure(data) {
11
+ * if (!data?.values?.length) return null; // the empty state
12
+ * return {
13
+ * traces: [buildHistogramTrace({ x: data.values })],
14
+ * layout: { margin: { l: 48, r: 16, t: 8, b: 36 } },
15
+ * };
16
+ * }
17
+ * }
18
+ *
19
+ * `figure(data)` answers `{traces, layout?, config?, frames?}` or `null`, and is
20
+ * synchronous: it computes, it does not wait. Everything else is the base's:
21
+ *
22
+ * - it awaits `ensurePlotly()`, so a page that loads Plotly lazily through
23
+ * `setPlotlySource` draws on first view instead of skipping the draw;
24
+ * - it draws through `createChart` (the theme defaults, `scrollZoom` off — a
25
+ * tile sits on a scrolling board — and the mode bar hidden; `config` from
26
+ * the figure goes over those), then adds `frames` when there are any;
27
+ * - a draw that finishes after a newer one, or after the tile went empty,
28
+ * loading, errored or disposed, is discarded and its figure purged;
29
+ * - `null` is the empty state (`static EMPTY_MESSAGE`). A subclass that wants
30
+ * to say WHY calls `this.showEmpty(message)` itself and returns `null`;
31
+ * - a `figure()` that throws, or a draw that fails (Plotly absent included),
32
+ * is the error state plus `tile:render-failed {tileId, message}` on the
33
+ * event bus — never an unhandled rejection;
34
+ * - one relayout when a resize settles (`onResize`), none while drawing;
35
+ * - Download PNG in the ⋯ menu, through the host's `dialogs.saveFile` when the
36
+ * grid was given a host, else a browser download;
37
+ * - Expand opens `openRawTracesWindow` with the last figure drawn;
38
+ * - `onPointClick({customdata, point, tile})` on a click on a mark.
39
+ *
40
+ * `menuItems()` here EXTENDS `TileBase.menuItems()` with Download PNG; a
41
+ * subclass extends these the same way (`[...super.menuItems(), …]`) and routes
42
+ * its own actions through `_onMenuAction`, calling `super` for the rest.
43
+ *
44
+ * ══ EVERY STATE CHANGE INVALIDATES THE FIGURE ═══════════════════════════
45
+ *
46
+ * `showEmpty`, `showLoading` and `showError` replace the content with
47
+ * `innerHTML`, which detaches the Plotly div without telling Plotly. So each of
48
+ * them here first purges the figure, drops the reference, marks it not drawn,
49
+ * and bumps `_drawSeq` — and a draw still in flight finds itself stale and
50
+ * purges its own figure instead of marking it drawn.
51
+ */
52
+
53
+ import { TileBase } from './tile_base.js';
54
+ import { createChart, ensurePlotly, purge, resize as resizePlot } from '../charts/plotly_wrapper.js';
55
+
56
+ /** What every tile's figure is drawn with; a figure's own `config` goes over it. */
57
+ const FIGURE_CONFIG = Object.freeze({
58
+ scrollZoom: false,
59
+ displayModeBar: false,
60
+ responsive: true,
61
+ });
62
+
63
+ const DOWNLOAD_PNG = Object.freeze({ action: 'download-png', label: 'Download PNG', icon: 'image' });
64
+
65
+ export class FigureTile extends TileBase {
66
+ static TYPE = 'flexdesk.figure';
67
+ static TITLE = 'Figure';
68
+ static EXPANDABLE = true;
69
+ /** The empty state's text when `figure()` answers `null`. */
70
+ static EMPTY_MESSAGE = 'No data to show.';
71
+
72
+ /**
73
+ * @param {Object} options - TileBase's, plus:
74
+ * @param {Function} [options.onPointClick] - `({customdata, point, tile})`
75
+ * on a click on a mark. Also assignable later as `tile.onPointClick`.
76
+ */
77
+ constructor(options = {}) {
78
+ super(options);
79
+ this.onPointClick = typeof options.onPointClick === 'function' ? options.onPointClick : null;
80
+ this._plotEl = null;
81
+ this._drawn = false;
82
+ this._drawSeq = 0;
83
+ /** The figure on screen, once drawn; what Expand opens. */
84
+ this._figure = null;
85
+ }
86
+
87
+ /**
88
+ * The one override: what to draw for `data`.
89
+ * @param {*} _data
90
+ * @returns {{traces: Array, layout?: Object, config?: Object, frames?: Array}|null}
91
+ */
92
+ figure(_data) {
93
+ throw new Error(`${this.constructor.name}.figure() must be implemented by the subclass`);
94
+ }
95
+
96
+ /**
97
+ * Hand this tile its data. Unlike `update()`, an empty or null value is
98
+ * drawn — as the empty state — rather than ignored.
99
+ */
100
+ setData(data) {
101
+ if (this._disposed) return;
102
+ this.data = data ?? null;
103
+ if (this.contentElement) this.render(this.data);
104
+ }
105
+
106
+ /**
107
+ * Kick off the draw and return. Deliberately not `async`: nothing awaits
108
+ * `render`, so a returned promise would be a rejection nobody catches.
109
+ */
110
+ render(data) {
111
+ this._draw(data);
112
+ }
113
+
114
+ /** One relayout when a resize settles; nothing while a draw is in flight. */
115
+ onResize() {
116
+ if (!this._plotEl || !this._drawn) return;
117
+ resizePlot(this._plotEl).catch(() => {});
118
+ }
119
+
120
+ menuItems() {
121
+ return [...super.menuItems(), ...this._figureMenuItems()];
122
+ }
123
+
124
+ /** The entries this class adds to the menu: Download PNG. For a subclass
125
+ * that wants them WITHOUT `TileBase`'s own. @protected */
126
+ _figureMenuItems() {
127
+ return [{ ...DOWNLOAD_PNG }];
128
+ }
129
+
130
+ _onMenuAction(action) {
131
+ if (action === DOWNLOAD_PNG.action) this._downloadPng();
132
+ else super._onMenuAction(action);
133
+ }
134
+
135
+ /** The last figure drawn, for the expand window; `null` while there is none. */
136
+ getExpandData() {
137
+ const fig = this._figure;
138
+ if (!fig) return null;
139
+ return {
140
+ title: this.config.title || this.constructor.TITLE,
141
+ // Copies: Plotly writes onto the trace and layout objects it draws.
142
+ traces: fig.traces.map((t) => ({ ...t })),
143
+ layout: { ...(fig.layout || {}) },
144
+ frames: Array.isArray(fig.frames) ? fig.frames : undefined,
145
+ };
146
+ }
147
+
148
+ // ── the three states: each one invalidates the figure ───────────────
149
+
150
+ showLoading() {
151
+ this._invalidate();
152
+ super.showLoading();
153
+ }
154
+
155
+ showEmpty(message) {
156
+ this._invalidate();
157
+ super.showEmpty(message);
158
+ }
159
+
160
+ showError(message) {
161
+ this._invalidate();
162
+ super.showError(message);
163
+ }
164
+
165
+ /** Release the figure BEFORE the element goes — `TileBase.dispose` nulls it. */
166
+ dispose() {
167
+ this._invalidate();
168
+ super.dispose();
169
+ }
170
+
171
+ /** Any draw in flight is now stale, and the figure on screen is released. */
172
+ _invalidate() {
173
+ this._drawSeq += 1;
174
+ this._releasePlot();
175
+ }
176
+
177
+ /** Hand the current figure back to Plotly and forget it. Idempotent, and
178
+ * not awaited: a rejected purge (Plotly absent, already purged) must not
179
+ * surface as an unhandled rejection. */
180
+ _releasePlot() {
181
+ const el = this._plotEl;
182
+ this._plotEl = null;
183
+ this._drawn = false;
184
+ this._figure = null;
185
+ if (el) purge(el).catch(() => {});
186
+ }
187
+
188
+ /**
189
+ * The nodes the content holds while `plotEl` draws `fig`: the plot alone
190
+ * here. A subclass adds what sits around the figure (a note, a caption).
191
+ * @param {HTMLElement} plotEl
192
+ * @param {Object} _fig - what `figure()` answered
193
+ * @returns {HTMLElement[]}
194
+ * @protected
195
+ */
196
+ _figureContent(plotEl, _fig) {
197
+ return [plotEl];
198
+ }
199
+
200
+ // ── the draw ────────────────────────────────────────────────────────
201
+
202
+ async _draw(data) {
203
+ this._drawSeq += 1;
204
+ const seq = this._drawSeq;
205
+ const content = this.contentElement;
206
+ if (!content) return;
207
+ const stale = () => this._disposed || seq !== this._drawSeq;
208
+
209
+ // Every path below replaces the content, so the figure there now is
210
+ // about to be orphaned whichever way this goes.
211
+ this._releasePlot();
212
+
213
+ let fig;
214
+ try {
215
+ fig = this.figure(data);
216
+ if (fig != null && (typeof fig !== 'object' || !Array.isArray(fig.traces))) {
217
+ throw new Error('figure() must answer {traces, layout} or null');
218
+ }
219
+ } catch (err) {
220
+ this._failed(err);
221
+ return;
222
+ }
223
+ if (!fig) {
224
+ // A subclass that already said why (showEmpty) has bumped the sequence.
225
+ if (!stale()) this.showEmpty(this.constructor.EMPTY_MESSAGE);
226
+ return;
227
+ }
228
+
229
+ const host = document.createElement('div');
230
+ host.className = 'fd-figure-tile';
231
+ content.replaceChildren(...this._figureContent(host, fig));
232
+ this._plotEl = host;
233
+ this._drawn = false;
234
+
235
+ try {
236
+ const P = await ensurePlotly();
237
+ // Data that changed while Plotly was loading: draw only the latest.
238
+ if (stale()) return;
239
+ await createChart(host, fig.traces, fig.layout || {}, { ...FIGURE_CONFIG, ...(fig.config || {}) });
240
+ if (Array.isArray(fig.frames) && fig.frames.length && !stale()) {
241
+ await P.addFrames(host, fig.frames);
242
+ }
243
+ } catch (err) {
244
+ if (stale()) return;
245
+ this._failed(err);
246
+ return;
247
+ }
248
+ if (stale()) {
249
+ // A newer draw or a state change has already replaced the content;
250
+ // this figure is detached, and purging it releases its listeners.
251
+ purge(host).catch(() => {});
252
+ return;
253
+ }
254
+ this._drawn = true;
255
+ this._figure = fig;
256
+ this._wireClick(host);
257
+ }
258
+
259
+ /** Plotly's click, reduced to the point's own `customdata`. A pie's point
260
+ * carries its slice index as `pointNumber` (older builds: `pointIndex`),
261
+ * and its `customdata` is read off the trace by that index. */
262
+ _wireClick(host) {
263
+ if (typeof host.on !== 'function') return;
264
+ host.on('plotly_click', (ev) => {
265
+ const point = ev?.points?.[0];
266
+ if (!point || typeof this.onPointClick !== 'function') return;
267
+ const trace = point.data || {};
268
+ let customdata = point.customdata;
269
+ if (trace.type === 'pie' || customdata === undefined) {
270
+ const i = point.pointNumber ?? point.pointIndex;
271
+ if (Array.isArray(trace.customdata) && i != null) customdata = trace.customdata[i];
272
+ }
273
+ try {
274
+ this.onPointClick({ customdata, point, tile: this });
275
+ } catch (err) {
276
+ console.error(`[${this.constructor.name}] onPointClick threw:`, err);
277
+ }
278
+ });
279
+ }
280
+
281
+ async _downloadPng() {
282
+ if (!this._plotEl || !this._drawn) return;
283
+ try {
284
+ const P = await ensurePlotly();
285
+ const uri = await P.toImage(this._plotEl, { format: 'png', scale: 2 });
286
+ const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
287
+ // The native Save dialog when the host has one (a desktop window).
288
+ // `saveFile` answers null when there is no dialog behind it, which
289
+ // means "download it yourself", not a failed save.
290
+ const dialogs = this.host?.dialogs;
291
+ const result = dialogs
292
+ ? await dialogs.saveFile({ data: uri.split(',')[1], filename, kind: 'png' })
293
+ : null;
294
+ if (result) {
295
+ if (result.ok) this._notify('Download', `Chart saved to ${result.path}`, 'success');
296
+ else if (!result.cancelled) this._notify('Download failed', result.error || 'The image could not be saved', 'error');
297
+ return;
298
+ }
299
+ const a = document.createElement('a');
300
+ a.href = uri;
301
+ a.download = filename;
302
+ a.click();
303
+ } catch (err) {
304
+ this._notify('Download failed', `The image could not be made: ${err?.message || err}`, 'error');
305
+ }
306
+ }
307
+
308
+ _failed(err) {
309
+ if (this._disposed) return;
310
+ this.showError(`This chart could not be drawn: ${err?.message || err}`);
311
+ this.eventBus?.emit?.('tile:render-failed', {
312
+ tileId: this.id,
313
+ message: err?.message || String(err),
314
+ });
315
+ }
316
+ }
317
+
318
+ function slug(text) {
319
+ return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'chart';
320
+ }
321
+
322
+ export default FigureTile;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Three things used to make this un-shippable, and all three are now injected:
5
5
  *
6
- * 1. It wrote to `localStorage` under 'ecosim.results.layouts' and
7
- * 'ecosim.results.configs' — a LIBRARY scribbling on one application's storage
6
+ * 1. It wrote to `localStorage` under two keys named after one application
7
+ * — a LIBRARY scribbling on one application's storage
8
8
  * keys. The keys are the consumer's now, and EcoAgent passes its existing ones,
9
9
  * so not a single saved layout is orphaned.
10
10
  *
@@ -639,7 +639,7 @@ let _options = {};
639
639
  * called first won. That is a trap with teeth: if ANY code path had reached the
640
640
  * store before the dashboard did, the singleton would have been built with the
641
641
  * library's generic defaults, and the app would have read
642
- * 'twm.tiles.layouts' instead of 'ecosim.results.layouts' — an empty store. Every
642
+ * 'twm.tiles.layouts' instead of the app's own key — an empty store. Every
643
643
  * saved dashboard a user had ever built would have silently vanished, with no
644
644
  * error, and no captured view would show it because the pixel gate runs on a
645
645
  * fresh template that has no saved layouts to lose.
@@ -130,11 +130,11 @@ class RowsTile extends TileBase {
130
130
  getConfigSchema() {
131
131
  return {
132
132
  fields: [
133
- { name: 'title', type: 'text', label: 'Tile title' },
134
- { name: 'empty_text', type: 'text', label: 'When there is nothing',
135
- help: 'What the tile says when the list is empty, e.g. "Nothing is late."' },
136
- { name: 'max_rows', type: 'number', label: 'Rows at most', min: 0,
137
- help: '0 shows them all; the tile scrolls.' },
133
+ { key: 'title', type: 'text', label: 'Tile title' },
134
+ { key: 'empty_text', type: 'text', label: 'When there is nothing',
135
+ description: 'What the tile says when the list is empty, e.g. "Nothing is late."' },
136
+ { key: 'max_rows', type: 'number', label: 'Rows at most', min: 0,
137
+ description: '0 shows them all; the tile scrolls.' },
138
138
  ],
139
139
  };
140
140
  }
@@ -367,8 +367,8 @@ export class PeopleTile extends RowsTile {
367
367
 
368
368
  getConfigSchema() {
369
369
  const { fields } = super.getConfigSchema();
370
- return { fields: [...fields, { name: 'count_label', type: 'text', label: 'Count reads',
371
- help: 'After each person\'s number, e.g. "open" for "3 open".' }] };
370
+ return { fields: [...fields, { key: 'count_label', type: 'text', label: 'Count reads',
371
+ description: 'After each person\'s number, e.g. "open" for "3 open".' }] };
372
372
  }
373
373
 
374
374
  _renderItem(p) {
File without changes
@@ -61,7 +61,7 @@ export class TileBase {
61
61
  // without one behaves exactly as it did before this line existed, and the
62
62
  // broadcast path is untouched.
63
63
  this.dataSource = dataSource;
64
- // `window.__ECOSIM_JS_NEW__?.stateGuard` — an application's global, reached for
64
+ // This used to be read off an application's window global — reached for
65
65
  // by a tile. The StateGuard is a FRAMEWORK service (@flexdesk/core); the INSTANCE is
66
66
  // the application's, and it is handed down through the grid. No guard => the
67
67
  // dataset writes happen directly, which is what the optional chain already did.