flexdesk 0.17.17 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +0 -0
  3. package/README.md +1 -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/flexdesk.css +7499 -5851
  9. package/css/reset.css +1 -1
  10. package/css/tokens.css +23 -1
  11. package/dist/canvas.js +0 -0
  12. package/dist/canvas.js.map +0 -0
  13. package/dist/charts.js +10 -6
  14. package/dist/charts.js.map +1 -1
  15. package/dist/chunk-3PHCPZHT.js +0 -0
  16. package/dist/chunk-3PHCPZHT.js.map +0 -0
  17. package/dist/chunk-5W4EEKSK.js +778 -0
  18. package/dist/chunk-5W4EEKSK.js.map +7 -0
  19. package/dist/chunk-DNJ3N4T4.js +82 -0
  20. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +0 -0
  22. package/dist/chunk-FOOS3T5L.js.map +0 -0
  23. package/dist/chunk-GOQLR5WR.js +0 -0
  24. package/dist/chunk-GOQLR5WR.js.map +0 -0
  25. package/dist/{chunk-JKZLNKG6.js → chunk-HKPB6OFA.js} +90 -25
  26. package/dist/{chunk-JKZLNKG6.js.map → chunk-HKPB6OFA.js.map} +2 -2
  27. package/dist/chunk-HNU6MM2B.js +0 -0
  28. package/dist/chunk-HNU6MM2B.js.map +0 -0
  29. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  30. package/dist/chunk-JYWURG5T.js +0 -0
  31. package/dist/chunk-JYWURG5T.js.map +0 -0
  32. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  33. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  34. package/dist/chunk-N7HC3ADV.js +0 -0
  35. package/dist/chunk-N7HC3ADV.js.map +0 -0
  36. package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
  37. package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
  38. package/dist/chunk-PAI2ZUWV.js +260 -0
  39. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  40. package/dist/chunk-Q7BALEPX.js +527 -0
  41. package/dist/chunk-Q7BALEPX.js.map +7 -0
  42. package/dist/chunk-QXAOTGCC.js +40 -0
  43. package/dist/chunk-QXAOTGCC.js.map +7 -0
  44. package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
  45. package/dist/chunk-RKVYIXDC.js.map +7 -0
  46. package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
  47. package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
  48. package/dist/chunk-UKP22RUG.js +219 -0
  49. package/dist/chunk-UKP22RUG.js.map +7 -0
  50. package/dist/chunk-VLOAI3HS.js +43 -0
  51. package/dist/chunk-VLOAI3HS.js.map +7 -0
  52. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  53. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  54. package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
  55. package/dist/chunk-ZL3VKINP.js.map +7 -0
  56. package/dist/editor.js +3 -2
  57. package/dist/editor.js.map +1 -1
  58. package/dist/flexdesk.css +7499 -5851
  59. package/dist/flow.js +29 -97
  60. package/dist/flow.js.map +3 -3
  61. package/dist/git.js +1356 -0
  62. package/dist/git.js.map +7 -0
  63. package/dist/host.js +0 -0
  64. package/dist/host.js.map +0 -0
  65. package/dist/reset.css +1 -1
  66. package/dist/tile_registry-6WZPTUZV.js +0 -0
  67. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  68. package/dist/tiles.js +20 -15
  69. package/dist/tiles.js.map +2 -2
  70. package/dist/tokens.css +23 -1
  71. package/dist/widgets.js +3969 -1189
  72. package/dist/widgets.js.map +4 -4
  73. package/dist/wm.js +121 -33
  74. package/dist/wm.js.map +2 -2
  75. package/docs/README.md +9 -4
  76. package/docs/best-practices.md +2 -2
  77. package/docs/components/chat-shell.md +198 -0
  78. package/docs/components/command-palette.md +0 -0
  79. package/docs/components/conversation.md +10 -0
  80. package/docs/components/data-table-grid.md +0 -0
  81. package/docs/components/data-table.md +0 -0
  82. package/docs/components/editor.md +0 -0
  83. package/docs/components/filter-builder.md +246 -0
  84. package/docs/components/flow-canvas-editor.md +0 -0
  85. package/docs/components/flow-editors.md +0 -0
  86. package/docs/components/flow-lane-editor.md +0 -0
  87. package/docs/components/flow-outline-editor.md +0 -0
  88. package/docs/components/forms-and-dialogs.md +22 -0
  89. package/docs/components/git.md +355 -0
  90. package/docs/components/input-fields.md +167 -2
  91. package/docs/components/keymap.md +8 -0
  92. package/docs/components/markdown-and-outline.md +0 -0
  93. package/docs/components/menus.md +120 -3
  94. package/docs/components/panels-and-desktops.md +16 -5
  95. package/docs/components/record-list-and-fact-table.md +0 -0
  96. package/docs/components/settings-view.md +0 -0
  97. package/docs/components/shell.md +104 -1
  98. package/docs/components/wizard.md +0 -0
  99. package/docs/components/workspace-picker.md +149 -0
  100. package/docs/concepts/building-and-packaging.md +2 -2
  101. package/docs/concepts/domain-neutrality.md +0 -0
  102. package/docs/concepts/host-port.md +0 -0
  103. package/docs/concepts/packages-and-entry-points.md +14 -13
  104. package/docs/concepts/peer-dependencies.md +0 -0
  105. package/docs/concepts/state-guard.md +3 -3
  106. package/docs/quickstart.md +1 -1
  107. package/editor.js +0 -0
  108. package/flow.js +0 -0
  109. package/git.js +13 -0
  110. package/host.js +0 -0
  111. package/package.json +6 -1
  112. package/src/canvas/canvas_adapter.js +0 -0
  113. package/src/canvas/edge_router.js +0 -0
  114. package/src/canvas/graph/connector_router.js +0 -0
  115. package/src/canvas/graph/history_service.js +0 -0
  116. package/src/canvas/graph/node_platform.js +0 -0
  117. package/src/canvas/index.js +0 -0
  118. package/src/canvas/layered_layout.js +0 -0
  119. package/src/charts/data_series_plot_window.js +0 -0
  120. package/src/charts/downsample.js +0 -0
  121. package/src/charts/plot_config.js +0 -0
  122. package/src/charts/plotly_wrapper.js +0 -0
  123. package/src/core/event_bus.js +0 -0
  124. package/src/core/logging.js +0 -0
  125. package/src/core/settings.js +0 -0
  126. package/src/core/state_guard.js +0 -0
  127. package/src/core/state_machine.js +0 -0
  128. package/src/editor/editor_pane.js +0 -0
  129. package/src/editor/monaco_editor_factory.js +0 -0
  130. package/src/editor/monaco_loader.js +0 -0
  131. package/src/editor/notebook_search.js +0 -0
  132. package/src/editor/notebook_undo_manager.js +0 -0
  133. package/src/editor/split_pane_container.js +0 -0
  134. package/src/flow/canvas_editor/connections.js +0 -0
  135. package/src/flow/canvas_editor/editor.js +0 -0
  136. package/src/flow/canvas_editor/index.js +0 -0
  137. package/src/flow/canvas_editor/layout.js +0 -0
  138. package/src/flow/canvas_editor/strings.js +0 -0
  139. package/src/flow/kit/always_before.js +0 -0
  140. package/src/flow/kit/catalogue.js +0 -0
  141. package/src/flow/kit/chip_input.js +0 -0
  142. package/src/flow/kit/dom.js +28 -119
  143. package/src/flow/kit/findings.js +0 -0
  144. package/src/flow/kit/history.js +0 -0
  145. package/src/flow/kit/index.js +0 -0
  146. package/src/flow/kit/keys.js +0 -0
  147. package/src/flow/kit/logic_graph.js +0 -0
  148. package/src/flow/kit/references.js +0 -0
  149. package/src/flow/kit/settings_panel.js +0 -0
  150. package/src/flow/kit/settings_schema.js +0 -0
  151. package/src/flow/kit/step_picker.js +1 -1
  152. package/src/flow/kit/strings.js +0 -0
  153. package/src/flow/kit/value_picker.js +1 -1
  154. package/src/flow/kit/widgets.js +0 -0
  155. package/src/flow/lanes/editor.js +0 -0
  156. package/src/flow/lanes/index.js +0 -0
  157. package/src/flow/lanes/layout.js +0 -0
  158. package/src/flow/lanes/pipeline.js +0 -0
  159. package/src/flow/lanes/preview.js +0 -0
  160. package/src/flow/lanes/strings.js +0 -0
  161. package/src/flow/outline/editor.js +0 -0
  162. package/src/flow/outline/index.js +0 -0
  163. package/src/flow/outline/mapping.js +0 -0
  164. package/src/flow/outline/menu.js +1 -1
  165. package/src/flow/outline/operations.js +0 -0
  166. package/src/flow/outline/recognise.js +0 -0
  167. package/src/flow/outline/strings.js +0 -0
  168. package/src/git/diff_viewer.js +212 -0
  169. package/src/git/diff_window.js +51 -0
  170. package/src/git/git_graph.js +401 -0
  171. package/src/git/git_ops.js +61 -0
  172. package/src/git/git_panel.js +755 -0
  173. package/src/git/index.js +9 -0
  174. package/src/git/line_diff.js +202 -0
  175. package/src/help/help_modal.js +0 -0
  176. package/src/help/help_registry.js +0 -0
  177. package/src/host/external_links.js +0 -0
  178. package/src/host/file_picker.js +0 -0
  179. package/src/host/host.js +0 -0
  180. package/src/host/photino_host.js +0 -0
  181. package/src/host/photino_host.md +0 -0
  182. package/src/host/pywebview_host.js +0 -0
  183. package/src/tiles/chart_tiles.js +0 -0
  184. package/src/tiles/config_schema.js +0 -0
  185. package/src/tiles/figure_tile.js +0 -0
  186. package/src/tiles/layout_persistence.js +0 -0
  187. package/src/tiles/list_tiles.js +0 -0
  188. package/src/tiles/scope_picker.js +0 -0
  189. package/src/tiles/tile_base.js +5 -4
  190. package/src/tiles/tile_config_form.js +0 -0
  191. package/src/tiles/tile_registry.js +0 -0
  192. package/src/tiling/bottom_panel_tabs.js +0 -0
  193. package/src/tiling/command_palette.js +0 -0
  194. package/src/tiling/content_registry.js +0 -0
  195. package/src/tiling/desktops.js +10 -8
  196. package/src/tiling/entity_sources.js +0 -0
  197. package/src/tiling/keymap.js +6 -5
  198. package/src/tiling/kind_taxonomy.js +15 -2
  199. package/src/tiling/landing_table.js +0 -0
  200. package/src/tiling/loading_overlay.js +0 -0
  201. package/src/tiling/nav_panel.js +0 -0
  202. package/src/tiling/page_factory.js +0 -0
  203. package/src/tiling/panel_availability.js +0 -0
  204. package/src/tiling/panel_keys.js +0 -0
  205. package/src/tiling/shell.js +124 -17
  206. package/src/tiling/tab_strip.js +0 -0
  207. package/src/tiling/tile_renderer.js +2 -3
  208. package/src/tiling/tile_tab_menu.js +0 -0
  209. package/src/tiling/zoom.js +0 -0
  210. package/src/ui/base/component_base.js +0 -0
  211. package/src/ui/base/controller_base.js +0 -0
  212. package/src/ui/base/page_base.js +0 -0
  213. package/src/ui/components/about_dialog.js +0 -0
  214. package/src/ui/components/action_dropdown.js +4 -78
  215. package/src/ui/components/alert_log.js +0 -0
  216. package/src/ui/components/alerts_pane.js +0 -0
  217. package/src/ui/components/anchored_layer.js +255 -0
  218. package/src/ui/components/attribute_list_editor.js +0 -0
  219. package/src/ui/components/autocomplete_field.js +0 -0
  220. package/src/ui/components/cell_editor_host.js +0 -0
  221. package/src/ui/components/cell_editors.js +0 -0
  222. package/src/ui/components/cell_grid.js +0 -0
  223. package/src/ui/components/cell_paste.js +0 -0
  224. package/src/ui/components/cell_selection.js +0 -0
  225. package/src/ui/components/chat_shell.js +920 -0
  226. package/src/ui/components/chat_state_machine.js +223 -0
  227. package/src/ui/components/choice_controls.js +45 -2
  228. package/src/ui/components/color_field.js +3 -2
  229. package/src/ui/components/color_list_field.js +3 -2
  230. package/src/ui/components/column_fit.js +0 -0
  231. package/src/ui/components/column_groups.js +0 -0
  232. package/src/ui/components/column_order.js +0 -0
  233. package/src/ui/components/combobox.js +422 -0
  234. package/src/ui/components/conversation_view.js +17 -2
  235. package/src/ui/components/crosstab.js +0 -0
  236. package/src/ui/components/data_table.js +2 -2
  237. package/src/ui/components/detail_header.js +0 -0
  238. package/src/ui/components/document_outline.js +0 -0
  239. package/src/ui/components/drag_reorder.js +0 -0
  240. package/src/ui/components/fact_table.js +0 -0
  241. package/src/ui/components/fill_handle.js +0 -0
  242. package/src/ui/components/filter_builder.js +766 -0
  243. package/src/ui/components/filter_model.js +614 -0
  244. package/src/ui/components/gallery_picker.js +0 -0
  245. package/src/ui/components/header_actions.js +0 -0
  246. package/src/ui/components/inline_renamer.js +0 -0
  247. package/src/ui/components/job_progress.js +0 -0
  248. package/src/ui/components/managed_window.js +24 -47
  249. package/src/ui/components/menu_bar.js +101 -24
  250. package/src/ui/components/modal.js +79 -109
  251. package/src/ui/components/option_list.js +160 -0
  252. package/src/ui/components/popover.js +94 -0
  253. package/src/ui/components/progress.js +0 -0
  254. package/src/ui/components/recent_items.js +169 -0
  255. package/src/ui/components/record_list.js +0 -0
  256. package/src/ui/components/row_activation.js +0 -0
  257. package/src/ui/components/section_stack.js +0 -0
  258. package/src/ui/components/select.js +322 -0
  259. package/src/ui/components/series.js +0 -0
  260. package/src/ui/components/settings_view.js +0 -0
  261. package/src/ui/components/slide_out_panel.js +0 -0
  262. package/src/ui/components/slider_field.js +0 -0
  263. package/src/ui/components/sortable_list.js +0 -0
  264. package/src/ui/components/staged_edits.js +0 -0
  265. package/src/ui/components/staged_history.js +0 -0
  266. package/src/ui/components/table_filter_chips.js +0 -0
  267. package/src/ui/components/table_state_store.js +0 -0
  268. package/src/ui/components/table_structure.js +0 -0
  269. package/src/ui/components/table_values.js +0 -0
  270. package/src/ui/components/tabs.js +0 -0
  271. package/src/ui/components/tag_input.js +77 -8
  272. package/src/ui/components/toast.js +0 -0
  273. package/src/ui/components/toggle_field.js +0 -0
  274. package/src/ui/components/tsv.js +0 -0
  275. package/src/ui/components/window_taskbar.js +0 -0
  276. package/src/ui/components/workspace_picker.js +1155 -0
  277. package/src/ui/controllers/window_chrome_controller.js +4 -1
  278. package/src/ui/utils/auto_scrollbars.js +0 -0
  279. package/src/ui/utils/fire_event.js +17 -0
  280. package/src/ui/utils/focusable.js +25 -0
  281. package/src/ui/utils/markdown.js +0 -0
  282. package/src/ui/utils/modal_host.js +27 -0
  283. package/src/ui/utils/native_context_menu.js +0 -0
  284. package/src/ui/utils/overlay_scrollbar.js +0 -0
  285. package/src/ui/utils/raf_resize_observer.js +0 -0
  286. package/src/ui/utils/scroll_lock.js +0 -0
  287. package/src/ui/utils/strip_scroll.js +0 -0
  288. package/src/ui/utils/tooltip_service.js +0 -0
  289. package/tiles.js +0 -0
  290. package/widgets.js +12 -2
  291. package/wm.js +0 -0
  292. package/css/base.css +0 -10527
  293. package/css/overrides.css +0 -2596
  294. package/dist/chunk-7JI7HHSY.js +0 -107
  295. package/dist/chunk-7JI7HHSY.js.map +0 -7
  296. package/dist/chunk-MIW7D6WC.js +0 -526
  297. package/dist/chunk-MIW7D6WC.js.map +0 -7
  298. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  299. package/dist/chunk-XJNADRGR.js.map +0 -7
  300. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -18,6 +18,8 @@
