flexdesk 0.17.13 → 0.17.15

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 (250) hide show
  1. package/CHANGELOG.md +43 -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 +58 -7
  9. package/css/flexdesk.css +90 -8
  10. package/css/overrides.css +21 -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-IG74S3ZH.js → chunk-AD7FIA3Z.js} +2 -2
  17. package/dist/chunk-FOOS3T5L.js +0 -0
  18. package/dist/chunk-FOOS3T5L.js.map +0 -0
  19. package/dist/chunk-GOQLR5WR.js +0 -0
  20. package/dist/chunk-GOQLR5WR.js.map +0 -0
  21. package/dist/chunk-HNU6MM2B.js +0 -0
  22. package/dist/chunk-HNU6MM2B.js.map +0 -0
  23. package/dist/chunk-JKZLNKG6.js +0 -0
  24. package/dist/chunk-JKZLNKG6.js.map +0 -0
  25. package/dist/chunk-JYWURG5T.js +0 -0
  26. package/dist/chunk-JYWURG5T.js.map +0 -0
  27. package/dist/chunk-N7HC3ADV.js +0 -0
  28. package/dist/chunk-N7HC3ADV.js.map +0 -0
  29. package/dist/{chunk-ZMH3OC7H.js → chunk-OEG6NPAN.js} +3 -1
  30. package/dist/chunk-OEG6NPAN.js.map +7 -0
  31. package/dist/core.js +3 -18
  32. package/dist/core.js.map +2 -2
  33. package/dist/editor.js.map +0 -0
  34. package/dist/flexdesk.css +90 -8
  35. package/dist/flow.js +1 -1
  36. package/dist/flow.js.map +0 -0
  37. package/dist/host.js +0 -0
  38. package/dist/host.js.map +0 -0
  39. package/dist/reset.css +0 -0
  40. package/dist/tile_registry-6WZPTUZV.js +0 -0
  41. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  42. package/dist/tiles.js +2 -2
  43. package/dist/widgets.js +29 -20
  44. package/dist/widgets.js.map +2 -2
  45. package/dist/wm.js +205 -35
  46. package/dist/wm.js.map +3 -3
  47. package/docs/README.md +1 -1
  48. package/docs/best-practices.md +1 -1
  49. package/docs/components/command-palette.md +0 -0
  50. package/docs/components/conversation.md +0 -0
  51. package/docs/components/data-table-grid.md +0 -0
  52. package/docs/components/data-table.md +0 -0
  53. package/docs/components/editor.md +0 -0
  54. package/docs/components/flow-canvas-editor.md +0 -0
  55. package/docs/components/flow-editors.md +0 -0
  56. package/docs/components/flow-lane-editor.md +0 -0
  57. package/docs/components/flow-outline-editor.md +0 -0
  58. package/docs/components/forms-and-dialogs.md +19 -2
  59. package/docs/components/input-fields.md +0 -0
  60. package/docs/components/keymap.md +66 -3
  61. package/docs/components/layout-widgets.md +32 -0
  62. package/docs/components/markdown-and-outline.md +0 -0
  63. package/docs/components/menus.md +9 -5
  64. package/docs/components/notifications.md +0 -0
  65. package/docs/components/panels-and-desktops.md +56 -0
  66. package/docs/components/record-list-and-fact-table.md +0 -0
  67. package/docs/components/settings-view.md +0 -0
  68. package/docs/components/shell.md +15 -0
  69. package/docs/components/slide-out-panel.md +15 -0
  70. package/docs/components/wizard.md +0 -0
  71. package/docs/concepts/building-and-packaging.md +0 -0
  72. package/docs/concepts/domain-neutrality.md +0 -0
  73. package/docs/concepts/host-port.md +0 -0
  74. package/docs/concepts/packages-and-entry-points.md +0 -0
  75. package/docs/concepts/peer-dependencies.md +0 -0
  76. package/docs/concepts/state-guard.md +65 -2
  77. package/docs/concepts/styling.md +24 -6
  78. package/docs/quickstart.md +0 -0
  79. package/editor.js +0 -0
  80. package/flow.js +0 -0
  81. package/host.js +0 -0
  82. package/package.json +2 -2
  83. package/src/canvas/canvas_adapter.js +0 -0
  84. package/src/canvas/edge_router.js +0 -0
  85. package/src/canvas/graph/connector_router.js +0 -0
  86. package/src/canvas/graph/history_service.js +0 -0
  87. package/src/canvas/graph/node_platform.js +0 -0
  88. package/src/canvas/index.js +0 -0
  89. package/src/canvas/layered_layout.js +0 -0
  90. package/src/charts/data_series_plot_window.js +0 -0
  91. package/src/charts/downsample.js +0 -0
  92. package/src/charts/plot_config.js +0 -0
  93. package/src/charts/plotly_wrapper.js +0 -0
  94. package/src/core/event_bus.js +0 -0
  95. package/src/core/logging.js +0 -0
  96. package/src/core/settings.js +0 -0
  97. package/src/core/state_guard.js +0 -0
  98. package/src/core/state_guard_config.js +19 -18
  99. package/src/core/state_machine.js +0 -0
  100. package/src/editor/editor_pane.js +0 -0
  101. package/src/editor/monaco_editor_factory.js +0 -0
  102. package/src/editor/monaco_loader.js +0 -0
  103. package/src/editor/notebook_search.js +0 -0
  104. package/src/editor/notebook_undo_manager.js +0 -0
  105. package/src/editor/split_pane_container.js +0 -0
  106. package/src/flow/canvas_editor/connections.js +0 -0
  107. package/src/flow/canvas_editor/editor.js +0 -0
  108. package/src/flow/canvas_editor/index.js +0 -0
  109. package/src/flow/canvas_editor/layout.js +0 -0
  110. package/src/flow/canvas_editor/strings.js +0 -0
  111. package/src/flow/kit/always_before.js +0 -0
  112. package/src/flow/kit/catalogue.js +0 -0
  113. package/src/flow/kit/chip_input.js +0 -0
  114. package/src/flow/kit/dom.js +0 -0
  115. package/src/flow/kit/findings.js +0 -0
  116. package/src/flow/kit/history.js +0 -0
  117. package/src/flow/kit/index.js +0 -0
  118. package/src/flow/kit/keys.js +0 -0
  119. package/src/flow/kit/logic_graph.js +0 -0
  120. package/src/flow/kit/references.js +0 -0
  121. package/src/flow/kit/settings_panel.js +0 -0
  122. package/src/flow/kit/settings_schema.js +0 -0
  123. package/src/flow/kit/step_picker.js +0 -0
  124. package/src/flow/kit/strings.js +0 -0
  125. package/src/flow/kit/value_picker.js +0 -0
  126. package/src/flow/kit/widgets.js +0 -0
  127. package/src/flow/lanes/editor.js +0 -0
  128. package/src/flow/lanes/index.js +0 -0
  129. package/src/flow/lanes/layout.js +0 -0
  130. package/src/flow/lanes/pipeline.js +0 -0
  131. package/src/flow/lanes/preview.js +0 -0
  132. package/src/flow/lanes/strings.js +0 -0
  133. package/src/flow/outline/editor.js +0 -0
  134. package/src/flow/outline/index.js +0 -0
  135. package/src/flow/outline/mapping.js +0 -0
  136. package/src/flow/outline/menu.js +0 -0
  137. package/src/flow/outline/operations.js +0 -0
  138. package/src/flow/outline/recognise.js +0 -0
  139. package/src/flow/outline/strings.js +0 -0
  140. package/src/help/help_modal.js +0 -0
  141. package/src/help/help_registry.js +0 -0
  142. package/src/host/external_links.js +0 -0
  143. package/src/host/file_picker.js +0 -0
  144. package/src/host/host.js +0 -0
  145. package/src/host/photino_host.js +0 -0
  146. package/src/host/photino_host.md +0 -0
  147. package/src/host/pywebview_host.js +0 -0
  148. package/src/tiles/chart_tiles.js +0 -0
  149. package/src/tiles/config_schema.js +0 -0
  150. package/src/tiles/figure_tile.js +0 -0
  151. package/src/tiles/layout_persistence.js +0 -0
  152. package/src/tiles/list_tiles.js +0 -0
  153. package/src/tiles/scope_picker.js +0 -0
  154. package/src/tiles/tile_config_form.js +0 -0
  155. package/src/tiles/tile_grid.js +0 -0
  156. package/src/tiles/tile_registry.js +0 -0
  157. package/src/tiling/bottom_panel_tabs.js +0 -0
  158. package/src/tiling/command_palette.js +0 -0
  159. package/src/tiling/content_registry.js +0 -0
  160. package/src/tiling/desktops.js +79 -16
  161. package/src/tiling/entity_sources.js +0 -0
  162. package/src/tiling/keymap.js +70 -2
  163. package/src/tiling/kind_taxonomy.js +0 -0
  164. package/src/tiling/landing_table.js +0 -0
  165. package/src/tiling/loading_overlay.js +0 -0
  166. package/src/tiling/nav_panel.js +0 -0
  167. package/src/tiling/page_factory.js +0 -0
  168. package/src/tiling/panel_availability.js +0 -0
  169. package/src/tiling/panel_keys.js +0 -0
  170. package/src/tiling/shell.js +9 -2
  171. package/src/tiling/tab_strip.js +0 -0
  172. package/src/tiling/tile_breadcrumb.js +12 -4
  173. package/src/tiling/tile_renderer.js +0 -0
  174. package/src/tiling/tile_tab_menu.js +0 -0
  175. package/src/tiling/tile_tree.js +63 -5
  176. package/src/tiling/wm.js +59 -6
  177. package/src/tiling/zoom.js +0 -0
  178. package/src/ui/base/component_base.js +0 -0
  179. package/src/ui/base/controller_base.js +0 -0
  180. package/src/ui/base/page_base.js +0 -0
  181. package/src/ui/components/about_dialog.js +0 -0
  182. package/src/ui/components/alert_log.js +0 -0
  183. package/src/ui/components/alerts_pane.js +0 -0
  184. package/src/ui/components/attribute_list_editor.js +0 -0
  185. package/src/ui/components/autocomplete_field.js +0 -0
  186. package/src/ui/components/cell_editor_host.js +0 -0
  187. package/src/ui/components/cell_editors.js +0 -0
  188. package/src/ui/components/cell_grid.js +0 -0
  189. package/src/ui/components/cell_paste.js +0 -0
  190. package/src/ui/components/cell_selection.js +0 -0
  191. package/src/ui/components/choice_controls.js +0 -0
  192. package/src/ui/components/color_field.js +0 -0
  193. package/src/ui/components/color_list_field.js +0 -0
  194. package/src/ui/components/column_fit.js +0 -0
  195. package/src/ui/components/column_groups.js +0 -0
  196. package/src/ui/components/column_order.js +0 -0
  197. package/src/ui/components/computing_status_window.js +0 -0
  198. package/src/ui/components/confirm_dialog.js +0 -0
  199. package/src/ui/components/conversation_view.js +0 -0
  200. package/src/ui/components/crosstab.js +0 -0
  201. package/src/ui/components/detail_header.js +0 -0
  202. package/src/ui/components/document_outline.js +0 -0
  203. package/src/ui/components/drag_reorder.js +0 -0
  204. package/src/ui/components/fact_table.js +0 -0
  205. package/src/ui/components/fill_handle.js +0 -0
  206. package/src/ui/components/gallery_picker.js +0 -0
  207. package/src/ui/components/header_actions.js +0 -0
  208. package/src/ui/components/inline_renamer.js +0 -0
  209. package/src/ui/components/job_progress.js +0 -0
  210. package/src/ui/components/menu_bar.js +0 -0
  211. package/src/ui/components/modal.js +11 -2
  212. package/src/ui/components/notification_history.js +0 -0
  213. package/src/ui/components/progress.js +0 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +0 -0
  217. package/src/ui/components/series.js +0 -0
  218. package/src/ui/components/settings_view.js +0 -0
  219. package/src/ui/components/slide_out_panel.js +0 -0
  220. package/src/ui/components/slider_field.js +0 -0
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +0 -0
  223. package/src/ui/components/staged_history.js +0 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +0 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +0 -0
  229. package/src/ui/components/tag_input.js +0 -0
  230. package/src/ui/components/toast.js +0 -0
  231. package/src/ui/components/toggle_field.js +0 -0
  232. package/src/ui/components/tree_view.js +41 -18
  233. package/src/ui/components/tsv.js +0 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +0 -0
  236. package/src/ui/controllers/window_chrome_controller.js +0 -0
  237. package/src/ui/notification_center.js +0 -0
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +0 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +0 -0
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +0 -0
  245. package/src/ui/utils/tooltip_service.js +0 -0
  246. package/tiles.js +0 -0
  247. package/widgets.js +0 -0
  248. package/wm.js +0 -0
  249. package/dist/chunk-ZMH3OC7H.js.map +0 -7
  250. /package/dist/{chunk-IG74S3ZH.js.map → chunk-AD7FIA3Z.js.map} +0 -0
@@ -23,6 +23,22 @@ const DEFAULT_ALLOWED_ATTRIBUTES = Object.freeze([
23
23
  const DEFAULT_ALLOWED_ATTRIBUTE_PREFIXES = Object.freeze([
24
24
  ]);
25
25
 
26
+ // What belongs here: the keys FlexDesk's own widgets write (the last group,
27
+ // which tests/state_guard_defaults.test.mjs keeps complete by scanning src/),
28
+ // the third-party attributes Plotly sets, and generic UI words. What does not:
29
+ // one application's own keys. Whether `data-order-id` is a legitimate write is
30
+ // that application's call, and it says so by adding the key (see
31
+ // docs/concepts/state-guard.md). A key listed here for one embedder's sake that
32
+ // no widget writes is a key every OTHER embedder's strict guard lets through
33
+ // without a word, which is the one thing a strict guard is there to stop.
34
+ //
35
+ // The list does not yet meet that, and it has not been audited key by key:
36
+ // many keys no widget writes are still here, some of them carried over from one
37
+ // application's markup (series-id, dataset-id, func-name and connector-id are
38
+ // examples). Thirteen that name one application's concepts were taken out. The
39
+ // test pins nine of the rest as still allowed, so dropping one is a deliberate
40
+ // edit, and the docs tell an embedder that a strict guard built from the
41
+ // defaults lets a write of such a key through. Do not add another like them.
26
42
  const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
27
43
  'testid',
28
44
  'phase',
@@ -70,11 +86,8 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
70
86
  'node-name',
71
87
  'lang',
72
88
  'playback-control',
73
- // Simulation/scenario state keys
89
+ // Generic state words
74
90
  'status',
75
- 'scenario-id',
76
- 'run-id',
77
- 'run-state',
78
91
  'selected',
79
92
  'active',
80
93
  'disabled',
@@ -90,13 +103,10 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
90
103
  'input-index',
91
104
  'output-index',
92
105
  'port-index',
93
- // Scenario editor keys
106
+ // Identifier and binding keys
94
107
  'source',
95
108
  'series-id',
96
109
  'dataset-id',
97
- 'mirror-input-bound',
98
- 'mirror-change-bound',
99
- 'mirror-input',
100
110
  'bound',
101
111
  'field',
102
112
  'key',
@@ -105,14 +115,11 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
105
115
  'tab-id',
106
116
  'param-id',
107
117
  'target-id',
108
- 'override-type',
109
118
  // Tree view component keys
110
119
  'item-id',
111
120
  'category-id',
112
- // Calibration page keys
113
- 'info-for',
114
- 'column',
115
121
  // Data page keys
122
+ 'column',
116
123
  'readonly',
117
124
  'id',
118
125
  'action',
@@ -131,12 +138,6 @@ const DEFAULT_ALLOWED_DATASET_KEYS = Object.freeze([
131
138
  'label',
132
139
  'title',
133
140
  'description',
134
- // DSL generator keys
135
- 'dsl-state',
136
- 'dsl-section-registered',
137
- 'dsl-scrollbars-installed',
138
- 'dsl-tabs-bound',
139
- 'dsl-controls-bound',
140
141
  // Inline highlighter keys
141
142
  'inline-hl',
142
143
  // Overlay scrollbar keys (UI-only, not state storage)
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
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
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
@@ -24,14 +24,19 @@
24
24
  /** Build a breadcrumb strip element + render initial state. Returns
25
25
  * `{ el, destroy }`. The strip is wired to the WM (clicks open content
26
26
  * in the primary tile) and, when the embedder supplies a `rootCrumb`
27
- * with a `subscribe` hook, to whatever keeps the root label fresh. */
27
+ * with a `subscribe` hook, to whatever keeps the root label fresh.
28
+ * While the path is a single segment (the tile itself) the strip is
29
+ * `hidden` and takes no room; `refresh()` shows it once there are more. */
28
30
  export function mountTileBreadcrumb(kind, props, ctx) {
29
31
  const eventBus = ctx?.eventBus;
30
32
  const taxonomy = ctx?.taxonomy;
31
33
  const rootCrumb = ctx?.rootCrumb || null;
32
34
  if (!taxonomy) {
33
35
  console.error('[breadcrumb] no taxonomy in ctx — cannot render');
34
- return { el: document.createElement('nav'), destroy: () => {} };
36
+ const el = document.createElement('nav');
37
+ el.className = 'twm-topbar-breadcrumb twm-tile-breadcrumb';
38
+ el.hidden = true; // nothing to draw: no strip, as for a one-segment path
39
+ return { el, refresh: () => {}, destroy: () => {} };
35
40
  }
36
41
  const root = document.createElement('nav');
37
42
  root.className = 'twm-topbar-breadcrumb twm-tile-breadcrumb';
@@ -82,9 +87,12 @@ export function mountTileBreadcrumb(kind, props, ctx) {
82
87
  try { return ctx.trailSegments() || []; }
83
88
  catch (err) { console.warn('[breadcrumb] trailSegments threw', err); return []; }
84
89
  };
90
+ // One segment is the tile itself, which its title already names: the strip
91
+ // is hidden (and its page-shell slot with it) until there is a level above.
85
92
  const render = () => {
86
- _renderInto(root,
87
- _segments(kind, props, taxonomy, rootCrumb, rootLabel, navigate, trailOf()));
93
+ const segments = _segments(kind, props, taxonomy, rootCrumb, rootLabel, navigate, trailOf());
94
+ _renderInto(root, segments);
95
+ root.hidden = segments.length < 2;
88
96
  };
89
97
  render();
90
98
 
File without changes
File without changes