flexdesk 0.16.1 → 0.17.1

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 (262) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2435 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +18 -0
  6. package/charts.js +21 -0
  7. package/core.js +14 -0
  8. package/css/base.css +1834 -75
  9. package/css/flexdesk.css +3165 -159
  10. package/css/overrides.css +16 -0
  11. package/css/reset.css +0 -0
  12. package/css/tokens.css +0 -0
  13. package/dist/canvas.js +6 -239
  14. package/dist/canvas.js.map +3 -3
  15. package/dist/charts.js +707 -18
  16. package/dist/charts.js.map +4 -4
  17. package/dist/chunk-3PHCPZHT.js +0 -0
  18. package/dist/chunk-3PHCPZHT.js.map +0 -0
  19. package/dist/chunk-4A7SJC3T.js +503 -0
  20. package/dist/chunk-4A7SJC3T.js.map +7 -0
  21. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  22. package/dist/chunk-CVMCZZX2.js.map +7 -0
  23. package/dist/chunk-D3COLZZ2.js +1311 -0
  24. package/dist/chunk-D3COLZZ2.js.map +7 -0
  25. package/dist/chunk-FOOS3T5L.js +0 -0
  26. package/dist/chunk-FOOS3T5L.js.map +0 -0
  27. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  28. package/dist/chunk-GOQLR5WR.js.map +7 -0
  29. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  30. package/dist/chunk-HNU6MM2B.js.map +7 -0
  31. package/dist/chunk-HYJYYEPK.js +375 -0
  32. package/dist/chunk-HYJYYEPK.js.map +7 -0
  33. package/dist/chunk-JKZLNKG6.js +2041 -0
  34. package/dist/chunk-JKZLNKG6.js.map +7 -0
  35. package/dist/chunk-JYWURG5T.js +0 -0
  36. package/dist/chunk-JYWURG5T.js.map +0 -0
  37. package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
  38. package/dist/chunk-LWV23YVE.js.map +7 -0
  39. package/dist/chunk-N7HC3ADV.js +30 -0
  40. package/dist/chunk-N7HC3ADV.js.map +7 -0
  41. package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
  42. package/dist/chunk-RMTRWWEO.js.map +7 -0
  43. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  44. package/dist/chunk-UURZ37SD.js.map +7 -0
  45. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  46. package/dist/chunk-UWO43BZU.js.map +7 -0
  47. package/dist/chunk-WVFGV5FT.js +0 -0
  48. package/dist/chunk-WVFGV5FT.js.map +0 -0
  49. package/dist/core.js +170 -118
  50. package/dist/core.js.map +2 -2
  51. package/dist/editor.js +156 -70
  52. package/dist/editor.js.map +2 -2
  53. package/dist/flexdesk.css +3165 -159
  54. package/dist/flow.js +2789 -306
  55. package/dist/flow.js.map +4 -4
  56. package/dist/host.js +109 -3
  57. package/dist/host.js.map +3 -3
  58. package/dist/reset.css +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js +0 -0
  60. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  61. package/dist/tiles.js +657 -234
  62. package/dist/tiles.js.map +4 -4
  63. package/dist/tokens.css +0 -0
  64. package/dist/widgets.js +3290 -2124
  65. package/dist/widgets.js.map +4 -4
  66. package/dist/wm.js +1227 -347
  67. package/dist/wm.js.map +4 -4
  68. package/editor.js +20 -0
  69. package/flow.js +21 -0
  70. package/host.js +14 -0
  71. package/package.json +12 -1
  72. package/src/canvas/canvas_adapter.js +5 -0
  73. package/src/canvas/edge_router.js +0 -0
  74. package/src/canvas/graph/connector_router.js +0 -0
  75. package/src/canvas/graph/history_service.js +0 -0
  76. package/src/canvas/graph/node_platform.js +0 -0
  77. package/src/canvas/index.js +1 -0
  78. package/src/canvas/layered_layout.js +244 -0
  79. package/src/charts/chart_types.js +0 -0
  80. package/src/charts/data_series_plot_window.js +0 -0
  81. package/src/charts/downsample.js +2 -2
  82. package/src/charts/plot_config.js +53 -29
  83. package/src/charts/plot_config_editor.js +627 -0
  84. package/src/charts/plot_grid.js +449 -0
  85. package/src/charts/plot_popout_window.js +103 -317
  86. package/src/charts/plotly_wrapper.js +22 -2
  87. package/src/charts/series_transforms.js +206 -0
  88. package/src/core/event_bus.js +0 -0
  89. package/src/core/logging.js +1 -1
  90. package/src/core/settings.js +105 -19
  91. package/src/core/state_guard.js +93 -51
  92. package/src/core/state_guard_config.js +85 -74
  93. package/src/core/state_machine.js +0 -0
  94. package/src/editor/editor_pane.js +3 -3
  95. package/src/editor/monaco_editor_factory.js +124 -60
  96. package/src/editor/monaco_loader.js +103 -28
  97. package/src/editor/notebook_search.js +0 -0
  98. package/src/editor/notebook_tab_bar.js +38 -52
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +6 -3
  101. package/src/flow/canvas_editor/connections.js +90 -0
  102. package/src/flow/canvas_editor/editor.js +2351 -153
  103. package/src/flow/canvas_editor/index.js +2 -1
  104. package/src/flow/canvas_editor/layout.js +119 -12
  105. package/src/flow/canvas_editor/strings.js +80 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +97 -3
  108. package/src/flow/kit/chip_input.js +0 -0
  109. package/src/flow/kit/dom.js +0 -0
  110. package/src/flow/kit/findings.js +0 -0
  111. package/src/flow/kit/history.js +0 -0
  112. package/src/flow/kit/index.js +3 -3
  113. package/src/flow/kit/keys.js +7 -1
  114. package/src/flow/kit/logic_graph.js +97 -15
  115. package/src/flow/kit/references.js +0 -0
  116. package/src/flow/kit/settings_panel.js +0 -0
  117. package/src/flow/kit/settings_schema.js +2 -1
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +3 -1
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +86 -109
  122. package/src/flow/lanes/editor.js +136 -14
  123. package/src/flow/lanes/index.js +3 -1
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +44 -2
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +1 -0
  128. package/src/flow/outline/editor.js +0 -0
  129. package/src/flow/outline/index.js +0 -0
  130. package/src/flow/outline/mapping.js +0 -0
  131. package/src/flow/outline/menu.js +0 -0
  132. package/src/flow/outline/operations.js +0 -0
  133. package/src/flow/outline/recognise.js +0 -0
  134. package/src/flow/outline/strings.js +0 -0
  135. package/src/help/help_modal.js +168 -42
  136. package/src/help/help_registry.js +12 -1
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +117 -0
  139. package/src/host/host.js +10 -2
  140. package/src/host/photino_host.js +9 -0
  141. package/src/host/photino_host.md +21 -3
  142. package/src/host/pywebview_host.js +10 -0
  143. package/src/tiles/chart_tiles.js +53 -245
  144. package/src/tiles/config_schema.js +8 -0
  145. package/src/tiles/figure_tile.js +322 -0
  146. package/src/tiles/layout_persistence.js +3 -3
  147. package/src/tiles/list_tiles.js +7 -7
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_base.js +1 -1
  150. package/src/tiles/tile_config_form.js +442 -0
  151. package/src/tiles/tile_grid.js +2 -0
  152. package/src/tiles/tile_registry.js +0 -0
  153. package/src/tiling/bottom_panel_tabs.js +67 -99
  154. package/src/tiling/command_palette.js +64 -19
  155. package/src/tiling/content_registry.js +4 -1
  156. package/src/tiling/desktops.js +11 -1
  157. package/src/tiling/entity_sources.js +0 -0
  158. package/src/tiling/keymap.js +403 -128
  159. package/src/tiling/kind_taxonomy.js +78 -0
  160. package/src/tiling/landing_table.js +0 -0
  161. package/src/tiling/loading_overlay.js +0 -0
  162. package/src/tiling/nav_panel.js +59 -10
  163. package/src/tiling/page_factory.js +5 -5
  164. package/src/tiling/panel_availability.js +49 -0
  165. package/src/tiling/panel_keys.js +0 -0
  166. package/src/tiling/shell.js +114 -12
  167. package/src/tiling/tab_strip.js +0 -0
  168. package/src/tiling/tile_breadcrumb.js +0 -0
  169. package/src/tiling/tile_renderer.js +105 -9
  170. package/src/tiling/tile_tab_menu.js +0 -0
  171. package/src/tiling/tile_tree.js +0 -0
  172. package/src/tiling/wm.js +562 -30
  173. package/src/tiling/zoom.js +0 -0
  174. package/src/ui/base/component_base.js +0 -0
  175. package/src/ui/base/controller_base.js +0 -0
  176. package/src/ui/base/page_base.js +0 -0
  177. package/src/ui/components/about_dialog.js +0 -0
  178. package/src/ui/components/action_dropdown.js +30 -2
  179. package/src/ui/components/alert_log.js +0 -0
  180. package/src/ui/components/alerts_pane.js +0 -0
  181. package/src/ui/components/attribute_list_editor.js +0 -0
  182. package/src/ui/components/autocomplete_field.js +0 -0
  183. package/src/ui/components/cell_editor_host.js +293 -0
  184. package/src/ui/components/cell_editors.js +480 -0
  185. package/src/ui/components/cell_grid.js +761 -0
  186. package/src/ui/components/cell_paste.js +243 -0
  187. package/src/ui/components/cell_selection.js +220 -0
  188. package/src/ui/components/choice_controls.js +351 -0
  189. package/src/ui/components/color_field.js +211 -0
  190. package/src/ui/components/color_list_field.js +210 -0
  191. package/src/ui/components/column_fit.js +0 -0
  192. package/src/ui/components/column_groups.js +314 -0
  193. package/src/ui/components/column_order.js +142 -0
  194. package/src/ui/components/computing_status_window.js +98 -25
  195. package/src/ui/components/confirm_dialog.js +81 -38
  196. package/src/ui/components/context_menu.js +2 -2
  197. package/src/ui/components/conversation_view.js +440 -0
  198. package/src/ui/components/crosstab.js +289 -0
  199. package/src/ui/components/data_table.js +696 -71
  200. package/src/ui/components/detail_header.js +22 -11
  201. package/src/ui/components/document_outline.js +397 -0
  202. package/src/ui/components/drag_reorder.js +0 -0
  203. package/src/ui/components/fact_table.js +0 -0
  204. package/src/ui/components/fill_handle.js +322 -0
  205. package/src/ui/components/gallery_picker.js +19 -8
  206. package/src/ui/components/header_actions.js +55 -0
  207. package/src/ui/components/inline_renamer.js +0 -0
  208. package/src/ui/components/job_progress.js +51 -0
  209. package/src/ui/components/managed_window.js +27 -4
  210. package/src/ui/components/menu_bar.js +745 -0
  211. package/src/ui/components/modal.js +449 -75
  212. package/src/ui/components/notification_history.js +3 -3
  213. package/src/ui/components/progress.js +483 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +298 -0
  217. package/src/ui/components/series.js +173 -0
  218. package/src/ui/components/settings_view.js +634 -0
  219. package/src/ui/components/slide_out_panel.js +43 -6
  220. package/src/ui/components/slider_field.js +275 -18
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +361 -0
  223. package/src/ui/components/staged_history.js +172 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +182 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +497 -0
  229. package/src/ui/components/tag_input.js +215 -0
  230. package/src/ui/components/toast.js +25 -8
  231. package/src/ui/components/toggle_field.js +175 -0
  232. package/src/ui/components/tree_view.js +55 -34
  233. package/src/ui/components/tsv.js +74 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +563 -0
  236. package/src/ui/controllers/window_chrome_controller.js +2 -2
  237. package/src/ui/notification_center.js +375 -119
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +709 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +16 -12
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +55 -0
  245. package/src/ui/utils/tooltip_service.js +0 -8
  246. package/tiles.js +27 -0
  247. package/widgets.js +69 -0
  248. package/wm.js +28 -0
  249. package/dist/chunk-6JSOVNID.js.map +0 -7
  250. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  251. package/dist/chunk-FBGP5KN5.js.map +0 -7
  252. package/dist/chunk-FL5KFNQH.js.map +0 -7
  253. package/dist/chunk-NLTOY57M.js.map +0 -7
  254. package/dist/chunk-NNFTETK3.js +0 -633
  255. package/dist/chunk-NNFTETK3.js.map +0 -7
  256. package/dist/chunk-O5OHMWBB.js +0 -284
  257. package/dist/chunk-O5OHMWBB.js.map +0 -7
  258. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  259. package/dist/chunk-SN4LSOAR.js.map +0 -7
  260. package/dist/chunk-W6FBJLPW.js +0 -367
  261. package/dist/chunk-W6FBJLPW.js.map +0 -7
  262. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -19,8 +19,12 @@ const HOOK_REGISTRY = {
19
19
  datasetProxyCache: new WeakMap(),
20
20
  };