18
18
  */
19
19
 
20
20
  import { getSetting } from '../../core/settings.js';
21
+ import { isInAnchoredLayer } from './anchored_layer.js';
22
+ import { collectFocusable } from '../utils/focusable.js';
21
23
 
22
24
  /** Where window geometry lives when the embedder names no key. An embedder
23
25
  * that saved geometry under another key keeps it by passing that key to
@@ -70,29 +72,6 @@ let _activeWindows = new Map(); // id -> ManagedWindow instance
70
72
  * the same event reaches it — one Esc must close one window. */
71
73
  const _escAnswered = new WeakSet();
72
74
 
73
- /** Collect the focusable elements inside `root`, in DOM order. Used
74
- * by the modal focus trap so Tab cycles within the modal frame and
75
- * can't escape into the underlying tiles. */
76
- function _collectFocusable(root) {
77
- if (!root) return [];
78
- const sel = [
79
- 'a[href]',
80
- 'button:not([disabled])',
81
- 'input:not([disabled]):not([type="hidden"])',
82
- 'select:not([disabled])',
83
- 'textarea:not([disabled])',
84
- '[tabindex]:not([tabindex="-1"])',
85
- ].join(',');
86
- return Array.from(root.querySelectorAll(sel)).filter((el) => {
87
- // Skip hidden / zero-size elements — Tab visits only what the
88
- // user can actually see.
89
- if (el.hidden) return false;
90
- if (el.closest('[hidden]')) return false;
91
- const r = el.getBoundingClientRect();
92
- return r.width > 0 || r.height > 0;
93
- });
94
- }
95
-
96
75
  // Global state cache
97
76
  let _stateCache = null;
98
77
 
@@ -548,12 +527,17 @@ export class ManagedWindow {
548
527
  // taskbar can filter on receipt: an in-panel taskbar shows only the
549
528
  // windows mounted in its own panel, and the viewport taskbar shows only
550
529
  // the ones with no container.
551
- window.dispatchEvent(new CustomEvent('managed-window-closed', {
552
- detail: { id: this.id, container: this.container }
553
- }));
530
+ this._announce('managed-window-closed', { id: this.id, container: this.container });
554
531
  this._release();
555
532
  }
556
533
 
534
+ /** Every window event goes out on the page's window, and a window takes only
535
+ * an event built by its own `CustomEvent`: Node's global one is refused
536
+ * under jsdom, in the middle of the close that fired it. */
537
+ _announce(name, detail) {
538
+ window.dispatchEvent(new window.CustomEvent(name, { detail }));
539
+ }
540
+
557
541
  /** BUG-0053. A closed window leaves the registry and lets go of what it
558
542
  * held: its content, its DOM and every consumer callback (an `onClose`
559
543
  * typically captures its owner). Until now the registry kept all of it
@@ -607,12 +591,10 @@ export class ManagedWindow {
607
591
  }
608
592
 
609
593
  // Dispatch event first so the taskbar button is created synchronously
610
- window.dispatchEvent(new CustomEvent('managed-window-minimized', {
611
- detail: {
612
- id: this.id, title: this.title, icon: this.icon,
613
- container: this.container,
614
- }
615
- }));
594
+ this._announce('managed-window-minimized', {
595
+ id: this.id, title: this.title, icon: this.icon,
596
+ container: this.container,
597
+ });
616
598
 
617
599
  if (!this.element) return;
618
600
 
@@ -744,9 +726,7 @@ export class ManagedWindow {
744
726
  this.bringToFront();
745
727
 
746
728
  // Notify taskbar (removes the taskbar button)
747
- window.dispatchEvent(new CustomEvent('managed-window-restored', {
748
- detail: { id: this.id, container: this.container }
749
- }));
729
+ this._announce('managed-window-restored', { id: this.id, container: this.container });
750
730
  }
751
731
 
752
732
  /**
@@ -825,7 +805,7 @@ export class ManagedWindow {
825
805
 
826
806
  // A CLASS THE STYLESHEET HAS ALWAYS KNOWN AND NOTHING EVER SET.
827
807
  // `.twm-managed-window--maximized .twm-managed-window__resize
828
- // { display: none }` is in `css/base.css` — "a maximised window is not
808
+ // { display: none }` is in `css/flexdesk.css` — "a maximised window is not
829
809
  // resized by dragging its edges" — and it has never matched anything,
830
810
  // because no code path adds the modifier. So a maximised window keeps
831
811
  // eight live resize handles hanging 3px outside its edges. Exactly the
@@ -841,9 +821,7 @@ export class ManagedWindow {
841
821
  // tiles — had no edge to hang the rule on and would have had to poll.
842
822
  // Same detail shape as the other three window events, `container`
843
823
  // included, so a per-region consumer can filter on receipt.
844
- window.dispatchEvent(new CustomEvent('managed-window-maximized', {
845
- detail: { id: this.id, maximized: this.isMaximized, container: this.container },
846
- }));
824
+ this._announce('managed-window-maximized', { id: this.id, maximized: this.isMaximized, container: this.container });
847
825
  }
848
826
 
849
827
  /**
@@ -965,7 +943,7 @@ export class ManagedWindow {
965
943
  }
966
944
  this.element.setAttribute('data-window-id', this.id);
967
945
  // BUG-0053. Focusable by script and by a press, never by Tab (the
968
- // modal trap's `_collectFocusable` skips tabindex -1): a press on the
946
+ // modal trap's `collectFocusable` skips tabindex -1): a press on the
969
947
  // title bar or on empty content keeps focus in the window, where a
970
948
  // non-modal window's Esc looks for it.
971
949
  this.element.tabIndex = -1;
@@ -1274,9 +1252,7 @@ export class ManagedWindow {
1274
1252
  this.width = bounds.width; this.height = bounds.height;
1275
1253
  }
1276
1254
  this._applyPosition();
1277
- window.dispatchEvent(new CustomEvent('managed-window-moved', {
1278
- detail: { id: this.id, container },
1279
- }));
1255
+ this._announce('managed-window-moved', { id: this.id, container });
1280
1256
  return true;
1281
1257
  }
1282
1258
 
@@ -1736,9 +1712,7 @@ export class ManagedWindow {
1736
1712
  this.height = rect.height;
1737
1713
  this._applyPosition();
1738
1714
  this.element?.classList.add('twm-managed-window--snapped');
1739
- window.dispatchEvent(new CustomEvent('managed-window-snapped', {
1740
- detail: { id: this.id, zone, container: this.container },
1741
- }));
1715
+ this._announce('managed-window-snapped', { id: this.id, zone, container: this.container });
1742
1716
  }
1743
1717
 
1744
1718
  /** Restore the geometry a snap replaced. Called when a snapped window is
@@ -1850,7 +1824,10 @@ export class ManagedWindow {
1850
1824
  // active element into the background — bad UX, and a
1851
1825
  // genuine WCAG focus-trap violation for accessibility tools.
1852
1826
  if (e.key === 'Tab' && this.modal && this.element) {
1853
- const focusables = _collectFocusable(this.element);
1827
+ // A popover opened from this modal is over it but outside its frame,
1828
+ // and sees to its own Tab.
1829
+ if (isInAnchoredLayer(document.activeElement)) return;
1830
+ const focusables = collectFocusable(this.element);
1854
1831
  if (focusables.length === 0) {
1855
1832
  e.preventDefault();
1856
1833
  return;
@@ -62,6 +62,15 @@
62
62
  * shortcut, context, action }`), so the palette and the bar list identical
63
63
  * verbs.
64
64
  *
65
+ * ── Two styles: a bar, or one hamburger button (STORY-0133) ───────────
66
+ * `style: 'bar'` (the default) draws a title per menu: File Edit Help.
67
+ * `style: 'hamburger'` draws ONE icon button (`icon`, 'menu' by default,
68
+ * named by `label`) whose menu lists the menus, each flying out its items:
69
+ * ☰ › File › Open…. It is the same model, so `commands()`, `setEnabled`,
70
+ * `setChecked` and `onAction` do not change; `open(menuId)` opens the
71
+ * hamburger with that menu flown out. `setStyle(style)` switches at run
72
+ * time (a setting, a narrow window), closing whatever is open.
73
+ *
65
74
  * ── Roles ─────────────────────────────────────────────────────────────
66
75
  * The WAI-ARIA menubar pattern: `role="menubar"` on the element you pass,
67
76
  * a `role="menuitem"` title per menu inside a `role="none"` wrapper, each
@@ -77,6 +86,9 @@
77
86
  const SEL_TITLE = '.twm-menubar__title';
78
87
  const SEL_ITEM = '.twm-menubar__item';
79
88
  const SEL_MENU = '.twm-menubar__menu';
89
+ const STYLES = ['bar', 'hamburger'];
90
+ /** The hamburger's own model ids. A menu's row in it is `${HAMBURGER}:${menu.id}`. */
91
+ const HAMBURGER = 'twm-menubar:hamburger';
80
92
 
81
93
  /** @returns {MenuBar} */
82
94
  export function createMenuBar(el, opts) {
@@ -91,16 +103,22 @@ export class MenuBar {
91
103
  * @param {Function} [opts.onAction] `(id, { item, checked }) => void`
92
104
  * @param {string} [opts.label] the bar's accessible name ('Menu')
93
105
  * @param {string} [opts.emptyLabel] the disabled row an empty submenu shows ('Empty')
106
+ * @param {string} [opts.style] 'bar' (a title per menu, the default) or 'hamburger' (one button)
107
+ * @param {string} [opts.icon] the hamburger's Material Symbols icon ('menu')
94
108
  */
95
- constructor(el, { menus, onAction = null, label = 'Menu', emptyLabel = 'Empty' } = {}) {
109
+ constructor(el, { menus, onAction = null, label = 'Menu', emptyLabel = 'Empty', style = 'bar', icon = 'menu' } = {}) {
96
110
  if (!el || typeof el.appendChild !== 'function') {
97
111
  throw new TypeError('createMenuBar: `el` must be an element');
98
112
  }
99
113
  _validateMenus(menus);
114
+ _validateStyle(style);
100
115
  this.el = el;
101
116
  this.menus = menus;
102
117
  this._onAction = typeof onAction === 'function' ? onAction : null;
103
118
  this._emptyLabel = emptyLabel;
119
+ this._label = label;
120
+ this._icon = icon;
121
+ this._style = style;
104
122
  this._doc = el.ownerDocument;
105
123
  this._disabled = new Map(); // id -> boolean (override)
106
124
  this._checked = new Map(); // id -> boolean (override, check items)
@@ -112,26 +130,10 @@ export class MenuBar {
112
130
  this._returnTo = null; // what had focus before the bar did
113
131
  this._altArmed = false;
114
132
 
115
- el.innerHTML = '';
116
133
  el.classList.add('twm-menubar');
117
134
  el.setAttribute('role', 'menubar');
118
135
  el.setAttribute('aria-label', label);
119
- this._titles = menus.map((m, i) => {
120
- const entry = this._doc.createElement('div');
121
- entry.className = 'twm-menubar__entry';
122
- entry.setAttribute('role', 'none');
123
- const btn = this._doc.createElement('button');
124
- btn.type = 'button';
125
- btn.className = 'twm-menubar__title';
126
- btn.setAttribute('role', 'menuitem');
127
- btn.setAttribute('aria-haspopup', 'menu');
128
- btn.setAttribute('aria-expanded', 'false');
129
- btn.tabIndex = i === 0 ? 0 : -1;
130
- btn.textContent = m.label;
131
- entry.appendChild(btn);
132
- el.appendChild(entry);
133
- return btn;
134
- });
136
+ this._renderTitles();
135
137
 
136
138
  this._h = {
137
139
  mousedown: (e) => this._onMouseDown(e),
@@ -163,12 +165,35 @@ export class MenuBar {
163
165
  /** Whether a menu is open. */
164
166
  isOpen() { return this._openIdx >= 0; }
165
167
 
166
- /** Open the menu with this id (focus on its title, or on its first item). */
168
+ /** Open the menu with this id (focus on its title, or on its first item).
169
+ * A hamburger opens with that menu flown out (focus on its row, or on
170
+ * the menu's first item). */
167
171
  open(menuId, { focusItem = false } = {}) {
168
172
  const i = this.menus.findIndex((m) => m.id === menuId);
169
173
  if (i < 0) return;
170
174
  this._enter();
171
- this._open(i, focusItem ? 'first' : null);
175
+ if (this._style !== 'hamburger') { this._open(i, focusItem ? 'first' : null); return; }
176
+ this._open(0, null);
177
+ const row = this._itemsIn(this._stack[0].menu)
178
+ .find((b) => this._models.get(b)?.id === `${HAMBURGER}:${menuId}`);
179
+ if (!row) return;
180
+ row.focus({ preventScroll: true });
181
+ this._openSub(row, focusItem ? 'first' : null);
182
+ }
183
+
184
+ /** 'bar' or 'hamburger'. */
185
+ get style() { return this._style; }
186
+
187
+ /** Draw the menus as a bar or as one hamburger button. Whatever is open
188
+ * closes; the model and every setEnabled / setChecked stay. */
189
+ setStyle(style) {
190
+ _validateStyle(style);
191
+ if (style === this._style) return;
192
+ const hadFocus = this.el.contains(this._doc.activeElement);
193
+ this._deactivate();
194
+ this._style = style;
195
+ this._renderTitles();
196
+ if (hadFocus) this._focusTitle(0);
172
197
  }
173
198
 
174
199
  /** Close every open menu. Focus stays in the bar if it was there. */
@@ -239,7 +264,7 @@ export class MenuBar {
239
264
  this._doc.removeEventListener('mousedown', this._h.docMousedown, true);
240
265
  this._doc.defaultView?.removeEventListener('blur', this._h.winBlur);
241
266
  this.el.innerHTML = '';
242
- this.el.classList.remove('twm-menubar');
267
+ this.el.classList.remove('twm-menubar', 'twm-menubar--hamburger');
243
268
  this.el.removeAttribute('role');
244
269
  this.el.removeAttribute('aria-label');
245
270
  }
@@ -317,6 +342,50 @@ export class MenuBar {
317
342
  }
318
343
  }
319
344
 
345
+ // ── titles ────────────────────────────────────────────────────────
346
+
347
+ /** The titles for the current style. `_top` is what they open: the
348
+ * menus themselves, or the hamburger's one menu that lists them. */
349
+ _renderTitles() {
350
+ const d = this._doc;
351
+ const burger = this._style === 'hamburger';
352
+ this.el.innerHTML = '';
353
+ this.el.classList.toggle('twm-menubar--hamburger', burger);
354
+ this._top = burger
355
+ ? [{ id: HAMBURGER, label: this._label, items: this.menus.map((m) => ({
356
+ id: `${HAMBURGER}:${m.id}`, label: m.label, submenu: m.items,
357
+ })) }]
358
+ : this.menus;
359
+ this._titles = this._top.map((m, i) => {
360
+ const entry = d.createElement('div');
361
+ entry.className = 'twm-menubar__entry';
362
+ entry.setAttribute('role', 'none');
363
+ const btn = d.createElement('button');
364
+ btn.type = 'button';
365
+ btn.className = 'twm-menubar__title';
366
+ btn.setAttribute('role', 'menuitem');
367
+ btn.setAttribute('aria-haspopup', 'menu');
368
+ btn.setAttribute('aria-expanded', 'false');
369
+ btn.tabIndex = i === 0 ? 0 : -1;
370
+ if (burger) {
371
+ btn.classList.add('twm-menubar__title--icon', 'twm-has-tooltip');
372
+ btn.setAttribute('aria-label', m.label);
373
+ btn.dataset.tooltip = m.label;
374
+ btn.dataset.tooltipPlacement = 'bottom';
375
+ const icon = d.createElement('span');
376
+ icon.className = 'material-symbols-outlined';
377
+ icon.setAttribute('aria-hidden', 'true');
378
+ icon.textContent = this._icon;
379
+ btn.appendChild(icon);
380
+ } else {
381
+ btn.textContent = m.label;
382
+ }
383
+ entry.appendChild(btn);
384
+ this.el.appendChild(entry);
385
+ return btn;
386
+ });
387
+ }
388
+
320
389
  // ── open / close ──────────────────────────────────────────────────
321
390
 
322
391
  _setRoving(i) {
@@ -359,7 +428,7 @@ export class MenuBar {
359
428
  // being removed never holds focus.
360
429
  this._focusTitle(i);
361
430
  this._closeAll();
362
- const m = this.menus[i];
431
+ const m = this._top[i];
363
432
  const title = this._titles[i];
364
433
  const menu = this._buildMenu(m.items, m.label);
365
434
  title.parentElement.appendChild(menu);
@@ -596,6 +665,8 @@ export class MenuBar {
596
665
  _titleKey(key, i) {
597
666
  const n = this._titles.length;
598
667
  const open = this._openIdx >= 0;
668
+ // One title (a hamburger): there is no next or previous menu.
669
+ if (n === 1 && (key === 'ArrowRight' || key === 'ArrowLeft' || key === 'Home' || key === 'End')) return true;
599
670
  switch (key) {
600
671
  case 'ArrowRight': open ? this._open(i + 1, null) : this._focusTitle((i + 1) % n); return true;
601
672
  case 'ArrowLeft': open ? this._open(i - 1, null) : this._focusTitle((i - 1 + n) % n); return true;
@@ -642,11 +713,11 @@ export class MenuBar {
642
713
  case 'End': this._focusIn(menu, 'last'); return true;
643
714
  case 'ArrowRight':
644
715
  if (item?.submenu != null && !this._isDisabled(item)) this._openSub(btn, 'first');
645
- else this._open(this._openIdx + 1, 'first');
716
+ else if (this._titles.length > 1) this._open(this._openIdx + 1, 'first');
646
717
  return true;
647
718
  case 'ArrowLeft':
648
719
  if (level > 0) this._closeFrom(level);
649
- else this._open(this._openIdx - 1, 'first');
720
+ else if (this._titles.length > 1) this._open(this._openIdx - 1, 'first');
650
721
  return true;
651
722
  case 'Enter': case ' ':
652
723
  this._activate(btn);
@@ -706,6 +777,12 @@ function _kind(item) {
706
777
  return 'menuitem';
707
778
  }
708
779
 
780
+ function _validateStyle(style) {
781
+ if (!STYLES.includes(style)) {
782
+ throw new TypeError(`createMenuBar: \`style\` must be ${STYLES.map((s) => `'${s}'`).join(' or ')}`);
783
+ }
784
+ }
785
+
709
786
  /** A bad model is a boot error, not a menu that silently lacks a row. */
710
787
  function _validateMenus(menus) {
711
788
  if (!Array.isArray(menus) || !menus.length) {