flexdesk 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. package/src/ui/utils/tooltip_service.js +406 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * StateGuardService
3
+ * ------------------
4
+ * Runtime enforcement layer that blocks storing application state inside DOM attributes.
5
+ * Hooks Element attribute/dataset APIs and emits violations through the event bus.
6
+ */
7
+
8
+ import { DEFAULT_STATE_GUARD_CONFIG, normalizeStateGuardConfig } from './state_guard_config.js';
9
+
10
+ const HOOK_REGISTRY = {
11
+ installed: false,
12
+ service: null,
13
+ originals: {
14
+ setAttribute: null,
15
+ removeAttribute: null,
16
+ getAttribute: null,
17
+ },
18
+ datasetDescriptor: null,
19
+ datasetProxyCache: new WeakMap(),
20
+ };
21
+
22
+ const STACK_CACHE = new Map();
23
+ const DATA_ATTRIBUTE_PREFIX = 'data-';
24
+ const EVENT_CHANNELS = Object.freeze({
25
+ READY: 'state-guard:ready',
26
+ VIOLATION: 'state-guard:violation',
27
+ MODE_CHANGED: 'state-guard:mode-changed',
28
+ });
29
+
30
+ export class StateGuardService {
31
+ constructor({
32
+ eventBus = null,
33
+ logger = null,
34
+ config = DEFAULT_STATE_GUARD_CONFIG,
35
+ } = {}) {
36
+ this.eventBus = eventBus;
37
+ this.logger = logger;
38
+ this.enabled = true;
39
+ this.bypassDepth = 0;
40
+ this.config = normalizeStateGuardConfig(config);
41
+ this.mode = this.config.mode;
42
+ this.allowedAttributes = new Set(this.config.allowedAttributes.map((attr) => attr.toLowerCase()));
43
+ this.allowedAttributePrefixes = this.config.allowedAttributePrefixes.map((attr) => attr.toLowerCase());
44
+ this.allowedDatasetKeys = new Set(this.config.allowedDatasetKeys.map((key) => key.toLowerCase()));
45
+ this.moduleOverrides = (this.config.moduleOverrides ?? []).map((entry) => ({
46
+ pattern: entry.pattern,
47
+ attributes: new Set(entry.attributes.map((attr) => attr.toLowerCase())),
48
+ datasetKeys: new Set(entry.datasetKeys.map((key) => key.toLowerCase())),
49
+ attributePrefixes: entry.attributePrefixes.map((prefix) => prefix.toLowerCase()),
50
+ }));
51
+ this.#installHooks();
52
+ }
53
+
54
+ setMode(mode) {
55
+ const normalized = typeof mode === 'string' ? mode.toLowerCase() : '';
56
+ if (!['off', 'warn', 'strict'].includes(normalized)) {
57
+ throw new Error(`Invalid state guard mode "${mode}"`);
58
+ }
59
+ if (this.mode === normalized) {
60
+ return this.mode;
61
+ }
62
+ this.mode = normalized;
63
+ this.eventBus?.emit?.(EVENT_CHANNELS.MODE_CHANGED, {
64
+ mode: this.mode,
65
+ timestamp: Date.now(),
66
+ });
67
+ return this.mode;
68
+ }
69
+
70
+ getMode() {
71
+ return this.mode;
72
+ }
73
+
74
+ allow(modulePattern, attributes = [], { datasetKeys = [], attributePrefixes = [] } = {}) {
75
+ const pattern = modulePattern instanceof RegExp
76
+ ? modulePattern
77
+ : new RegExp(String(modulePattern ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
78
+ const entry = {
79
+ pattern,
80
+ attributes: new Set(attributes.map((attr) => String(attr).toLowerCase())),
81
+ datasetKeys: new Set(datasetKeys.map((key) => String(key).toLowerCase())),
82
+ attributePrefixes: attributePrefixes.map((prefix) => String(prefix).toLowerCase()),
83
+ };
84
+ this.moduleOverrides.push(entry);
85
+ return () => {
86
+ const idx = this.moduleOverrides.indexOf(entry);
87
+ if (idx >= 0) {
88
+ this.moduleOverrides.splice(idx, 1);
89
+ }
90
+ };
91
+ }
92
+
93
+ executeWithBypass(label, fn) {
94
+ if (typeof fn !== 'function') {
95
+ throw new TypeError('executeWithBypass requires a function');
96
+ }
97
+ try {
98
+ this.bypassDepth += 1;
99
+ return fn();
100
+ } finally {
101
+ this.bypassDepth = Math.max(0, this.bypassDepth - 1);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Execute an async function with state guard bypass.
107
+ * The bypass remains active for the entire duration of the async operation.
108
+ * @param {string} label - Label for debugging
109
+ * @param {Function} fn - Async function to execute
110
+ * @returns {Promise<*>} Result of the function
111
+ */
112
+ async executeWithBypassAsync(label, fn) {
113
+ if (typeof fn !== 'function') {
114
+ throw new TypeError('executeWithBypassAsync requires a function');
115
+ }
116
+ try {
117
+ this.bypassDepth += 1;
118
+ return await fn();
119
+ } finally {
120
+ this.bypassDepth = Math.max(0, this.bypassDepth - 1);
121
+ }
122
+ }
123
+
124
+ handleAttributeMutation(element, action, attribute, value) {
125
+ const attrName = this.#normalizeAttribute(attribute);
126
+ if (!this.#shouldGuard(attrName)) {
127
+ return;
128
+ }
129
+ this.#evaluateMutation({
130
+ action,
131
+ attribute: attrName,
132
+ element,
133
+ value,
134
+ source: 'attribute',
135
+ });
136
+ }
137
+
138
+ handleDatasetMutation(element, key, value, { deleting = false } = {}) {
139
+ const attrName = this.#datasetKeyToAttribute(key);
140
+ if (!this.#shouldGuard(attrName)) {
141
+ return;
142
+ }
143
+ this.#evaluateMutation({
144
+ action: deleting ? 'dataset:delete' : 'dataset:set',
145
+ attribute: attrName,
146
+ element,
147
+ value,
148
+ source: 'dataset',
149
+ });
150
+ }
151
+
152
+ #installHooks() {
153
+ if (typeof Element === 'undefined' || typeof HTMLElement === 'undefined') {
154
+ this.enabled = false;
155
+ this.logger?.warn?.('state-guard', 'DOM environment unavailable; guard disabled');
156
+ return;
157
+ }
158
+ installDomHooks(this);
159
+ this.eventBus?.emit?.(EVENT_CHANNELS.READY, {
160
+ mode: this.mode,
161
+ timestamp: Date.now(),
162
+ });
163
+ }
164
+
165
+ #shouldGuard(attribute) {
166
+ if (!this.enabled) {
167
+ return false;
168
+ }
169
+ if (this.mode === 'off') {
170
+ return false;
171
+ }
172
+ if (this.bypassDepth > 0) {
173
+ return false;
174
+ }
175
+ if (!attribute || !attribute.startsWith(DATA_ATTRIBUTE_PREFIX)) {
176
+ return false;
177
+ }
178
+ if (this.allowedAttributes.has(attribute)) {
179
+ return false;
180
+ }
181
+ if (attribute.startsWith(DATA_ATTRIBUTE_PREFIX)) {
182
+ const datasetKey = attribute.slice(DATA_ATTRIBUTE_PREFIX.length);
183
+ if (this.allowedDatasetKeys.has(datasetKey)) {
184
+ return false;
185
+ }
186
+ }
187
+ if (this.allowedAttributePrefixes.some((prefix) => attribute.startsWith(prefix))) {
188
+ return false;
189
+ }
190
+ return true;
191
+ }
192
+
193
+ #evaluateMutation({ action, attribute, element, value, source }) {
194
+ const moduleKey = this.#resolveModuleFromStack();
195
+ if (this.#isAllowedForModule(moduleKey, attribute)) {
196
+ return;
197
+ }
198
+ const payload = {
199
+ attribute,
200
+ action,
201
+ source,
202
+ module: moduleKey,
203
+ mode: this.mode,
204
+ timestamp: Date.now(),
205
+ };
206
+ this.logger?.warn?.('state-guard', 'Blocked DOM state mutation', {
207
+ ...payload,
208
+ valuePreview: this.#previewValue(value),
209
+ });
210
+ this.eventBus?.emit?.(EVENT_CHANNELS.VIOLATION, payload);
211
+ if (this.mode === 'strict') {
212
+ const label = moduleKey ? `${moduleKey}` : 'unknown module';
213
+ throw new Error(`StateGuard blocked ${action} on "${attribute}" from ${label}`);
214
+ }
215
+ }
216
+
217
+ #isAllowedForModule(moduleKey, attribute) {
218
+ if (!moduleKey || !this.moduleOverrides.length) {
219
+ return false;
220
+ }
221
+ const lowerAttr = attribute.toLowerCase();
222
+ for (const entry of this.moduleOverrides) {
223
+ if (!entry.pattern.test(moduleKey)) {
224
+ continue;
225
+ }
226
+ if (entry.attributes.has(lowerAttr)) {
227
+ return true;
228
+ }
229
+ if (entry.attributePrefixes.some((prefix) => lowerAttr.startsWith(prefix))) {
230
+ return true;
231
+ }
232
+ if (lowerAttr.startsWith(DATA_ATTRIBUTE_PREFIX)) {
233
+ const datasetKey = lowerAttr.slice(DATA_ATTRIBUTE_PREFIX.length);
234
+ if (entry.datasetKeys.has(datasetKey)) {
235
+ return true;
236
+ }
237
+ }
238
+ }
239
+ return false;
240
+ }
241
+
242
+ #normalizeAttribute(attribute) {
243
+ if (!attribute) {
244
+ return '';
245
+ }
246
+ return String(attribute).toLowerCase();
247
+ }
248
+
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;
268
+ if (!stack) {
269
+ return null;
270
+ }
271
+ if (STACK_CACHE.has(stack)) {
272
+ return STACK_CACHE.get(stack);
273
+ }
274
+ const lines = stack.split('\n');
275
+ let modulePath = null;
276
+ for (const line of lines) {
277
+ const match = line.match(/(js_new\/[^)\s]+)/);
278
+ if (match) {
279
+ modulePath = match[1];
280
+ break;
281
+ }
282
+ }
283
+ if (STACK_CACHE.size > 500) {
284
+ STACK_CACHE.clear();
285
+ }
286
+ STACK_CACHE.set(stack, modulePath);
287
+ return modulePath;
288
+ }
289
+
290
+ #previewValue(value) {
291
+ if (value === null || value === undefined) {
292
+ return String(value);
293
+ }
294
+ if (typeof value === 'object') {
295
+ return '[object]';
296
+ }
297
+ const str = String(value);
298
+ return str.length > 50 ? `${str.slice(0, 47)}...` : str;
299
+ }
300
+ }
301
+
302
+ function installDomHooks(service) {
303
+ if (HOOK_REGISTRY.installed) {
304
+ HOOK_REGISTRY.service = service;
305
+ return;
306
+ }
307
+ HOOK_REGISTRY.installed = true;
308
+ HOOK_REGISTRY.service = service;
309
+
310
+ const { setAttribute, removeAttribute } = Element.prototype;
311
+ const nativeSetAttribute = setAttribute;
312
+ const nativeRemoveAttribute = removeAttribute;
313
+
314
+ HOOK_REGISTRY.originals.setAttribute = nativeSetAttribute;
315
+ HOOK_REGISTRY.originals.removeAttribute = nativeRemoveAttribute;
316
+
317
+ Element.prototype.setAttribute = function patchedSetAttribute(name, value) {
318
+ const guard = HOOK_REGISTRY.service;
319
+ try {
320
+ guard?.handleAttributeMutation(this, 'setAttribute', name, value);
321
+ } catch (error) {
322
+ throw error;
323
+ }
324
+ return nativeSetAttribute.call(this, name, value);
325
+ };
326
+
327
+ Element.prototype.removeAttribute = function patchedRemoveAttribute(name) {
328
+ const guard = HOOK_REGISTRY.service;
329
+ try {
330
+ guard?.handleAttributeMutation(this, 'removeAttribute', name, undefined);
331
+ } catch (error) {
332
+ throw error;
333
+ }
334
+ return nativeRemoveAttribute.call(this, name);
335
+ };
336
+
337
+ wrapDataset(service);
338
+ }
339
+
340
+ function wrapDataset(service) {
341
+ const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'dataset');
342
+ if (!descriptor || typeof descriptor.get !== 'function') {
343
+ return;
344
+ }
345
+ HOOK_REGISTRY.datasetDescriptor = descriptor;
346
+ Object.defineProperty(HTMLElement.prototype, 'dataset', {
347
+ configurable: true,
348
+ enumerable: descriptor.enumerable,
349
+ get: function datasetGetter() {
350
+ const nativeDataset = descriptor.get.call(this);
351
+ if (!nativeDataset || typeof nativeDataset !== 'object') {
352
+ return nativeDataset;
353
+ }
354
+ const cached = HOOK_REGISTRY.datasetProxyCache.get(nativeDataset);
355
+ if (cached && cached.proxy) {
356
+ cached.element = this;
357
+ return cached.proxy;
358
+ }
359
+ const proxyState = { element: this };
360
+ const proxy = new Proxy(nativeDataset, {
361
+ set(target, prop, value) {
362
+ const guard = HOOK_REGISTRY.service;
363
+ const key = typeof prop === 'string' ? prop : null;
364
+ if (guard && key) {
365
+ guard.handleDatasetMutation(proxyState.element, key, value);
366
+ }
367
+ return Reflect.set(target, prop, value);
368
+ },
369
+ deleteProperty(target, prop) {
370
+ const guard = HOOK_REGISTRY.service;
371
+ const key = typeof prop === 'string' ? prop : null;
372
+ if (guard && key) {
373
+ guard.handleDatasetMutation(proxyState.element, key, undefined, { deleting: true });
374
+ }
375
+ return Reflect.deleteProperty(target, prop);
376
+ },
377
+ });
378
+ HOOK_REGISTRY.datasetProxyCache.set(nativeDataset, { proxy, element: this });
379
+ return proxy;
380
+ },
381
+ });
382
+ }
@@ -0,0 +1,309 @@
1
+ const DEFAULT_ALLOWED_ATTRIBUTES = Object.freeze([
2
+ 'data-testid',
3
+ 'data-field',
4
+ 'data-descriptor-key',
5
+ 'aria-label',
6
+ 'aria-labelledby',
7
+ 'aria-describedby',
8
+ 'aria-hidden',
9
+ 'aria-expanded',
10
+ 'aria-selected',
11
+ 'aria-disabled',
12
+ 'aria-checked',
13
+ 'aria-controls',
14
+ 'aria-haspopup',
15
+ 'aria-live',
16
+ 'aria-busy',
17
+ 'role',
18
+ 'title',
19
+ 'twm-has-tooltip',
20
+ 'data-tooltip',
21
+ 'data-help-topic',
22
+ ]);
23
+
24
+ const DEFAULT_ALLOWED_ATTRIBUTE_PREFIXES = Object.freeze([
25
+ ]);
26
+
27
+ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
28
+ 'testid',
29
+ 'phase',
30
+ 'page-key',
31
+ 'deleting',
32
+ // Notification center
33
+ 'notification-id',
34
+ 'notificationId',
35
+ // Plotly.js internal attributes (third-party charting library)
36
+ 'unformatted',
37
+ 'toggle',
38
+ 'notex',
39
+ 'math',
40
+ 'originalindex',
41
+ 'original-index',
42
+ 'gravity',
43
+ 'attr',
44
+ 'val',
45
+ 'subplot',
46
+ 'group',
47
+ 'trace',
48
+ 'point',
49
+ 'curveNumber',
50
+ 'pointNumber',
51
+ // UI state keys used by original HTML structure
52
+ 'active-content',
53
+ 'activeContent',
54
+ 'panel-content',
55
+ 'panelContent',
56
+ 'collapsible-id',
57
+ 'collapsibleId',
58
+ 'collapsible-default',
59
+ 'collapsibleDefault',
60
+ 'twm-collapsible-header',
61
+ 'collapsibleHeader',
62
+ 'twm-collapsible-content',
63
+ 'collapsibleContent',
64
+ 'collapsible-group',
65
+ 'collapsibleGroup',
66
+ 'collapsible-exclusive',
67
+ 'collapsibleExclusive',
68
+ 'collapsible-float',
69
+ 'collapsibleFloat',
70
+ 'collapsible-fill',
71
+ 'collapsibleFill',
72
+ 'tool',
73
+ 'tab',
74
+ 'role',
75
+ 'op',
76
+ 'func-name',
77
+ 'funcName',
78
+ 'func-expr',
79
+ 'funcExpr',
80
+ 'arity',
81
+ 'tooltip',
82
+ 'twm-latex-tooltip',
83
+ 'latexTooltip',
84
+ 'node-type',
85
+ 'nodeType',
86
+ 'twm-node-name',
87
+ 'nodeName',
88
+ 'lang',
89
+ 'playback-control',
90
+ 'playbackControl',
91
+ // Simulation/scenario state keys
92
+ 'status',
93
+ 'scenario-id',
94
+ 'scenarioId',
95
+ 'run-id',
96
+ 'runId',
97
+ 'run-state',
98
+ 'runState',
99
+ 'selected',
100
+ 'active',
101
+ 'disabled',
102
+ 'expanded',
103
+ 'collapsed',
104
+ 'visible',
105
+ 'hidden',
106
+ // Node/canvas state keys
107
+ 'node-id',
108
+ 'nodeId',
109
+ 'connector-id',
110
+ 'connectorId',
111
+ 'connector-type',
112
+ 'connectorType',
113
+ 'connection-id',
114
+ 'connectionId',
115
+ 'input-index',
116
+ 'inputIndex',
117
+ 'output-index',
118
+ 'outputIndex',
119
+ 'port-index',
120
+ 'portIndex',
121
+ // Scenario editor keys
122
+ 'source',
123
+ 'series-id',
124
+ 'seriesId',
125
+ 'dataset-id',
126
+ 'datasetId',
127
+ 'mirror-input-bound',
128
+ 'mirrorInputBound',
129
+ 'mirror-change-bound',
130
+ 'mirrorChangeBound',
131
+ 'mirror-input',
132
+ 'mirrorInput',
133
+ 'bound',
134
+ 'field',
135
+ 'key',
136
+ 'namespace',
137
+ 'namespace-id',
138
+ 'namespaceId',
139
+ 'tab-id',
140
+ 'tabId',
141
+ 'param-id',
142
+ 'paramId',
143
+ 'target-id',
144
+ 'targetId',
145
+ 'override-type',
146
+ 'overrideType',
147
+ // Tree view component keys
148
+ 'item-id',
149
+ 'itemId',
150
+ 'category-id',
151
+ 'categoryId',
152
+ // Calibration page keys
153
+ 'info-for',
154
+ 'infoFor',
155
+ 'column',
156
+ // Data page keys
157
+ 'readonly',
158
+ 'id',
159
+ 'action',
160
+ 'type',
161
+ 'name',
162
+ 'path',
163
+ 'index',
164
+ 'count',
165
+ 'total',
166
+ 'page',
167
+ 'sort',
168
+ 'order',
169
+ 'filter',
170
+ 'search',
171
+ 'value',
172
+ 'label',
173
+ 'title',
174
+ 'description',
175
+ // DSL generator keys
176
+ 'dsl-state',
177
+ 'dslState',
178
+ 'dsl-section-registered',
179
+ 'dslSectionRegistered',
180
+ 'dsl-scrollbars-installed',
181
+ 'dslScrollbarsInstalled',
182
+ 'dsl-tabs-bound',
183
+ 'dslTabsBound',
184
+ 'dsl-controls-bound',
185
+ 'dslControlsBound',
186
+ // Inline highlighter keys
187
+ 'inline-hl',
188
+ 'inlineHl',
189
+ 'tooltip',
190
+ // Overlay scrollbar keys (UI-only, not state storage)
191
+ 'scrollable',
192
+ 'scrollable-x',
193
+ 'scrollableX',
194
+ 'scrollable-y',
195
+ 'scrollableY',
196
+ // ManagedWindow component keys
197
+ 'window-id',
198
+ 'windowId',
199
+ // Help system keys
200
+ '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
+ },
212
+ ]);
213
+
214
+ const DEFAULT_MODE = typeof window !== 'undefined' && Boolean(window.__ECOSIM_DEBUG__)
215
+ ? 'strict'
216
+ : 'warn';
217
+
218
+ export const DEFAULT_STATE_GUARD_CONFIG = Object.freeze({
219
+ mode: DEFAULT_MODE,
220
+ allowedAttributes: DEFAULT_ALLOWED_ATTRIBUTES,
221
+ allowedAttributePrefixes: DEFAULT_ALLOWED_ATTRIBUTE_PREFIXES,
222
+ allowedDatasetKeys: DEFAULT_ALLOWED_DATASET_KEYS,
223
+ moduleOverrides: DEFAULT_OVERRIDES,
224
+ });
225
+
226
+ export function normalizeStateGuardConfig(config = {}) {
227
+ if (!config || typeof config !== 'object') {
228
+ return { ...DEFAULT_STATE_GUARD_CONFIG };
229
+ }
230
+ return {
231
+ mode: normalizeMode(config.mode ?? DEFAULT_STATE_GUARD_CONFIG.mode),
232
+ allowedAttributes: normalizeStringList(
233
+ config.allowedAttributes ?? DEFAULT_STATE_GUARD_CONFIG.allowedAttributes,
234
+ ),
235
+ allowedAttributePrefixes: normalizeStringList(
236
+ config.allowedAttributePrefixes ?? DEFAULT_STATE_GUARD_CONFIG.allowedAttributePrefixes,
237
+ ),
238
+ allowedDatasetKeys: normalizeStringList(
239
+ config.allowedDatasetKeys ?? DEFAULT_STATE_GUARD_CONFIG.allowedDatasetKeys,
240
+ ),
241
+ moduleOverrides: normalizeOverrides(
242
+ config.moduleOverrides ?? DEFAULT_STATE_GUARD_CONFIG.moduleOverrides,
243
+ ),
244
+ };
245
+ }
246
+
247
+ function normalizeMode(mode) {
248
+ const normalized = typeof mode === 'string' ? mode.toLowerCase() : '';
249
+ return normalized === 'off' || normalized === 'warn' || normalized === 'strict'
250
+ ? normalized
251
+ : DEFAULT_STATE_GUARD_CONFIG.mode;
252
+ }
253
+
254
+ function normalizeStringList(value) {
255
+ if (!value) {
256
+ return [];
257
+ }
258
+ if (Array.isArray(value)) {
259
+ return value
260
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
261
+ .filter(Boolean);
262
+ }
263
+ if (typeof value === 'string') {
264
+ return value
265
+ .split(',')
266
+ .map((entry) => entry.trim())
267
+ .filter(Boolean);
268
+ }
269
+ return [];
270
+ }
271
+
272
+ function normalizeOverrides(overrides) {
273
+ if (!Array.isArray(overrides)) {
274
+ return [];
275
+ }
276
+ return overrides
277
+ .map((entry) => normalizeOverride(entry))
278
+ .filter(Boolean);
279
+ }
280
+
281
+ function normalizeOverride(entry) {
282
+ if (!entry || typeof entry !== 'object') {
283
+ return null;
284
+ }
285
+ if (!entry.pattern) {
286
+ return null;
287
+ }
288
+ const attributes = normalizeStringList(entry.attributes);
289
+ const datasetKeys = normalizeStringList(entry.datasetKeys);
290
+ const prefixes = normalizeStringList(entry.attributePrefixes);
291
+ if (!attributes.length && !datasetKeys.length && !prefixes.length) {
292
+ return null;
293
+ }
294
+ return {
295
+ pattern: patternToRegExp(entry.pattern),
296
+ attributes,
297
+ datasetKeys,
298
+ attributePrefixes: prefixes,
299
+ };
300
+ }
301
+
302
+ function patternToRegExp(pattern) {
303
+ if (pattern instanceof RegExp) {
304
+ return pattern;
305
+ }
306
+ const source = String(pattern ?? '')
307
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
308
+ return new RegExp(source);
309
+ }