flexdesk 0.17.13 → 0.17.14

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 (243) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +0 -0
  3. package/README.md +0 -0
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +0 -0
  6. package/charts.js +0 -0
  7. package/core.js +0 -0
  8. package/css/base.css +45 -3
  9. package/css/flexdesk.css +60 -3
  10. package/css/overrides.css +15 -0
  11. package/css/reset.css +0 -0
  12. package/dist/canvas.js +0 -0
  13. package/dist/canvas.js.map +0 -0
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-FOOS3T5L.js +0 -0
  17. package/dist/chunk-FOOS3T5L.js.map +0 -0
  18. package/dist/chunk-GOQLR5WR.js +0 -0
  19. package/dist/chunk-GOQLR5WR.js.map +0 -0
  20. package/dist/chunk-HNU6MM2B.js +0 -0
  21. package/dist/chunk-HNU6MM2B.js.map +0 -0
  22. package/dist/chunk-JKZLNKG6.js +0 -0
  23. package/dist/chunk-JKZLNKG6.js.map +0 -0
  24. package/dist/chunk-JYWURG5T.js +0 -0
  25. package/dist/chunk-JYWURG5T.js.map +0 -0
  26. package/dist/chunk-N7HC3ADV.js +0 -0
  27. package/dist/chunk-N7HC3ADV.js.map +0 -0
  28. package/dist/core.js +3 -18
  29. package/dist/core.js.map +2 -2
  30. package/dist/editor.js.map +0 -0
  31. package/dist/flexdesk.css +60 -3
  32. package/dist/flow.js.map +0 -0
  33. package/dist/host.js +0 -0
  34. package/dist/host.js.map +0 -0
  35. package/dist/reset.css +0 -0
  36. package/dist/tile_registry-6WZPTUZV.js +0 -0
  37. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  38. package/dist/widgets.js +27 -18
  39. package/dist/widgets.js.map +2 -2
  40. package/dist/wm.js +195 -28
  41. package/dist/wm.js.map +3 -3
  42. package/docs/README.md +1 -1
  43. package/docs/best-practices.md +1 -1
  44. package/docs/components/command-palette.md +0 -0
  45. package/docs/components/conversation.md +0 -0
  46. package/docs/components/data-table-grid.md +0 -0
  47. package/docs/components/data-table.md +0 -0
  48. package/docs/components/editor.md +0 -0
  49. package/docs/components/flow-canvas-editor.md +0 -0
  50. package/docs/components/flow-editors.md +0 -0
  51. package/docs/components/flow-lane-editor.md +0 -0
  52. package/docs/components/flow-outline-editor.md +0 -0
  53. package/docs/components/forms-and-dialogs.md +0 -0
  54. package/docs/components/input-fields.md +0 -0
  55. package/docs/components/keymap.md +66 -3
  56. package/docs/components/layout-widgets.md +32 -0
  57. package/docs/components/markdown-and-outline.md +0 -0
  58. package/docs/components/menus.md +9 -5
  59. package/docs/components/notifications.md +0 -0
  60. package/docs/components/panels-and-desktops.md +56 -0
  61. package/docs/components/record-list-and-fact-table.md +0 -0
  62. package/docs/components/settings-view.md +0 -0
  63. package/docs/components/shell.md +0 -0
  64. package/docs/components/slide-out-panel.md +0 -0
  65. package/docs/components/wizard.md +0 -0
  66. package/docs/concepts/building-and-packaging.md +0 -0
  67. package/docs/concepts/domain-neutrality.md +0 -0
  68. package/docs/concepts/host-port.md +0 -0
  69. package/docs/concepts/packages-and-entry-points.md +0 -0
  70. package/docs/concepts/peer-dependencies.md +0 -0
  71. package/docs/concepts/state-guard.md +65 -2
  72. package/docs/concepts/styling.md +24 -6
  73. package/docs/quickstart.md +0 -0
  74. package/editor.js +0 -0
  75. package/flow.js +0 -0
  76. package/host.js +0 -0
  77. package/package.json +2 -2
  78. package/src/canvas/canvas_adapter.js +0 -0
  79. package/src/canvas/edge_router.js +0 -0
  80. package/src/canvas/graph/connector_router.js +0 -0
  81. package/src/canvas/graph/history_service.js +0 -0
  82. package/src/canvas/graph/node_platform.js +0 -0
  83. package/src/canvas/index.js +0 -0
  84. package/src/canvas/layered_layout.js +0 -0
  85. package/src/charts/data_series_plot_window.js +0 -0
  86. package/src/charts/downsample.js +0 -0
  87. package/src/charts/plot_config.js +0 -0
  88. package/src/charts/plotly_wrapper.js +0 -0
  89. package/src/core/event_bus.js +0 -0
  90. package/src/core/logging.js +0 -0
  91. package/src/core/settings.js +0 -0
  92. package/src/core/state_guard.js +0 -0
  93. package/src/core/state_guard_config.js +19 -18
  94. package/src/core/state_machine.js +0 -0
  95. package/src/editor/editor_pane.js +0 -0
  96. package/src/editor/monaco_editor_factory.js +0 -0
  97. package/src/editor/monaco_loader.js +0 -0
  98. package/src/editor/notebook_search.js +0 -0
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +0 -0
  101. package/src/flow/canvas_editor/connections.js +0 -0
  102. package/src/flow/canvas_editor/editor.js +0 -0
  103. package/src/flow/canvas_editor/index.js +0 -0
  104. package/src/flow/canvas_editor/layout.js +0 -0
  105. package/src/flow/canvas_editor/strings.js +0 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +0 -0
  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 +0 -0
  113. package/src/flow/kit/keys.js +0 -0
  114. package/src/flow/kit/logic_graph.js +0 -0
  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 +0 -0
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +0 -0
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +0 -0
  122. package/src/flow/lanes/editor.js +0 -0
  123. package/src/flow/lanes/index.js +0 -0
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +0 -0
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +0 -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 +0 -0
  136. package/src/help/help_registry.js +0 -0
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +0 -0
  139. package/src/host/host.js +0 -0
  140. package/src/host/photino_host.js +0 -0
  141. package/src/host/photino_host.md +0 -0
  142. package/src/host/pywebview_host.js +0 -0
  143. package/src/tiles/chart_tiles.js +0 -0
  144. package/src/tiles/config_schema.js +0 -0
  145. package/src/tiles/figure_tile.js +0 -0
  146. package/src/tiles/layout_persistence.js +0 -0
  147. package/src/tiles/list_tiles.js +0 -0
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_config_form.js +0 -0
  150. package/src/tiles/tile_grid.js +0 -0
  151. package/src/tiles/tile_registry.js +0 -0
  152. package/src/tiling/bottom_panel_tabs.js +0 -0
  153. package/src/tiling/command_palette.js +0 -0
  154. package/src/tiling/content_registry.js +0 -0
  155. package/src/tiling/desktops.js +79 -16
  156. package/src/tiling/entity_sources.js +0 -0
  157. package/src/tiling/keymap.js +70 -2
  158. package/src/tiling/kind_taxonomy.js +0 -0
  159. package/src/tiling/landing_table.js +0 -0
  160. package/src/tiling/loading_overlay.js +0 -0
  161. package/src/tiling/nav_panel.js +0 -0
  162. package/src/tiling/page_factory.js +0 -0
  163. package/src/tiling/panel_availability.js +0 -0
  164. package/src/tiling/panel_keys.js +0 -0
  165. package/src/tiling/shell.js +9 -2
  166. package/src/tiling/tab_strip.js +0 -0
  167. package/src/tiling/tile_breadcrumb.js +0 -0
  168. package/src/tiling/tile_renderer.js +0 -0
  169. package/src/tiling/tile_tab_menu.js +0 -0
  170. package/src/tiling/tile_tree.js +63 -5
  171. package/src/tiling/wm.js +59 -6
  172. package/src/tiling/zoom.js +0 -0
  173. package/src/ui/base/component_base.js +0 -0
  174. package/src/ui/base/controller_base.js +0 -0
  175. package/src/ui/base/page_base.js +0 -0
  176. package/src/ui/components/about_dialog.js +0 -0
  177. package/src/ui/components/alert_log.js +0 -0
  178. package/src/ui/components/alerts_pane.js +0 -0
  179. package/src/ui/components/attribute_list_editor.js +0 -0
  180. package/src/ui/components/autocomplete_field.js +0 -0
  181. package/src/ui/components/cell_editor_host.js +0 -0
  182. package/src/ui/components/cell_editors.js +0 -0
  183. package/src/ui/components/cell_grid.js +0 -0
  184. package/src/ui/components/cell_paste.js +0 -0
  185. package/src/ui/components/cell_selection.js +0 -0
  186. package/src/ui/components/choice_controls.js +0 -0
  187. package/src/ui/components/color_field.js +0 -0
  188. package/src/ui/components/color_list_field.js +0 -0
  189. package/src/ui/components/column_fit.js +0 -0
  190. package/src/ui/components/column_groups.js +0 -0
  191. package/src/ui/components/column_order.js +0 -0
  192. package/src/ui/components/computing_status_window.js +0 -0
  193. package/src/ui/components/confirm_dialog.js +0 -0
  194. package/src/ui/components/conversation_view.js +0 -0
  195. package/src/ui/components/crosstab.js +0 -0
  196. package/src/ui/components/detail_header.js +0 -0
  197. package/src/ui/components/document_outline.js +0 -0
  198. package/src/ui/components/drag_reorder.js +0 -0
  199. package/src/ui/components/fact_table.js +0 -0
  200. package/src/ui/components/fill_handle.js +0 -0
  201. package/src/ui/components/gallery_picker.js +0 -0
  202. package/src/ui/components/header_actions.js +0 -0
  203. package/src/ui/components/inline_renamer.js +0 -0
  204. package/src/ui/components/job_progress.js +0 -0
  205. package/src/ui/components/menu_bar.js +0 -0
  206. package/src/ui/components/modal.js +0 -0
  207. package/src/ui/components/notification_history.js +0 -0
  208. package/src/ui/components/progress.js +0 -0
  209. package/src/ui/components/record_list.js +0 -0
  210. package/src/ui/components/row_activation.js +0 -0
  211. package/src/ui/components/section_stack.js +0 -0
  212. package/src/ui/components/series.js +0 -0
  213. package/src/ui/components/settings_view.js +0 -0
  214. package/src/ui/components/slide_out_panel.js +0 -0
  215. package/src/ui/components/slider_field.js +0 -0
  216. package/src/ui/components/sortable_list.js +0 -0
  217. package/src/ui/components/staged_edits.js +0 -0
  218. package/src/ui/components/staged_history.js +0 -0
  219. package/src/ui/components/table_filter_chips.js +0 -0
  220. package/src/ui/components/table_state_store.js +0 -0
  221. package/src/ui/components/table_structure.js +0 -0
  222. package/src/ui/components/table_values.js +0 -0
  223. package/src/ui/components/tabs.js +0 -0
  224. package/src/ui/components/tag_input.js +0 -0
  225. package/src/ui/components/toast.js +0 -0
  226. package/src/ui/components/toggle_field.js +0 -0
  227. package/src/ui/components/tree_view.js +41 -18
  228. package/src/ui/components/tsv.js +0 -0
  229. package/src/ui/components/window_taskbar.js +0 -0
  230. package/src/ui/components/wizard.js +0 -0
  231. package/src/ui/controllers/window_chrome_controller.js +0 -0
  232. package/src/ui/notification_center.js +0 -0
  233. package/src/ui/utils/auto_scrollbars.js +0 -0
  234. package/src/ui/utils/markdown.js +0 -0
  235. package/src/ui/utils/native_context_menu.js +0 -0
  236. package/src/ui/utils/overlay_scrollbar.js +0 -0
  237. package/src/ui/utils/raf_resize_observer.js +0 -0
  238. package/src/ui/utils/scroll_lock.js +0 -0
  239. package/src/ui/utils/strip_scroll.js +0 -0
  240. package/src/ui/utils/tooltip_service.js +0 -0
  241. package/tiles.js +0 -0
  242. package/widgets.js +0 -0
  243. package/wm.js +0 -0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/host/host.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -27,6 +27,25 @@ const DEFAULT_PANEL_STATE = {
27
27
  bottom: true,
28
28
  };
