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,206 @@
1
+ /**
2
+ * series_transforms.js — derived series an embedder supplies: a trend, a moving
3
+ * average, a decomposition. @flexdesk/charts ships none of its own.
4
+ *
5
+ * A transform is a description plus an async function:
6
+ *
7
+ * const seriesTransforms = {
8
+ * movingAverage: {
9
+ * label: 'Moving average',
10
+ * params: { window: { label: 'Window', default: 5, min: 1, step: 1 } },
11
+ * outputs: {
12
+ * mean: { label: 'Mean', suffix: ' (MA)', lineStyle: 'dashed' },
13
+ * },
14
+ * run: async (y, params) => ({ mean: rollingMean(y, params.window) }),
15
+ * },
16
+ * };
17
+ *
18
+ * `run(y, params)` resolves `{ [output]: number[] }`. It is called ONCE per
19
+ * eligible trace (a 2-D line/scatter trace) for each transform with at least one
20
+ * output switched on, and every switched-on output it returns is drawn as an
21
+ * extra trace in the source trace's colour, named `<source name><suffix>`, with
22
+ * the output's `lineStyle` (solid | dashed | dotted) and `lineWidth`.
23
+ *
24
+ * What is switched on lives in the subplot, under `subplot.transforms`:
25
+ *
26
+ * subplot.transforms = { movingAverage: { outputs: { mean: true }, params: { window: 7 } } }
27
+ *
28
+ * A transform that throws, rejects or resolves something that is not an object
29
+ * is skipped for that trace; the figure is drawn without it.
30
+ */
31
+
32
+ import { buildTrace } from './chart_types.js';
33
+
34
+ /** A transform's parameter values: its declared defaults under the subplot's own. */
35
+ function resolveParams(transform, state) {
36
+ const params = {};
37
+ for (const [name, spec] of Object.entries(transform.params ?? {})) {
38
+ params[name] = spec?.default;
39
+ }
40
+ return { ...params, ...(state?.params ?? {}) };
41
+ }
42
+
43
+ /** The output names of `transform` that `state` switches on, in declared order. */
44
+ function enabledOutputs(transform, state) {
45
+ return Object.keys(transform.outputs ?? {}).filter((name) => state?.outputs?.[name] === true);
46
+ }
47
+
48
+ /** A trace a transform may run on: 2-D, line or scatter, with a y array. */
49
+ function isEligible(trace) {
50
+ const type = trace.type ?? 'scatter';
51
+ return (type === 'scatter' || type === 'scattergl') && Array.isArray(trace.y);
52
+ }
53
+
54
+ /**
55
+ * The derived traces for one subplot's figure.
56
+ *
57
+ * @param {Object[]} traces - The figure's Plotly traces so far (not modified).
58
+ * @param {Object} subplot - The subplot config; reads `subplot.transforms`.
59
+ * @param {Object|null} seriesTransforms - `{ [id]: transform }`, or null for none.
60
+ * @param {Object} [options]
61
+ * @param {Function} [options.onError] - `({ transformId, traceName, error })`,
62
+ * called for each skipped run.
63
+ * @returns {Promise<Object[]>} The extra traces, in transform then trace order.
64
+ */
65
+ async function deriveSeriesTraces(traces, subplot, seriesTransforms, { onError } = {}) {
66
+ const derived = [];
67
+ if (!seriesTransforms) return derived;
68
+ const sources = traces.filter(isEligible);
69
+
70
+ for (const [transformId, transform] of Object.entries(seriesTransforms)) {
71
+ const state = subplot?.transforms?.[transformId];
72
+ const outputs = enabledOutputs(transform, state);
73
+ if (!outputs.length || typeof transform?.run !== 'function') continue;
74
+ const params = resolveParams(transform, state);
75
+
76
+ for (const source of sources) {
77
+ let result;
78
+ try {
79
+ result = await transform.run(source.y, params);
80
+ } catch (error) {
81
+ onError?.({ transformId, traceName: source.name, error });
82
+ continue;
83
+ }
84
+ if (!result || typeof result !== 'object') {
85
+ onError?.({ transformId, traceName: source.name,
86
+ error: new Error(`transform "${transformId}" resolved no outputs`) });
87
+ continue;
88
+ }
89
+ const color = source.line?.color || source.marker?.color;
90
+ for (const name of outputs) {
91
+ const y = result[name];
92
+ if (!Array.isArray(y)) continue;
93
+ const spec = transform.outputs[name];
94
+ derived.push({
95
+ ...buildTrace({
96
+ chartType: 'line',
97
+ x: source.x,
98
+ y,
99
+ name: `${source.name}${spec.suffix ?? ` (${spec.label ?? name})`}`,
100
+ color,
101
+ lineWidth: spec.lineWidth ?? 2,
102
+ lineStyle: spec.lineStyle ?? 'solid',
103
+ yAxisId: source.yaxis || 'y',
104
+ }),
105
+ showlegend: true,
106
+ });
107
+ }
108
+ }
109
+ }
110
+ return derived;
111
+ }
112
+
113
+ /**
114
+ * The controls for `seriesTransforms` in a subplot's config: per transform, a
115
+ * toggle for each output and a number field for each parameter. They edit
116
+ * `subplot.transforms` in place and call `onChange()` after every edit. With no
117
+ * transforms (null or `{}`) nothing is rendered.
118
+ *
119
+ * @param {HTMLElement} container
120
+ * @param {Object} subplot
121
+ * @param {Object|null} seriesTransforms
122
+ * @param {Object} [options]
123
+ * @param {Function} [options.onChange]
124
+ * @returns {{ dispose: Function }}
125
+ */
126
+ function renderSeriesTransformControls(container, subplot, seriesTransforms, { onChange } = {}) {
127
+ const entries = Object.entries(seriesTransforms ?? {});
128
+ if (!entries.length) return { dispose() {} };
129
+
130
+ const root = document.createElement('div');
131
+ root.className = 'twm-series-transforms';
132
+
133
+ const stateOf = (transformId) => {
134
+ subplot.transforms ??= {};
135
+ subplot.transforms[transformId] ??= {};
136
+ return subplot.transforms[transformId];
137
+ };
138
+
139
+ for (const [transformId, transform] of entries) {
140
+ const group = document.createElement('fieldset');
141
+ group.className = 'twm-series-transforms__group';
142
+ const legend = document.createElement('legend');
143
+ legend.className = 'twm-series-transforms__label';
144
+ legend.textContent = transform.label ?? transformId;
145
+ group.appendChild(legend);
146
+
147
+ const paramInputs = [];
148
+ const syncDisabled = () => {
149
+ const on = enabledOutputs(transform, subplot.transforms?.[transformId]).length > 0;
150
+ for (const input of paramInputs) input.disabled = !on;
151
+ };
152
+
153
+ for (const [name, spec] of Object.entries(transform.outputs ?? {})) {
154
+ const row = document.createElement('label');
155
+ row.className = 'twm-series-transforms__row';
156
+ const box = document.createElement('input');
157
+ box.type = 'checkbox';
158
+ box.checked = subplot.transforms?.[transformId]?.outputs?.[name] === true;
159
+ box.addEventListener('change', () => {
160
+ const state = stateOf(transformId);
161
+ state.outputs = { ...(state.outputs ?? {}), [name]: box.checked };
162
+ syncDisabled();
163
+ onChange?.();
164
+ });
165
+ const text = document.createElement('span');
166
+ text.textContent = spec?.label ?? name;
167
+ row.append(box, text);
168
+ group.appendChild(row);
169
+ }
170
+
171
+ for (const [name, spec] of Object.entries(transform.params ?? {})) {
172
+ const row = document.createElement('label');
173
+ row.className = 'twm-series-transforms__row';
174
+ const text = document.createElement('span');
175
+ text.textContent = spec?.label ?? name;
176
+ const input = document.createElement('input');
177
+ input.type = 'number';
178
+ input.className = 'twm-series-transforms__param';
179
+ for (const attr of ['min', 'max', 'step']) {
180
+ if (spec?.[attr] != null) input[attr] = String(spec[attr]);
181
+ }
182
+ const current = subplot.transforms?.[transformId]?.params?.[name] ?? spec?.default;
183
+ input.value = current == null ? '' : String(current);
184
+ input.addEventListener('change', () => {
185
+ const value = parseFloat(input.value);
186
+ const state = stateOf(transformId);
187
+ const params = { ...(state.params ?? {}) };
188
+ if (Number.isFinite(value)) params[name] = value;
189
+ else delete params[name];
190
+ state.params = params;
191
+ onChange?.();
192
+ });
193
+ paramInputs.push(input);
194
+ row.append(text, input);
195
+ group.appendChild(row);
196
+ }
197
+
198
+ syncDisabled();
199
+ root.appendChild(group);
200
+ }
201
+
202
+ container.appendChild(root);
203
+ return { dispose() { root.remove(); } };
204
+ }
205
+
206
+ export { deriveSeriesTraces, renderSeriesTransformControls };
File without changes
@@ -26,7 +26,7 @@ const QUIET_MODE_LEVEL = 'warn';
26
26
 
27
27
  export class LoggingService {
28
28
  constructor({
29
- name = 'ecosim',
29
+ name = 'flexdesk',
30
30
  eventBus = null,
31
31
  minLevel = 'info',
32
32
  historyLimit = 500,
@@ -7,7 +7,8 @@
7
7
  * SCHEMA. The store knows nothing about any particular application's settings:
8
8
  * it ships the *shell's* own namespaces (workspace, window, logging, data,
9
9
  * notebook, ai, …) and an embedder PUSHES its own namespace in at boot with
10
- * `registerSettings()`. The Settings page renders whatever is in the schema.
10
+ * `registerSettings()`. The settings page — `SettingsView` in
11
+ * `src/ui/components/settings_view.js` — renders whatever is in the schema.
11
12
  *
12
13
  * Two doors, on purpose
13
14
  * ---------------------
@@ -18,8 +19,9 @@
18
19
  * getSetting() / setSetting() / registerSettings() / …
19
20
  * A thin binding over ONE instance of that primitive for THIS document.
20
21
  * The cardinality is correct, not merely convenient: the store has a
21
- * single physical backing key (`localStorage['ecosim.settings.v1']`), so
22
- * two live stores in one document would be two writers to one key.
22
+ * single physical backing key (`localStorage['flexdesk.settings.v1']`
23
+ * unless `configureSettings({ storageKey })` names another), so two
24
+ * live stores in one document would be two writers to one key.
23
25
  *
24
26
  * Usage
25
27
  * -----
@@ -300,7 +302,7 @@ const SCHEMA = Object.freeze({
300
302
  label: 'Projects directory',
301
303
  description: 'Default directory for new projects and bundled demos. Leave empty for OS default.',
302
304
  defaultValue: null,
303
- placeholder: 'OS default (%APPDATA%/EcoSim/projects)',
305
+ placeholder: 'OS default',
304
306
  },
305
307
 
306
308
  // ── AI Assistant ────────────────────────────────────────────────────────
@@ -387,7 +389,10 @@ const CATEGORIES = Object.freeze([
387
389
  { id: 'advanced', label: 'Advanced', icon: 'code', description: 'Debug and developer settings', order: 80 },
388
390
  ]);
389
391
 
390
- const STORAGE_KEY = 'ecosim.settings.v1';
392
+ /** The binding's localStorage key when the embedder names none. An embedder
393
+ * that stored settings under another key keeps them by passing that key to
394
+ * `configureSettings({ storageKey })` before anything reads a setting. */
395
+ export const DEFAULT_SETTINGS_STORAGE_KEY = 'flexdesk.settings.v1';
391
396
 
392
397
  // ─── Path / value helpers ────────────────────────────────────────────────────
393
398
 
@@ -494,8 +499,9 @@ export function createSettingsStore({
494
499
  const _defaults = _clone(defaults) ?? {};
495
500
  const _schema = { ...schema };
496
501
  let _categories = categories.map(c => ({ ...c }));
497
- const _values = _clone(defaults) ?? {};
502
+ let _values = _clone(defaults) ?? {};
498
503
  let _bus = eventBus;
504
+ let _storageKey = storageKey;
499
505
 
500
506
  const _sortCategories = () => {
501
507
  // Array#sort is stable, so equal `order` keeps registration order.
@@ -513,8 +519,8 @@ export function createSettingsStore({
513
519
  */
514
520
  const _load = () => {
515
521
  try {
516
- if (!storageKey || typeof localStorage === 'undefined') return;
517
- const raw = localStorage.getItem(storageKey);
522
+ if (!_storageKey || typeof localStorage === 'undefined') return;
523
+ const raw = localStorage.getItem(_storageKey);
518
524
  if (!raw) return;
519
525
  const saved = JSON.parse(raw);
520
526
  for (const path of Object.keys(_schema)) {
@@ -532,7 +538,7 @@ export function createSettingsStore({
532
538
  /** Persist values that differ from their defaults (sparse storage). */
533
539
  const _save = () => {
534
540
  try {
535
- if (!storageKey || typeof localStorage === 'undefined') return;
541
+ if (!_storageKey || typeof localStorage === 'undefined') return;
536
542
  const sparse = {};
537
543
  for (const path of Object.keys(_schema)) {
538
544
  const parts = path.split('.');
@@ -543,9 +549,9 @@ export function createSettingsStore({
543
549
  }
544
550
  }
545
551
  if (Object.keys(sparse).length === 0) {
546
- localStorage.removeItem(storageKey);
552
+ localStorage.removeItem(_storageKey);
547
553
  } else {
548
- localStorage.setItem(storageKey, JSON.stringify(sparse));
554
+ localStorage.setItem(_storageKey, JSON.stringify(sparse));
549
555
  }
550
556
  } catch (err) {
551
557
  console.warn('[Settings] Failed to persist settings', err);
@@ -569,19 +575,43 @@ export function createSettingsStore({
569
575
  * namespace is registered would garbage-collect that namespace's saved
570
576
  * values. Registering at the top of bootstrap satisfies this trivially.
571
577
  *
578
+ * Listeners hear `settings:registered` with `{ paths, categories }` — the
579
+ * slice's schema paths and the ids of the categories it added — so a view
580
+ * that is already open (the settings view) can draw the new rows.
581
+ *
572
582
  * @param {SettingsSlice} slice
573
583
  */
574
584
  const extend = ({ defaults: d = {}, schema: s = {}, categories: c = [] } = {}) => {
575
585
  _deepMerge(_defaults, d);
576
586
  _deepMerge(_values, d);
577
587
  Object.assign(_schema, s);
588
+ const added = [];
578
589
  for (const cat of c) {
579
590
  if (!cat?.id) continue;
580
591
  if (_categories.some(x => x.id === cat.id)) continue;
581
592
  _categories.push({ ...cat });
593
+ added.push(cat.id);
582
594
  }
583
595
  _sortCategories();
584
596
  _load(); // idempotent: overlays only paths it finds in storage
597
+ if (_bus) _bus.emit('settings:registered', { paths: Object.keys(s), categories: added });
598
+ };
599
+
600
+ /**
601
+ * Move the store onto another localStorage key: every value goes back to
602
+ * its default and is then overlaid from the NEW key only. Nothing is
603
+ * written, so the key the store leaves keeps whatever it held. Listeners
604
+ * hear one `settings:changed` with a null path, as after a reset.
605
+ *
606
+ * @param {string|null} key localStorage key; null -> no persistence.
607
+ * @param {object} [opts]
608
+ * @param {boolean} [opts.notify=true] false -> emit nothing.
609
+ */
610
+ const setStorageKey = (key, { notify = true } = {}) => {
611
+ _storageKey = key || null;
612
+ _values = _clone(_defaults) ?? {};
613
+ _load();
614
+ if (notify && _bus) _bus.emit('settings:changed', { path: null });
585
615
  };
586
616
 
587
617
  const get = (path, defaultValue = undefined) => {
@@ -683,6 +713,8 @@ export function createSettingsStore({
683
713
  getCategories: () => Object.freeze(_categories.slice()),
684
714
  getByCategory,
685
715
  setEventBus: (bus) => { _bus = bus; },
716
+ setStorageKey,
717
+ getStorageKey: () => _storageKey,
686
718
  });
687
719
  }
688
720
 
@@ -691,7 +723,8 @@ export function createSettingsStore({
691
723
  // A capability *port* (the host bridge) may have many implementations at once,
692
724
  // so a singleton there is a category error. A *value store* with a single
693
725
  // physical backing key has cardinality one by construction: two live stores in
694
- // one document would be two writers to `localStorage[STORAGE_KEY]`. The
726
+ // one document would be two writers to the one key `configureSettings()`
727
+ // names (default `DEFAULT_SETTINGS_STORAGE_KEY`). The
695
728
  // singleton is the correct cardinality, not a shortcut — and the factory above
696
729
  // is the primitive, so tests and a standalone demo never touch this binding.
697
730
 
@@ -699,9 +732,62 @@ const _store = createSettingsStore({
699
732
  defaults: DEFAULTS,
700
733
  schema: SCHEMA,
701
734
  categories: CATEGORIES,
702
- storageKey: STORAGE_KEY,
735
+ storageKey: null,
703
736
  });
704
737
 
738
+ // The binding reads its key LAZILY — on the first call that reads, writes or
739
+ // resets a VALUE — so an embedder that calls `configureSettings({ storageKey })`
740
+ // at boot never has the default key read at all, and importing this module
741
+ // touches no storage. registerSettings(), registerSettingsEventBus() and the
742
+ // schema/category/default getters do not attach it: they may come before or
743
+ // after configureSettings().
744
+ let _bindingKey = DEFAULT_SETTINGS_STORAGE_KEY;
745
+ let _bound = false;
746
+
747
+ /** The binding's store, attached to its key on first use. */
748
+ function _binding() {
749
+ if (!_bound) {
750
+ _bound = true;
751
+ _store.setStorageKey(_bindingKey, { notify: false });
752
+ }
753
+ return _store;
754
+ }
755
+
756
+ /**
757
+ * The app-global binding's store itself — the handle `createSettingsStore()`
758
+ * returns — for code that takes a store, such as the settings view
759
+ * (`new SettingsView({ store: getSettingsStore(), eventBus })`). Its `set` is
760
+ * the one `setSetting()` calls. Like `getSetting()`, it attaches the binding
761
+ * to its storage key, so call `configureSettings()` first.
762
+ *
763
+ * @returns {object}
764
+ */
765
+ export function getSettingsStore() {
766
+ return _binding();
767
+ }
768
+
769
+ /**
770
+ * Choose the localStorage key the app-global binding reads and writes.
771
+ * Call it at boot, before the first getSetting/setSetting/reset*: from then on
772
+ * the binding touches only `storageKey`.
773
+ *
774
+ * Called later, the binding moves: values return to their defaults, are
775
+ * reloaded from the new key, and a `settings:changed` (path null) is emitted.
776
+ * The old key is left as it was.
777
+ *
778
+ * @param {object} [opts]
779
+ * @param {string} [opts.storageKey=DEFAULT_SETTINGS_STORAGE_KEY]
780
+ */
781
+ export function configureSettings({ storageKey = DEFAULT_SETTINGS_STORAGE_KEY } = {}) {
782
+ _bindingKey = storageKey;
783
+ if (_bound) _store.setStorageKey(storageKey);
784
+ }
785
+
786
+ /** @returns {string} The localStorage key the app-global binding uses. */
787
+ export function getSettingsStorageKey() {
788
+ return _bindingKey;
789
+ }
790
+
705
791
  /**
706
792
  * Push an embedder's settings namespace into the app-global store.
707
793
  *
@@ -733,7 +819,7 @@ export function registerSettingsEventBus(eventBus) {
733
819
  * @returns {*} The setting value or defaultValue
734
820
  */
735
821
  export function getSetting(path, defaultValue = undefined) {
736
- return _store.get(path, defaultValue);
822
+ return _binding().get(path, defaultValue);
737
823
  }
738
824
 
739
825
  /**
@@ -743,7 +829,7 @@ export function getSetting(path, defaultValue = undefined) {
743
829
  * @returns {boolean} True if set successfully
744
830
  */
745
831
  export function setSetting(path, value) {
746
- return _store.set(path, value);
832
+ return _binding().set(path, value);
747
833
  }
748
834
 
749
835
  /**
@@ -761,7 +847,7 @@ export function getDefaultValue(path) {
761
847
  * @returns {boolean} True if reset successfully
762
848
  */
763
849
  export function resetSetting(path) {
764
- return _store.resetOne(path);
850
+ return _binding().resetOne(path);
765
851
  }
766
852
 
767
853
  /**
@@ -769,12 +855,12 @@ export function resetSetting(path) {
769
855
  * @param {string} categoryId
770
856
  */
771
857
  export function resetCategory(categoryId) {
772
- _store.resetCategory(categoryId);
858
+ _binding().resetCategory(categoryId);
773
859
  }
774
860
 
775
861
  /** Reset all settings to their defaults. */
776
862
  export function resetAllSettings() {
777
- _store.resetAll();
863
+ _binding().resetAll();
778
864
  }
779
865
 
780
866
  /**
@@ -782,7 +868,7 @@ export function resetAllSettings() {
782
868
  * @returns {object} Deep copy of current settings
783
869
  */
784
870
  export function getAllSettings() {
785
- return _store.getAll();
871
+ return _binding().getAll();
786
872
  }
787
873
 
788
874
  // ─── Schema / Category API (for Settings page UI) ───────────────────────────