21
21
 
22
- const STACK_CACHE = new Map();
22
+ const STACK_CACHE_LIMIT = 500;
23
+ /** `url:line:col` at the end of a frame, in V8 (`at fn (url:1:2)`) and in Firefox/WebKit (`fn@url:1:2`). */
24
+ const FRAME_LOCATION = /(?:\(|@|\bat\s+)([^()\s@][^()\s]*?):\d+(?::\d+)?\)?\s*$/;
23
25
  const DATA_ATTRIBUTE_PREFIX = 'data-';
26
+ /** Set by the DOM hooks below: one hook frame sits between the writer and handle*Mutation. */
27
+ const VIA_DOM_HOOK = Symbol('state-guard:via-dom-hook');
24
28
  const EVENT_CHANNELS = Object.freeze({
25
29
  READY: 'state-guard:ready',
26
30
  VIOLATION: 'state-guard:violation',
@@ -41,13 +45,15 @@ export class StateGuardService {
41
45
  this.mode = this.config.mode;
42
46
  this.allowedAttributes = new Set(this.config.allowedAttributes.map((attr) => attr.toLowerCase()));
43
47
  this.allowedAttributePrefixes = this.config.allowedAttributePrefixes.map((attr) => attr.toLowerCase());
44
- this.allowedDatasetKeys = new Set(this.config.allowedDatasetKeys.map((key) => key.toLowerCase()));
48
+ this.allowedDatasetKeys = new Set(this.config.allowedDatasetKeys.map(normalizeDatasetKey));
45
49
  this.moduleOverrides = (this.config.moduleOverrides ?? []).map((entry) => ({
46
50
  pattern: entry.pattern,
47
51
  attributes: new Set(entry.attributes.map((attr) => attr.toLowerCase())),
48
- datasetKeys: new Set(entry.datasetKeys.map((key) => key.toLowerCase())),
52
+ datasetKeys: new Set(entry.datasetKeys.map(normalizeDatasetKey)),
49
53
  attributePrefixes: entry.attributePrefixes.map((prefix) => prefix.toLowerCase()),
50
54
  }));
55
+ this.moduleFramePattern = this.config.moduleFramePattern;
56
+ this.stackCache = new Map();
51
57
  this.#installHooks();
52
58
  }
53
59
 
@@ -78,7 +84,7 @@ export class StateGuardService {
78
84
  const entry = {
79
85
  pattern,
80
86
  attributes: new Set(attributes.map((attr) => String(attr).toLowerCase())),
81
- datasetKeys: new Set(datasetKeys.map((key) => String(key).toLowerCase())),
87
+ datasetKeys: new Set(datasetKeys.map(normalizeDatasetKey)),
82
88
  attributePrefixes: attributePrefixes.map((prefix) => String(prefix).toLowerCase()),
83
89
  };
84
90
  this.moduleOverrides.push(entry);
@@ -121,7 +127,11 @@ export class StateGuardService {
121
127
  }
122
128
  }
123
129
 
124
- handleAttributeMutation(element, action, attribute, value) {
130
+ /**
131
+ * Check one attribute write. The write is attributed to the caller of this
132
+ * method, or to the caller of the patched DOM API when it came through one.
133
+ */
134
+ handleAttributeMutation(element, action, attribute, value, options = {}) {
125
135
  const attrName = this.#normalizeAttribute(attribute);
126
136
  if (!this.#shouldGuard(attrName)) {
127
137
  return;
@@ -132,15 +142,19 @@ export class StateGuardService {
132
142
  element,
133
143
  value,
134
144
  source: 'attribute',
145
+ stack: new Error().stack,
146
+ writerFrame: options?.[VIA_DOM_HOOK] ? 2 : 1,
135
147
  });
136
148
  }
137
149
 
138
- handleDatasetMutation(element, key, value, { deleting = false } = {}) {
139
- const attrName = this.#datasetKeyToAttribute(key);
150
+ handleDatasetMutation(element, key, value, { deleting = false, [VIA_DOM_HOOK]: viaHook = false } = {}) {
151
+ const attrName = datasetKeyToAttribute(key);
140
152
  if (!this.#shouldGuard(attrName)) {
141
153
  return;
142
154
  }
143
155
  this.#evaluateMutation({
156
+ stack: new Error().stack,
157
+ writerFrame: viaHook ? 2 : 1,
144
158
  action: deleting ? 'dataset:delete' : 'dataset:set',
145
159
  attribute: attrName,
146
160
  element,
@@ -190,8 +204,8 @@ export class StateGuardService {
190
204
  return true;
191
205
  }
192
206
 
193
- #evaluateMutation({ action, attribute, element, value, source }) {
194
- const moduleKey = this.#resolveModuleFromStack();
207
+ #evaluateMutation({ action, attribute, element, value, source, stack, writerFrame }) {
208
+ const moduleKey = this.#resolveModuleFromStack(stack, writerFrame);
195
209
  if (this.#isAllowedForModule(moduleKey, attribute)) {
196
210
  return;
197
211
  }
@@ -246,44 +260,47 @@ export class StateGuardService {
246
260
  return String(attribute).toLowerCase();
247
261
  }
248
262
 
249
- #datasetKeyToAttribute(key) {
250
- if (!key && key !== 0) {
251
- return '';
252
- }
253
- const normalized = String(key)
254
- .replace(/([A-Z])/g, '-$1')
255
- .replace(/[^a-zA-Z0-9_-]/g, '-')
256
- .toLowerCase()
257
- .replace(/-{2,}/g, '-')
258
- .replace(/^-+/, '')
259
- .replace(/-+$/, '');
260
- if (!normalized) {
261
- return '';
262
- }
263
- return `${DATA_ATTRIBUTE_PREFIX}${normalized}`;
264
- }
265
-
266
- #resolveModuleFromStack() {
267
- const stack = new Error().stack;
263
+ /**
264
+ * The module a write is attributed to. `stack` was captured in a
265
+ * handle*Mutation method (frame 0); `writerFrame` is the index of the frame
266
+ * that made the write (1 for a direct call, 2 through a DOM hook). Without a
267
+ * `moduleFramePattern` that writer's location (its URL or path, line and
268
+ * column dropped) is the module. With one, the first frame from the writer
269
+ * up whose location matches is used, and the module is the pattern's first
270
+ * capture group, or that frame's whole location when it has none.
271
+ */
272
+ #resolveModuleFromStack(stack, writerFrame) {
268
273
  if (!stack) {
269
274
  return null;
270
275
  }
271
- if (STACK_CACHE.has(stack)) {
272
- return STACK_CACHE.get(stack);
276
+ const cacheKey = `${writerFrame}\n${stack}`;
277
+ if (this.stackCache.has(cacheKey)) {
278
+ return this.stackCache.get(cacheKey);
273
279
  }
274
- const lines = stack.split('\n');
275
- let modulePath = null;
276
- for (const line of lines) {
277
- const match = line.match(/(js_new\/[^)\s]+)/);
280
+ const locations = [];
281
+ for (const line of String(stack).split('\n')) {
282
+ const match = line.match(FRAME_LOCATION);
278
283
  if (match) {
279
- modulePath = match[1];
280
- break;
284
+ locations.push(match[1]);
285
+ }
286
+ }
287
+ const callers = locations.slice(writerFrame);
288
+ let modulePath = null;
289
+ if (!this.moduleFramePattern) {
290
+ modulePath = callers[0] ?? null;
291
+ } else {
292
+ for (const location of callers) {
293
+ const match = location.match(this.moduleFramePattern);
294
+ if (match) {
295
+ modulePath = match[1] ?? location;
296
+ break;
297
+ }
281
298
  }
282
299
  }
283
- if (STACK_CACHE.size > 500) {
284
- STACK_CACHE.clear();
300
+ if (this.stackCache.size > STACK_CACHE_LIMIT) {
301
+ this.stackCache.clear();
285
302
  }
286
- STACK_CACHE.set(stack, modulePath);
303
+ this.stackCache.set(cacheKey, modulePath);
287
304
  return modulePath;
288
305
  }
289
306
 
@@ -299,6 +316,36 @@ export class StateGuardService {
299
316
  }
300
317
  }
301
318
 
319
+ /**
320
+ * The attribute a dataset key writes: `fooBar` -> `data-foo-bar`, as the DOM
321
+ * maps it. Empty for a key that names nothing.
322
+ */
323
+ function datasetKeyToAttribute(key) {
324
+ if (!key && key !== 0) {
325
+ return '';
326
+ }
327
+ const normalized = String(key)
328
+ .replace(/([A-Z])/g, '-$1')
329
+ .replace(/[^a-zA-Z0-9_-]/g, '-')
330
+ .toLowerCase()
331
+ .replace(/-{2,}/g, '-')
332
+ .replace(/^-+/, '')
333
+ .replace(/-+$/, '');
334
+ if (!normalized) {
335
+ return '';
336
+ }
337
+ return `${DATA_ATTRIBUTE_PREFIX}${normalized}`;
338
+ }
339
+
340
+ /**
341
+ * An allow-list dataset key in the one spelling the guard compares: the
342
+ * attribute's, without `data-`. `fooBar` and `foo-bar` are the same key, so an
343
+ * allow-list may use either.
344
+ */
345
+ function normalizeDatasetKey(key) {
346
+ return datasetKeyToAttribute(key).slice(DATA_ATTRIBUTE_PREFIX.length);
347
+ }
348
+
302
349
  function installDomHooks(service) {
303
350
  if (HOOK_REGISTRY.installed) {
304
351
  HOOK_REGISTRY.service = service;
@@ -316,21 +363,13 @@ function installDomHooks(service) {
316
363
 
317
364
  Element.prototype.setAttribute = function patchedSetAttribute(name, value) {
318
365
  const guard = HOOK_REGISTRY.service;
319
- try {
320
- guard?.handleAttributeMutation(this, 'setAttribute', name, value);
321
- } catch (error) {
322
- throw error;
323
- }
366
+ guard?.handleAttributeMutation(this, 'setAttribute', name, value, { [VIA_DOM_HOOK]: true });
324
367
  return nativeSetAttribute.call(this, name, value);
325
368
  };
326
369
 
327
370
  Element.prototype.removeAttribute = function patchedRemoveAttribute(name) {
328
371
  const guard = HOOK_REGISTRY.service;
329
- try {
330
- guard?.handleAttributeMutation(this, 'removeAttribute', name, undefined);
331
- } catch (error) {
332
- throw error;
333
- }
372
+ guard?.handleAttributeMutation(this, 'removeAttribute', name, undefined, { [VIA_DOM_HOOK]: true });
334
373
  return nativeRemoveAttribute.call(this, name);
335
374
  };
336
375
 
@@ -362,7 +401,7 @@ function wrapDataset(service) {
362
401
  const guard = HOOK_REGISTRY.service;
363
402
  const key = typeof prop === 'string' ? prop : null;
364
403
  if (guard && key) {
365
- guard.handleDatasetMutation(proxyState.element, key, value);
404
+ guard.handleDatasetMutation(proxyState.element, key, value, { [VIA_DOM_HOOK]: true });
366
405
  }
367
406
  return Reflect.set(target, prop, value);
368
407
  },
@@ -370,7 +409,10 @@ function wrapDataset(service) {
370
409
  const guard = HOOK_REGISTRY.service;
371
410
  const key = typeof prop === 'string' ? prop : null;
372
411
  if (guard && key) {
373
- guard.handleDatasetMutation(proxyState.element, key, undefined, { deleting: true });
412
+ guard.handleDatasetMutation(proxyState.element, key, undefined, {
413
+ deleting: true,
414
+ [VIA_DOM_HOOK]: true,
415
+ });
374
416
  }
375
417
  return Reflect.deleteProperty(target, prop);
376
418
  },
@@ -16,7 +16,6 @@ const DEFAULT_ALLOWED_ATTRIBUTES = Object.freeze([
16
16
  'aria-busy',
17
17
  'role',
18
18
  'title',
19
- 'twm-has-tooltip',
20
19
  'data-tooltip',
21
20
  'data-help-topic',
22
21
  ]);
@@ -31,7 +30,6 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
31
30
  'deleting',
32
31
  // Notification center
33
32
  'notification-id',
34
- 'notificationId',
35
33
  // Plotly.js internal attributes (third-party charting library)
36
34
  'unformatted',
37
35
  'toggle',
@@ -46,56 +44,37 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
46
44
  'group',
47
45
  'trace',
48
46
  'point',
49
- 'curveNumber',
50
- 'pointNumber',
47
+ 'curve-number',
48
+ 'point-number',
51
49
  // UI state keys used by original HTML structure
52
50
  'active-content',
53
- 'activeContent',
54
51
  'panel-content',
55
- 'panelContent',
56
52
  'collapsible-id',
57
- 'collapsibleId',
58
53
  'collapsible-default',
59
- 'collapsibleDefault',
60
- 'twm-collapsible-header',
61
- 'collapsibleHeader',
62
- 'twm-collapsible-content',
63
- 'collapsibleContent',
54
+ 'collapsible-header',
55
+ 'collapsible-content',
64
56
  'collapsible-group',
65
- 'collapsibleGroup',
66
57
  'collapsible-exclusive',
67
- 'collapsibleExclusive',
68
58
  'collapsible-float',
69
- 'collapsibleFloat',
70
59
  'collapsible-fill',
71
- 'collapsibleFill',
72
60
  'tool',
73
61
  'tab',
74
62
  'role',
75
63
  'op',
76
64
  'func-name',
77
- 'funcName',
78
65
  'func-expr',
79
- 'funcExpr',
80
66
  'arity',
81
67
  'tooltip',
82
- 'twm-latex-tooltip',
83
- 'latexTooltip',
68
+ 'latex-tooltip',
84
69
  'node-type',
85
- 'nodeType',
86
- 'twm-node-name',
87
- 'nodeName',
70
+ 'node-name',
88
71
  'lang',
89
72
  'playback-control',
90
- 'playbackControl',
91
73
  // Simulation/scenario state keys
92
74
  'status',
93
75
  'scenario-id',
94
- 'scenarioId',
95
76
  'run-id',
96
- 'runId',
97
77
  'run-state',
98
- 'runState',
99
78
  'selected',
100
79
  'active',
101
80
  'disabled',
@@ -105,53 +84,33 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
105
84
  'hidden',
106
85
  // Node/canvas state keys
107
86
  'node-id',
108
- 'nodeId',
109
87
  'connector-id',
110
- 'connectorId',
111
88
  'connector-type',
112
- 'connectorType',
113
89
  'connection-id',
114
- 'connectionId',
115
90
  'input-index',
116
- 'inputIndex',
117
91
  'output-index',
118
- 'outputIndex',
119
92
  'port-index',
120
- 'portIndex',
121
93
  // Scenario editor keys
122
94
  'source',
123
95
  'series-id',
124
- 'seriesId',
125
96
  'dataset-id',
126
- 'datasetId',
127
97
  'mirror-input-bound',
128
- 'mirrorInputBound',
129
98
  'mirror-change-bound',
130
- 'mirrorChangeBound',
131
99
  'mirror-input',
132
- 'mirrorInput',
133
100
  'bound',
134
101
  'field',
135
102
  'key',
136
103
  'namespace',
137
104
  'namespace-id',
138
- 'namespaceId',
139
105
  'tab-id',
140
- 'tabId',
141
106
  'param-id',
142
- 'paramId',
143
107
  'target-id',
144
- 'targetId',
145
108
  'override-type',
146
- 'overrideType',
147
109
  // Tree view component keys
148
110
  'item-id',
149
- 'itemId',
150
111
  'category-id',
151
- 'categoryId',
152
112
  // Calibration page keys
153
113
  'info-for',
154
- 'infoFor',
155
114
  'column',
156
115
  // Data page keys
157
116
  'readonly',
@@ -174,53 +133,102 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
174
133
  'description',
175
134
  // DSL generator keys
176
135
  'dsl-state',
177
- 'dslState',
178
136
  'dsl-section-registered',
179
- 'dslSectionRegistered',
180
137
  'dsl-scrollbars-installed',
181
- 'dslScrollbarsInstalled',
182
138
  'dsl-tabs-bound',
183
- 'dslTabsBound',
184
139
  'dsl-controls-bound',
185
- 'dslControlsBound',
186
140
  // Inline highlighter keys
187
141
  'inline-hl',
188
- 'inlineHl',
189
- 'tooltip',
190
142
  // Overlay scrollbar keys (UI-only, not state storage)
191
143
  'scrollable',
192
144
  'scrollable-x',
193
- 'scrollableX',
194
145
  'scrollable-y',
195
- 'scrollableY',
196
146
  // ManagedWindow component keys
197
147
  'window-id',
198
- 'windowId',
199
148
  // Help system keys
200
149
  'help-topic',
201
- 'helpTopic',
202
- ]);
203
-
204
- const DEFAULT_OVERRIDES = Object.freeze([
205
- {
206
- // Panel controller writes dataset hooks for field identification/focus.
207
- pattern: /js_new\/ui\/controllers\/panel_controller\.js/,
208
- datasetKeys: ['field', 'descriptor-key', 'descriptorKey'],
209
- attributes: ['data-field', 'data-descriptor-key'],
210
- attributePrefixes: [],
211
- },
150
+ // FlexDesk widgets: every literal data key written under src/ is listed
151
+ // here (tests/state_guard_defaults.test.mjs scans for them), so the
152
+ // shell, dialogs, forms, tooltips and editors pass a strict guard.
153
+ 'accent',
154
+ 'act',
155
+ 'action-idx',
156
+ 'action-value',
157
+ 'add',
158
+ 'category',
159
+ 'code',
160
+ 'data',
161
+ 'direction',
162
+ 'dismiss',
163
+ 'entry',
164
+ 'error',
165
+ 'extra',
166
+ 'flow',
167
+ 'from',
168
+ 'gap',
169
+ 'group-id',
170
+ 'item',
171
+ 'kind',
172
+ 'lane',
173
+ 'leaf-id',
174
+ 'level',
175
+ 'list-action',
176
+ 'match',
177
+ 'mode',
178
+ 'node',
179
+ 'note-id',
180
+ 'notification-summary',
181
+ 'open',
182
+ 'pager',
183
+ 'pane',
184
+ 'pane-id',
185
+ 'parameter',
186
+ 'placeholder',
187
+ 'placement',
188
+ 'port',
189
+ 'record-key',
190
+ 'ref',
191
+ 'refusal',
192
+ 'refusal-for',
193
+ 'renameable',
194
+ 'row-key',
195
+ 'rule',
196
+ 'scope',
197
+ 'shape',
198
+ 'slot',
199
+ 'slot-idx',
200
+ 'split-id',
201
+ 'state',
202
+ 'step',
203
+ 'syntax',
204
+ 'tile-id',
205
+ 'tile-type',
206
+ 'tip',
207
+ 'to',
208
+ 'tone',
209
+ 'tooltip-max-width',
210
+ 'tooltip-placement',
211
+ 'twm-flow-item',
212
+ 'twm-flow-title',
213
+ 'twm-icon',
214
+ 'twm-required',
215
+ 'twm-tabs',
216
+ 'twm-tail',
217
+ 'verb',
212
218
  ]);
213
219
 
214
- const DEFAULT_MODE = typeof window !== 'undefined' && Boolean(window.__ECOSIM_DEBUG__)
215
- ? 'strict'
216
- : 'warn';
217
-
218
220
  export const DEFAULT_STATE_GUARD_CONFIG = Object.freeze({
219
- mode: DEFAULT_MODE,
221
+ mode: 'warn',
220
222
  allowedAttributes: DEFAULT_ALLOWED_ATTRIBUTES,
221
223
  allowedAttributePrefixes: DEFAULT_ALLOWED_ATTRIBUTE_PREFIXES,
222
224
  allowedDatasetKeys: DEFAULT_ALLOWED_DATASET_KEYS,
223
- moduleOverrides: DEFAULT_OVERRIDES,
225
+ // FlexDesk ships no module overrides: which of YOUR modules may write which
226
+ // keys is yours to say, through `moduleOverrides` or `allow()`.
227
+ moduleOverrides: Object.freeze([]),
228
+ // null: a write is attributed to the frame that made it. A RegExp (or a
229
+ // literal string) picks the first frame from there up whose location
230
+ // matches instead; its first capture group, if any, names the module.
231
+ moduleFramePattern: null,
224
232
  });
225
233
 
226
234
  export function normalizeStateGuardConfig(config = {}) {
@@ -241,6 +249,9 @@ export function normalizeStateGuardConfig(config = {}) {
241
249
  moduleOverrides: normalizeOverrides(
242
250
  config.moduleOverrides ?? DEFAULT_STATE_GUARD_CONFIG.moduleOverrides,
243
251
  ),
252
+ moduleFramePattern: config.moduleFramePattern == null || config.moduleFramePattern === ''
253
+ ? DEFAULT_STATE_GUARD_CONFIG.moduleFramePattern
254
+ : patternToRegExp(config.moduleFramePattern),
244
255
  };
245
256
  }
246
257
 
File without changes
@@ -21,7 +21,7 @@
21
21
  * └──────────────────────────────┘
22
22
  */
23
23
 
24
- import { NotebookTabBar } from './notebook_tab_bar.js';
24
+ import { NotebookTabBar, getTabDragMime } from './notebook_tab_bar.js';
25
25
 
26
26
  export class EditorPane {
27
27
  /** @type {string} */
@@ -141,7 +141,7 @@ export class EditorPane {
141
141
  if (this.#isExternalTabDrag(e)) {
142
142
  e.preventDefault();
143
143
  this.#editorContainer.classList.remove('notebook-editor-container--drop-target');
144
- const filePath = e.dataTransfer.getData('application/x-ecosim-tab')
144
+ const filePath = e.dataTransfer.getData(getTabDragMime())
145
145
  || e.dataTransfer.getData('text/plain');
146
146
  if (filePath) {
147
147
  this.#callbacks.onDropFromOtherPane?.(this.#id, filePath);
@@ -248,7 +248,7 @@ export class EditorPane {
248
248
 
249
249
  /** Check if a drag event is carrying a tab from another pane. */
250
250
  #isExternalTabDrag(e) {
251
- try { return e.dataTransfer.types.includes('application/x-ecosim-tab'); } catch { return false; }
251
+ try { return e.dataTransfer.types.includes(getTabDragMime()); } catch { return false; }
252
252
  }
253
253
 
254
254
  dispose() {