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
@@ -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
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
@@ -56,8 +56,12 @@
56
56
  * Rows — an entry of the form `{ row: [field, field, ...] }` puts its fields
57
57
  * side by side (a min and a max, say). Each is an ordinary field: its own
58
58
  * label, hint, error, `setVisible`, value and submitted key — the row is
59
- * layout only and has no name. The row stacks its fields when the form is
60
- * narrower than 360px (a container query on `.twm-modal__form`).
59
+ * layout only and has no name. Its inputs are equally wide (up to each
60
+ * control's own padding): the space left after the labels is shared equally.
61
+ * A form whose widest row has three or more fields opens wider (about 230px
62
+ * per field; at least 640px, otherwise at most 90% of the window).
63
+ * The row stacks its fields when the form is narrower than 360px (a
64
+ * container query on `.twm-modal__form`).
61
65
  *
62
66
  * Picker — a value the user LOOKS UP rather than types (an id chosen in a
63
67
  * search dialog, say). The row shows a read-only box and a search button;
@@ -913,6 +917,11 @@ function _estimateWidth(fields) {
913
917
  // Forms with a textarea, a colour list, a section divider or a row of
914
918
  // fields get the wider layout so multi-line content + grouped fields
915
919
  // breathe; otherwise 480 px is enough for label + single-line input pairs.
920
+ // A row of three or more fields needs more than 640 px for inputs of a
921
+ // usable width (BUG-0050): ~230 px per field (its label and input) past the
922
+ // label column, clamped to 90% of the window.
923
+ const widest = Math.max(0, ...fields.filter((f) => Array.isArray(f?.row)).map((f) => _formFields(f.row).length));
924
+ if (widest >= 3) return Math.max(640, Math.min(170 + widest * 230, Math.floor(window.innerWidth * 0.9)));
916
925
  const wide = fields.some((f) => f && (f.type === 'textarea' || f.type === 'colorList' || f.section || Array.isArray(f.row)));
917
926
  return wide ? 640 : 480;
918
927
  }
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
@@ -128,28 +128,39 @@ export function createTreeNode(options) {
128
128
  `;
129
129
  setText(node, '.twm-tree-node__label', label);
130
130
 
131
- // Add action buttons if specified
132
- if (actions) {
133
- const actionsEl = node.querySelector('.twm-tree-node__actions');
134
- Object.entries(actions).forEach(([actionId, actionConfig]) => {
135
- const btn = document.createElement('button');
136
- btn.className = 'twm-tree-node__action-btn';
137
- btn.dataset.action = actionId;
138
- btn.title = actionConfig.title || actionId;
139
- btn.innerHTML = `<span class="material-symbols-outlined">${actionConfig.icon}</span>`;
140
- if (actionConfig.onClick) {
141
- btn.addEventListener('click', (e) => {
142
- e.stopPropagation();
143
- actionConfig.onClick(id, e);
144
- });
145
- }
146
- actionsEl.appendChild(btn);
147
- });
148
- }
131
+ if (actions) appendActionButtons(node.querySelector('.twm-tree-node__actions'), id, actions);
149
132
 
150
133
  return node;
151
134
  }
152
135
 
136
+ /**
137
+ * Fill a node's or an item's actions slot: one `.twm-tree-node__action-btn`
138
+ * per entry. A click on an entry with `onClick` runs `onClick(id, event)` and
139
+ * goes no further, so it neither selects the item nor toggles the node; nor
140
+ * does a double-click, so it never reaches TreeView's onItemDoubleClick.
141
+ * @param {HTMLElement} actionsEl
142
+ * @param {string} id - The node's or item's id
143
+ * @param {Object<string, { icon: string, title?: string, onClick?: Function }>} actions
144
+ */
145
+ function appendActionButtons(actionsEl, id, actions) {
146
+ Object.entries(actions).forEach(([actionId, actionConfig]) => {
147
+ const btn = document.createElement('button');
148
+ btn.type = 'button';
149
+ btn.className = 'twm-tree-node__action-btn';
150
+ btn.dataset.action = actionId;
151
+ btn.title = actionConfig.title || actionId;
152
+ btn.innerHTML = `<span class="material-symbols-outlined">${actionConfig.icon}</span>`;
153
+ if (actionConfig.onClick) {
154
+ btn.addEventListener('click', (e) => {
155
+ e.stopPropagation();
156
+ actionConfig.onClick(id, e);
157
+ });
158
+ btn.addEventListener('dblclick', (e) => e.stopPropagation());
159
+ }
160
+ actionsEl.appendChild(btn);
161
+ });
162
+ }
163
+
153
164
  /**
154
165
  * Create a tree item (leaf node)
155
166
  * @param {Object} options - Item options
@@ -164,6 +175,10 @@ export function createTreeNode(options) {
164
175
  * @param {Object} [options.badge] - Optional badge config
165
176
  * @param {string} [options.badge.text] - Badge text (if empty, renders as status dot)
166
177
  * @param {string} [options.badge.variant] - 'muted'|'pass'|'fail'|'error'
178
+ * @param {Object} [options.actions] - Row actions, as createTreeNode takes them
179
+ * ({ id: { icon, title, onClick(id, event) } }): buttons in one line at the
180
+ * row's end (`.twm-tree-item__actions`), shown on hover, on focus within and
181
+ * on the selected item
167
182
  * @returns {HTMLElement} - Item element
168
183
  */
169
184
  export function createTreeItem(options) {
@@ -177,6 +192,7 @@ export function createTreeItem(options) {
177
192
  subtitle = null,
178
193
  data = null,
179
194
  badge = null,
195
+ actions = null,
180
196
  } = options;
181
197
 
182
198
  const item = document.createElement('div');
@@ -220,6 +236,13 @@ export function createTreeItem(options) {
220
236
  if (subtitle) setText(item, '.twm-tree-item__subtitle', subtitle);
221
237
  if (badge?.text) setText(item, '.twm-tree-item__badge', badge.text);
222
238
 
239
+ if (actions) {
240
+ const actionsEl = document.createElement('span');
241
+ actionsEl.className = 'twm-tree-item__actions';
242
+ appendActionButtons(actionsEl, id, actions);
243
+ item.appendChild(actionsEl);
244
+ }
245
+
223
246
  return item;
224
247
  }
225
248
 
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/tiles.js CHANGED
File without changes
package/widgets.js CHANGED
File without changes
package/wm.js CHANGED
File without changes