29
29
 
30
+ /** An object that is not an array: what a saved desktop or map is. */
31
+ function _isRecord(v) {
32
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
33
+ }
34
+
35
+ /** Throw a `TypeError` saying what the saved layout was expected to hold at
36
+ * `path` and what it held instead (see the same helper in tile_tree.js, which
37
+ * reads the trees these desktops hold). */
38
+ function _expect(ok, path, want, got) {
39
+ if (ok) return;
40
+ let shown;
41
+ if (got === undefined) shown = 'nothing';
42
+ else {
43
+ try { shown = JSON.stringify(got); } catch { shown = String(got); }
44
+ if (shown.length > 40) shown = `${shown.slice(0, 37)}...`;
45
+ }
46
+ throw new TypeError(`${path}: expected ${want}, found ${shown}`);
47
+ }
48
+
30
49
  /**
31
50
  * `seed()` yields the leaf a fresh (or emptied) desktop starts with. It comes
32
51
  * from the taxonomy's root kind — see WindowManager, which supplies it. The
@@ -178,23 +197,65 @@ export class DesktopManager {
178
197
  };
179
198
  }
180
199
 
200
+ /**
201
+ * Rebuild the manager from what `serialize()` wrote. `null`/`undefined` is
202
+ * nothing saved and gives the fresh manager.
203
+ *
204
+ * The blob is whatever the host's `state` held under `DESKTOPS_KEY`: a file
205
+ * anyone can truncate or edit, which is valid JSON of any shape. So it is
206
+ * read STRICTLY, and a blob that `serialize()` could not have written throws
207
+ * a `TypeError` naming the path and what was expected there. It builds only
208
+ * objects of its own and touches nothing its caller holds, which is what lets
209
+ * `WindowManager.load` treat a throw as "nothing saved" with nothing to undo.
210
+ *
211
+ * Strict about SHAPE, not about every value. What the loader, the tree
212
+ * operations and the renderer index, iterate or branch on has to be what they
213
+ * need (containers, node and tab kinds, `dir`, `children`, `sizes`,
214
+ * `activeTabIdx`). What is only drawn or coerced is taken as it is: a desktop's
215
+ * `id`, `label`, `group` and `fixed`, a panel's flag, a tab's title and props,
216
+ * `activeIdx`. A layout that fails on one of those would be thrown away for
217
+ * nothing, and an app can put almost anything in a title.
218
+ *
219
+ * And strict is not hostile to an old blob. A key the loader has always given
220
+ * a default may be absent: a desktop's `id`, `label` and `panels`, the keys
221
+ * later releases added (`panelShares`, `group`, `fixed`), and a leaf's `tabs`
222
+ * and `pageTabs`. What `serialize()` has always written and the loader cannot
223
+ * do without (`desktops`, a desktop's `tree`, a split's `dir`, `children` and
224
+ * `sizes`) may not.
225
+ */
181
226
  static deserialize(blob, { seed, panelDefaults = null } = {}) {
182
227
  const m = new DesktopManager({ seed, panelDefaults });
183
- if (!blob || !Array.isArray(blob.desktops) || blob.desktops.length === 0) return m;
184
- m.desktops = blob.desktops.map((raw) => ({
185
- id: raw.id || `desk-${Math.random().toString(36).slice(2,8)}`,
186
- label: raw.label || '?',
187
- tree: raw.tree ? TileTree.deserialize(raw.tree) : new TileTree(),
188
- windows: [],
189
- // A RESTORED desktop's own answer wins over the default: the user
190
- // closed that panel, and re-opening it on every reload is the bug
191
- // this merge order avoids. The default only fills a key the stored
192
- // blob predates.
193
- panels: { ...m.panelDefaults, ...(raw.panels || {}) },
194
- panelShares: { ...(raw.panelShares || {}) },
195
- group: raw.group || null,
196
- fixed: !!raw.fixed,
197
- }));
228
+ if (blob == null) return m;
229
+ _expect(Array.isArray(blob.desktops) && blob.desktops.length > 0,
230
+ 'desktops', 'a non-empty array of desktops', blob.desktops);
231
+ m.desktops = blob.desktops.map((raw, i) => {
232
+ const at = `desktops[${i}]`;
233
+ _expect(_isRecord(raw), at, 'a desktop, an object', raw);
234
+ // `null` is a desktop with nothing on it (serialize() writes that);
235
+ // a desktop with no `tree` key at all is not one serialize() writes.
236
+ _expect(raw.tree === null || (_isRecord(raw.tree) && _isRecord(raw.tree.root)),
237
+ `${at}.tree`, '{ root }, or null for an empty desktop', raw.tree);
238
+ _expect(raw.panels == null || _isRecord(raw.panels), `${at}.panels`, 'an object', raw.panels);
239
+ // The VALUES of `panels` and `panelShares` are not checked: the loader
240
+ // already coerces a flag (`!!`) and `_validShare` drops a share that
241
+ // is not one, so the panel opens at its default.
242
+ _expect(raw.panelShares == null || _isRecord(raw.panelShares),
243
+ `${at}.panelShares`, 'an object', raw.panelShares);
244
+ return {
245
+ id: raw.id || `desk-${Math.random().toString(36).slice(2,8)}`,
246
+ label: raw.label || '?',
247
+ tree: raw.tree ? TileTree.deserialize(raw.tree, `${at}.tree`) : new TileTree(),
248
+ windows: [],
249
+ // A RESTORED desktop's own answer wins over the default: the user
250
+ // closed that panel, and re-opening it on every reload is the bug
251
+ // this merge order avoids. The default only fills a key the stored
252
+ // blob predates.
253
+ panels: { ...m.panelDefaults, ...(raw.panels || {}) },
254
+ panelShares: { ...(raw.panelShares || {}) },
255
+ group: raw.group || null,
256
+ fixed: !!raw.fixed,
257
+ };
258
+ });
198
259
  // Empty tree → seed with the taxonomy root so the desktop is usable.
199
260
  for (const d of m.desktops) {
200
261
  if (!d.tree.rootId) d.tree.setRoot(makeLeaf(seed()));
@@ -212,7 +273,9 @@ export class DesktopManager {
212
273
  * against a host that cannot persist (e.g. the standalone demo).
213
274
  */
214
275
 
215
- const DESKTOPS_KEY = 'desktops';
276
+ /** The logical state key the layout is saved under. Exported so a warning about
277
+ * a layout that could not be read can name it. */
278
+ export const DESKTOPS_KEY = 'desktops';
216
279
 
217
280
  export async function loadDesktops(state) {
218
281
  if (!state) return null;
File without changes
@@ -287,6 +287,70 @@ function _compose(mods, key) {
287
287
  return [...used, k].join('+');
288
288
  }
289
289
 
290
+ /** What counts as "focus is in an editable field": a key press aimed at one of
291
+ * these may be typing. */
292
+ const TEXT_FIELDS = 'input, textarea, select, [contenteditable="true"]';
293
+
294
+ /**
295
+ * Whether Alt on this machine is a key that TYPES: Option on a Mac (and on an
296
+ * iPad with a keyboard). Option changes the character on every Mac layout, so
297
+ * Option+digit is how a programmer types `{ } [ ] | \ @ #` on a German,
298
+ * Spanish, Swedish or French keyboard, where Windows and Linux keep Alt for
299
+ * shortcuts and type those with AltGr. Nothing in the event tells the two
300
+ * apart (a US Windows Alt+Shift+2 and a German Mac Option+8 both arrive as
301
+ * `altKey` with a printable `key`), so this is read from the platform, at the
302
+ * key press so that it is never stale.
303
+ */
304
+ function _altTypes() {
305
+ const nav = globalThis.navigator;
306
+ return /^(mac|iphone|ipad|ipod)/i.test(nav?.userAgentData?.platform || nav?.platform || '');
307
+ }
308
+
309
+ /**
310
+ * The digit ('1'..'9') a keydown on the digit row stands for, or null.
311
+ *
312
+ * `e.key` is what the layout and the modifiers TYPE, and `Alt+Shift+2` names a
313
+ * key, not a character. With Shift held a US keyboard's 2 types "@" and a
314
+ * German one's '"'; Option on a Mac types "™" or "€"; and an AZERTY keyboard's
315
+ * digit row types "&", "é", '"' and the like with no modifier at all and the
316
+ * digits only with Shift. Read `e.key` and Alt+Shift+2 is never seen from a
317
+ * real key press (only from a synthetic event that sets `key: '2'`, which is
318
+ * how the tests hid it), and on AZERTY it is the unshifted Alt+1..9 that never
319
+ * fire. `e.code` is the key's position (`Digit2`), which neither Shift nor the
320
+ * layout changes.
321
+ *
322
+ * `e.key` is still read in three cases:
323
+ *
324
+ * - An event with no `Digit` code: a synthetic one that carries only `key`,
325
+ * and the numpad (`Numpad2`), whose `key` is the digit itself with NumLock
326
+ * on and a navigation key ("ArrowDown") with it off. The numpad is left to
327
+ * say which it is, so a numpad used as arrows still moves focus.
328
+ * - AltGr. Windows reports it as Ctrl+Alt, and a German, French or Spanish
329
+ * layout types `{ [ ] @ # |` on its digit row with it. Taking those events
330
+ * for Alt+digit would switch desktops, and swallow the character in a
331
+ * field (the Alt chords work in fields), instead of typing it.
332
+ * - Option on a Mac, with focus in an editable field. It types the same
333
+ * characters AltGr does (German Option+8 is "{"), but arrives as a bare
334
+ * `altKey` with no Ctrl and no AltGraph state, so the AltGr test above does
335
+ * not see it. Out of a field nothing is typed, so there the key's position
336
+ * is read as everywhere else and Option+2 switches desktop; in a field the
337
+ * character is typed, as it was before the position was read, and the
338
+ * chord waits for focus to leave the field. The test is the platform, not
339
+ * "does `key` look like a character": US Windows Alt+Shift+2 types "@" in
340
+ * the same way and is the very press this function exists to read.
341
+ *
342
+ * This is for the digit row only. Letter keys move with the layout (AZERTY and
343
+ * QWERTZ swap some, Dvorak most), so `KeyW` is not "the W key" there: a letter
344
+ * chord stays named by the character its key types.
345
+ */
346
+ function _digitOfEvent(e) {
347
+ const typesCharacter = e.getModifierState?.('AltGraph') || (e.ctrlKey && e.altKey)
348
+ || (_altTypes() && !!e.target?.closest?.(TEXT_FIELDS));
349
+ const byPosition = typesCharacter ? null : /^Digit([1-9])$/.exec(e.code ?? '');
350
+ if (byPosition) return byPosition[1];
351
+ return /^[1-9]$/.test(e.key) ? e.key : null;
352
+ }
353
+
290
354
  /**
291
355
  * The chord id a keydown event is read as, in `DEFAULT_CHORDS`' spelling, or
292
356
  * null. This is NOT an exact match on the modifier set: it reads an event the
@@ -303,6 +367,9 @@ function _compose(mods, key) {
303
367
  * F1..F12, Shift+Left/Right, Ctrl+Left/Right exactly those modifiers
304
368
  * with Alt held, Ctrl and Meta are ignored:
305
369
  * Alt(+Shift)+1..9, +H, +V, +Arrow Shift picks the Alt+Shift+… row
370
+ * (1..9 by the key's position,
371
+ * not the character it types:
372
+ * see `_digitOfEvent`)
306
373
  * Alt+T / N / W / F Shift ignored
307
374
  * any other Alt+letter Alt+Letter
308
375
  *
@@ -330,7 +397,8 @@ function _chordOfEvent(e) {
330
397
  return null;
331
398
  }
332
399
  const sh = shift ? 'Shift+' : '';
333
- if (/^[1-9]$/.test(key)) return `Alt+${sh}${key}`;
400
+ const digit = _digitOfEvent(e);
401
+ if (digit) return `Alt+${sh}${digit}`;
334
402
  if (lower === 'h' || lower === 'v') return `Alt+${sh}${lower.toUpperCase()}`;
335
403
  if (lower === 't' || lower === 'n' || lower === 'w' || lower === 'f') return `Alt+${lower.toUpperCase()}`;
336
404
  if (ARROWS.includes(key)) return `Alt+${sh}${key}`;
@@ -442,7 +510,7 @@ export function installKeymap({ wm, palette, navSelector = null, bindings = null
442
510
  if (!chord) return;
443
511
  const b = table.get(chord);
444
512
  if (!b || b.disabled) return;
445
- if (!b.inField && e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
513
+ if (!b.inField && e.target?.closest?.(TEXT_FIELDS)) return;
446
514
  if (b.run(ctx, e) !== false) e.preventDefault();
447
515
  };
448
516
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -62,7 +62,11 @@ import { openForm } from '../ui/components/modal.js';
62
62
  * it through (the palette + tile tab menu
63
63
  * hand it to the entity catalog's fetchers).
64
64
  * @param {object} [cfg.eventBus]
65
- * @param {object} [cfg.logger]
65
+ * @param {object} [cfg.logger] `{ warn? }`. Told of problems the shell
66
+ * carries on from: a keymap binding it
67
+ * does not know, a saved layout it cannot
68
+ * read (the default one opens instead).
69
+ * Else `console`.
66
70
  * @param {object} [cfg.tableStore] threaded into ctx for table persistence
67
71
  * @param {object} [cfg.events] bus-event NAMES the WM listens for
68
72
  * @param {object} [cfg.rootCrumb] the breadcrumb's leading segment
@@ -237,6 +241,7 @@ export async function createShell({
237
241
  host,
238
242
  taxonomy,
239
243
  events,
244
+ logger,
240
245
  panelDefaults: panels,
241
246
  snapPromotion,
242
247
  promoteInPlace,
@@ -703,7 +708,9 @@ function _tileContextMenu(wm, leafId, x, y) {
703
708
  ];
704
709
  if (wm.desktops.desktops.length > 1 && !isPanel) {
705
710
  for (const [i, d] of wm.desktops.desktops.entries()) {
706
- // C40. Never onto a fixed desktop — `moveFocusedToDesktop` refuses.
711
+ // C40. Never onto a fixed desktop, nor the one you are on, and (see
712
+ // `isPanel` above) never a panel: `moveFocusedToDesktop` refuses all
713
+ // three too, because the keyboard reaches it without this menu.
707
714
  if (i === wm.desktops.activeIdx || d.fixed) continue;
708
715
  items.push({ label: `Move to desktop ${d.label}`, icon: 'sweep',
709
716
  action: `move:${i}` });
File without changes
File without changes
File without changes
File without changes
@@ -21,6 +21,25 @@
21
21
  let _idCounter = 1;
22
22
  function _newId(prefix) { return `${prefix}-${_idCounter++}`; }
23
23
 
24
+ /** An object that is not an array: what a saved node, tab or map is. */
25
+ function _isRecord(v) {
26
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
27
+ }
28
+
29
+ /** Throw a `TypeError` saying what the saved layout was expected to hold at
30
+ * `path` and what it held instead. The value is shown short: a layout can be
31
+ * large, and the line is read in a console. */
32
+ function _expect(ok, path, want, got) {
33
+ if (ok) return;
34
+ let shown;
35
+ if (got === undefined) shown = 'nothing';
36
+ else {
37
+ try { shown = JSON.stringify(got); } catch { shown = String(got); }
38
+ if (shown.length > 40) shown = `${shown.slice(0, 37)}...`;
39
+ }
40
+ throw new TypeError(`${path}: expected ${want}, found ${shown}`);
41
+ }
42
+
24
43
  function _isPanel(leaf) {
25
44
  return String(leaf?.content?.kind || '').startsWith('panel:');
26
45
  }
@@ -939,11 +958,42 @@ export class TileTree {
939
958
  };
940
959
  }
941
960
 
942
- static deserialize(blob) {
961
+ /** Rebuild a tree from what `serialize()` wrote. A blob with no root is an
962
+ * empty tree, as it always was. Anything else is read STRICTLY: a node that
963
+ * `serialize()` could not have written throws a `TypeError` naming where
964
+ * (`at` is the path of the blob, for the message only) and what it wanted,
965
+ * rather than building half a tree or failing later, inside the renderer,
966
+ * with `node.sizes.reduce is not a function`. The caller decides what a
967
+ * tree that cannot be read means; see `WindowManager.load`. */
968
+ static deserialize(blob, at = 'tree') {
943
969
  const t = new TileTree();
944
970
  if (!blob || !blob.root) return t;
945
- const build = (raw) => {
971
+ // A tab, and the legacy `content` a leaf mirrors its active tab into, are
972
+ // the same shape: what `makeLeaf` and `_syncActiveTab` read. Its `title` and
973
+ // `props` are NOT checked. A title is only drawn, and is whatever a content
974
+ // factory returned (a number, say); props are the app's, and a factory that
975
+ // cannot use them is told so by the registry's mount-error tile, not by a
976
+ // layout thrown away.
977
+ const checkTab = (tab, path) => {
978
+ _expect(_isRecord(tab) && typeof tab.kind === 'string', path, 'a tab, { kind, props, title }', tab);
979
+ };
980
+ const build = (raw, path) => {
981
+ _expect(_isRecord(raw), path, 'a node, an object', raw);
982
+ _expect(raw.kind === 'leaf' || raw.kind === 'split', `${path}.kind`, '"leaf" or "split"', raw.kind);
946
983
  if (raw.kind === 'leaf') {
984
+ if (raw.content != null) checkTab(raw.content, `${path}.content`);
985
+ _expect(raw.tabs == null || Array.isArray(raw.tabs), `${path}.tabs`, 'an array of tabs', raw.tabs);
986
+ (raw.tabs || []).forEach((tab, i) => checkTab(tab, `${path}.tabs[${i}]`));
987
+ _expect(raw.activeTabIdx == null || Number.isFinite(raw.activeTabIdx),
988
+ `${path}.activeTabIdx`, 'a number', raw.activeTabIdx);
989
+ _expect(raw.pageTabs == null || _isRecord(raw.pageTabs), `${path}.pageTabs`, 'an object', raw.pageTabs);
990
+ for (const [page, arc] of Object.entries(raw.pageTabs || {})) {
991
+ const where = `${path}.pageTabs[${JSON.stringify(page)}]`;
992
+ _expect(_isRecord(arc) && Array.isArray(arc.tabs), where, '{ tabs, activeTabIdx }', arc);
993
+ arc.tabs.forEach((tab, i) => checkTab(tab, `${where}.tabs[${i}]`));
994
+ _expect(arc.activeTabIdx == null || Number.isFinite(arc.activeTabIdx),
995
+ `${where}.activeTabIdx`, 'a number', arc.activeTabIdx);
996
+ }
947
997
  // Forward-compat with the pre-tabs layout: an old
948
998
  // serialized leaf carries `content` but no `tabs`.
949
999
  // makeLeaf will derive a single-tab list from `content`.
@@ -961,7 +1011,6 @@ export class TileTree {
961
1011
  if (raw.pageTabs && typeof raw.pageTabs === 'object') {
962
1012
  leaf.pageTabs = {};
963
1013
  for (const [k, v] of Object.entries(raw.pageTabs)) {
964
- if (!v || !Array.isArray(v.tabs)) continue;
965
1014
  leaf.pageTabs[k] = {
966
1015
  tabs: v.tabs.map((t) => ({
967
1016
  kind: t.kind,
@@ -974,7 +1023,16 @@ export class TileTree {
974
1023
  }
975
1024
  return leaf;
976
1025
  }
977
- const children = raw.children.map(build);
1026
+ // `sizes` run parallel to `children`: the renderer, `setSplitSizes`,
1027
+ // `swapSiblings` and the panel canonicalizer all index one by the
1028
+ // other, and none of them looks at a length first.
1029
+ _expect(raw.dir === 'h' || raw.dir === 'v', `${path}.dir`, '"h" or "v"', raw.dir);
1030
+ _expect(Array.isArray(raw.children) && raw.children.length > 0,
1031
+ `${path}.children`, 'a non-empty array of nodes', raw.children);
1032
+ _expect(Array.isArray(raw.sizes) && raw.sizes.length === raw.children.length
1033
+ && raw.sizes.every((n) => Number.isFinite(n) && n >= 0),
1034
+ `${path}.sizes`, `${raw.children.length} non-negative numbers, one per child`, raw.sizes);
1035
+ const children = raw.children.map((c, i) => build(c, `${path}.children[${i}]`));
978
1036
  const split = makeSplit({
979
1037
  dir: raw.dir, children: children.map((c) => c.id),
980
1038
  sizes: raw.sizes,
@@ -982,7 +1040,7 @@ export class TileTree {
982
1040
  children.forEach((c) => t._register(c));
983
1041
  return split;
984
1042
  };
985
- const root = build(blob.root);
1043
+ const root = build(blob.root, `${at}.root`);
986
1044
  t.setRoot(root);
987
1045
  return t;
988
1046
  }
package/src/tiling/wm.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * wm.switchDesktop(idx) / wm.addDesktop() / wm.moveFocusedToDesktop(idx)
19
19
  */
20
20
 
21
- import { DesktopManager, saveDesktops, loadDesktops } from './desktops.js';
21
+ import { DesktopManager, saveDesktops, loadDesktops, DESKTOPS_KEY } from './desktops.js';
22
22
  import { TileRenderer } from './tile_renderer.js';
23
23
  import { makeLeaf } from './tile_tree.js';
24
24
  import { PLACEHOLDER_KIND } from './content_registry.js';
@@ -38,7 +38,7 @@ const PANEL_TITLES = {
38
38
 
39
39
  export class WindowManager {
40
40
  constructor({ rootEl, api, ctx, onChange, eventBus, host, taxonomy, events, content,
41
- panelDefaults = null, snapPromotion = false,
41
+ logger = null, panelDefaults = null, snapPromotion = false,
42
42
  promoteInPlace = false, tabLayout = null, backToOpenList = false,
43
43
  floatActiveTab = false, newTabButton = true }) {
44
44
  if (!taxonomy) throw new Error('WindowManager: a taxonomy is required');
@@ -62,6 +62,9 @@ export class WindowManager {
62
62
  this.events = events || {};
63
63
  this.ctx = ctx || {};
64
64
  this.eventBus = eventBus || null;
65
+ // Where load() reports a saved layout it could not read; else `console`,
66
+ // as the keymap does.
67
+ this.logger = logger || null;
65
68
  this.onChange = onChange || (() => {});
66
69
  // Panel availability. Runtime overrides (`setPanelAvailability`) win
67
70
  // over the primary kind's declaration (`KindDef.panels`); the last
@@ -250,10 +253,41 @@ export class WindowManager {
250
253
  // ── Persistence ─────────────────────────────────────────────────
251
254
  async load() {
252
255
  const blob = await loadDesktops(this.host?.state);
253
- if (blob) {
254
- this.desktops = DesktopManager.deserialize(blob, {
255
- seed: this._rootLeaf, panelDefaults: this.panelDefaults });
256
- this.renderer.tree = this.desktops.active().tree;
256
+ // A SAVED LAYOUT THAT CANNOT BE READ IS NO LAYOUT. The state is a file that
257
+ // can be truncated or edited into valid JSON of the wrong shape, and a boot
258
+ // that dies on it dies on every start until someone deletes the file by
259
+ // hand. `deserialize` reads the blob strictly and throws before anything the
260
+ // WM holds has been touched, so recovering is doing nothing: `this.desktops`
261
+ // and the renderer are as they were, and the rest of this method runs as it
262
+ // does for a host with nothing saved. The catch stops there on purpose. The
263
+ // canonicalize and the render below only ever see a layout that WAS read, so
264
+ // a throw from them is a bug that has to reach the embedder, not a bad file
265
+ // to be answered by discarding what the user saved.
266
+ //
267
+ // The bad blob is LEFT in the store. Deleting it here would destroy the only
268
+ // copy of whatever the user was trying to restore; the next `_persist` (any
269
+ // layout change) overwrites it, and until then each boot warns again, which
270
+ // is the reminder.
271
+ if (blob != null) {
272
+ let restored = null;
273
+ try {
274
+ restored = DesktopManager.deserialize(blob, {
275
+ seed: this._rootLeaf, panelDefaults: this.panelDefaults });
276
+ } catch (err) {
277
+ // ONE string, as the keymap's warning is. The embedder's logger is
278
+ // usually a LoggingService, whose `scoped()` loggers read two leading
279
+ // strings as (subcategory, message) and keep only the second, and
280
+ // whose own warn(message, metadata) keeps the first string and files
281
+ // the rest as metadata: spread over several arguments, the key would
282
+ // be the whole recorded message and the cause would be gone. The
283
+ // console prints one string the same as several.
284
+ (this.logger ?? console).warn?.(`[wm] bad layout for state key ${DESKTOPS_KEY} `
285
+ + `(opening the default layout): ${err?.message ?? err}`);
286
+ }
287
+ if (restored) {
288
+ this.desktops = restored;
289
+ this.renderer.tree = this.desktops.active().tree;
290
+ }
257
291
  }
258
292
  // Normalize: managed windows don't survive a reload, so any
259
293
  // window-placeholder leaves restore their original content.
@@ -3941,10 +3975,26 @@ export class WindowManager {
3941
3975
  return true;
3942
3976
  }
3943
3977
 
3978
+ /**
3979
+ * Move the focused tile, with every tab it holds, to desktop `idx` (from 0).
3980
+ * A refused move returns `undefined` and changes nothing; the callers (the
3981
+ * keymap's `toDesktop:N`, the tile menu's *Move to desktop*) read no result.
3982
+ *
3983
+ * The tile menu (`shell.js`, `_tileContextMenu`) never offers a panel, a
3984
+ * fixed desktop or the desktop you are on, and this method refuses all
3985
+ * three itself, because the menu is one door and Alt+Shift+N is another.
3986
+ * A fixed desktop it always refused; a panel and the desktop you are on it
3987
+ * did not, and the move to the desktop you are on destroys the tile: source
3988
+ * and target are one tree, so the tabs are appended to its primary tile and
3989
+ * then the focused tile is closed. A stray Shift on Alt+N reaches that.
3990
+ */
3944
3991
  moveFocusedToDesktop(idx) {
3945
3992
  const tree = this._tree();
3946
3993
  const focused = tree.focused();
3947
3994
  if (!focused || !focused.content) return;
3995
+ // A panel is chrome, not content (the menu's `isPanel`). Moved, it would
3996
+ // replace the target desktop's home ground.
3997
+ if (String(focused.content.kind || '').startsWith('panel:')) return;
3948
3998
  // C20. IT DESTROYS A LEAF, SO IT ANSWERS TO THE SAME VETO `closeFocused`
3949
3999
  // does. A move is not a close from the pane's point of view, but it is
3950
4000
  // exactly a close from the PAGE's: this was the fourth door out of a
@@ -3954,6 +4004,9 @@ export class WindowManager {
3954
4004
  if (closeChrome === false || closeChrome?.disabled === true) return;
3955
4005
  // C40. A fixed desktop is the embedder's, and nothing is moved onto it.
3956
4006
  if (this.desktops.desktops[idx]?.fixed) return;
4007
+ // Already there. Without this, `tree.close` below removes the tile from
4008
+ // the same tree its tabs were just appended to, and it is gone.
4009
+ if (idx === this.desktops.activeIdx) return;
3957
4010
  this.desktops.ensureCount(idx + 1);
3958
4011
  const target = this.desktops.desktops[idx];
3959
4012
  // C40. THE PANE'S TABS JOIN THE TARGET'S PRIMARY LEAF, ALL OF THEM. It
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes