flexdesk 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2261 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3373 -238
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/chunk-UWO43BZU.js +111 -0
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3373 -238
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +1131 -253
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3254 -2112
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -345
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +422 -0
  144. package/src/tiles/scope_picker.js +96 -0
  145. package/src/tiles/tile_base.js +12 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +38 -1
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +26 -4
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-CWPMLX67.js +0 -475
  241. package/dist/chunk-CWPMLX67.js.map +0 -7
  242. package/dist/chunk-FBGP5KN5.js.map +0 -7
  243. package/dist/chunk-FL5KFNQH.js.map +0 -7
  244. package/dist/chunk-NLTOY57M.js.map +0 -7
  245. package/dist/chunk-NNFTETK3.js +0 -633
  246. package/dist/chunk-NNFTETK3.js.map +0 -7
  247. package/dist/chunk-O5OHMWBB.js +0 -284
  248. package/dist/chunk-O5OHMWBB.js.map +0 -7
  249. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  250. package/dist/chunk-SN4LSOAR.js.map +0 -7
  251. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -91,6 +91,26 @@
91
91
  * tree's title until the content mounts and says otherwise. A call from a
92
92
  * content whose leaf has since been rebuilt does nothing.
93
93
  *
94
+ * ── C42. A CONTENT CAN REPLACE ITS OWN CHROME ACTIONS ──────────────────
95
+ *
96
+ * A factory's returned `chromeActions` were read once, after the mount, and
97
+ * drawn icon-only — so a toggle could not show that it was on, an action that
98
+ * did not apply yet could not say so, and a content whose offers changed had
99
+ * to be re-mounted to change them.
100
+ *
101
+ * The per-leaf ctx carries `setChromeActions(specs)`, and it behaves exactly
102
+ * like `setTitle`: it paints the content strip now; called DURING the mount it
103
+ * wins over the returned `chromeActions`; the later call wins; it survives
104
+ * repaints (a cached repaint never rebuilds the strip); `null` or `[]` clears
105
+ * it; and a call from a content whose leaf has since been rebuilt does
106
+ * nothing. A spec is `{icon, title, onClick}` as before, plus `label` (text
107
+ * beside, or instead of, the glyph), `disabled` (greyed, inert, its tooltip
108
+ * kept — `aria-disabled` rather than the `disabled` attribute, because a
109
+ * disabled button receives no pointer events and some engines then never show
110
+ * its `title`), `pressed` (`aria-pressed`, and a pressed style) and
111
+ * `{separator: true}` (a divider). A spec with none of the additions renders
112
+ * exactly as it did.
113
+ *
94
114
  * ── C33. A TAB DRAGGED INTO A TILE ───────────────────────────────────────
95
115
  *
96
116
  * *"I would like to be able to drag & drop tabs into tiles, too."* A tab could
@@ -199,6 +219,8 @@
199
219
  import { NotebookTabBar } from '../editor/notebook_tab_bar.js';
200
220
 
201
221
  const SPLITTER_PX = 4;
222
+ /** The smallest share of its split a slot can be dragged down to. */
223
+ const MIN_SLOT_FRACTION = 0.05;
202
224
  /** The three tab layouts, and the framework's default. `bottom` is the default
203
225
  * because it is what every existing embedder already renders. */
204
226
  const TAB_LAYOUTS = ['bottom', 'top', 'header'];
@@ -302,8 +324,13 @@ export class TileRenderer {
302
324
  * stub that this renderer will admit. */
303
325
  static get TAB_MIME() { return TILE_TAB_MIME; }
304
326
 
327
+ /** The smallest share of its split a splitter drag leaves a slot. Readable
328
+ * so a caller that sets split sizes itself (the WM rebuilding its panel
329
+ * splits) never builds one a drag could not have produced. */
330
+ static get MIN_SLOT_FRACTION() { return MIN_SLOT_FRACTION; }
331
+
305
332
  constructor({ root, tree, content, ctx, onFocusChange, onAfterRender,
306
- tabLayout = null, newTabButton = true }) {
333
+ onSplitResized, tabLayout = null, newTabButton = true }) {
307
334
  if (!content || typeof content.mount !== 'function') {
308
335
  throw new Error('TileRenderer: a content registry is required');
309
336
  }
@@ -326,6 +353,11 @@ export class TileRenderer {
326
353
  * The renderer cannot know about windows, so it says "I have
327
354
  * repainted" and the WM re-homes what it owns. */
328
355
  this.onAfterRender = onAfterRender || (() => {});
356
+ /** Fired once at the END of a splitter drag that changed the split's
357
+ * sizes, with the split's id. A drag only rewrites the tree's sizes
358
+ * (and the slots' flex) as the pointer moves; whoever owns the tree
359
+ * decides what a finished resize means — the WM saves the layout. */
360
+ this.onSplitResized = onSplitResized || (() => {});
329
361
  /** C38. Whether the `header` strip draws its `+`. An embedder whose
330
362
  * new tab would open nothing useful turns it off; `wm.newTab` stays. */
331
363
  this.newTabButton = newTabButton !== false;
@@ -821,6 +853,9 @@ export class TileRenderer {
821
853
  // the entry after that.
822
854
  let liveTitle = null;
823
855
  let liveEntry = null;
856
+ // C42. Whether the content called `ctx.setChromeActions` DURING the
857
+ // mount — if it did, that list wins over the returned `chromeActions`.
858
+ let liveActionsSet = false;
824
859
  if (leaf.content) {
825
860
  const setTitle = (text) => {
826
861
  const next = text == null || text === '' ? null : String(text);
@@ -836,7 +871,18 @@ export class TileRenderer {
836
871
  const now = this.tree?.get?.(leaf.id) || leaf;
837
872
  liveEntry.titleEl.textContent = next || now.title || (now.content ? now.content.kind : 'empty');
838
873
  };
839
- const leafCtx = { ...this.ctx, leafId: leaf.id, setTitle };
874
+ const setChromeActions = (specs) => {
875
+ if (!liveEntry) {
876
+ liveActionsSet = true;
877
+ _paintContentActions(contentActions, specs);
878
+ return;
879
+ }
880
+ // The same guard as `setTitle`: a rebuilt leaf's old content
881
+ // must not repaint the new content's buttons.
882
+ if (this._leafCache.get(leaf.id) !== liveEntry) return;
883
+ _paintContentActions(liveEntry.contentActionsEl, specs);
884
+ };
885
+ const leafCtx = { ...this.ctx, leafId: leaf.id, setTitle, setChromeActions };
840
886
  // Mount with the ACTIVE TAB's props, not `content.props`.
841
887
  // `wm.updateActiveTabProps` (the canonical per-tile view-state
842
888
  // store — survives reload + desktop switches via desktops.json)
@@ -858,7 +904,9 @@ export class TileRenderer {
858
904
  // `[{icon, title, onClick}]`. Read AFTER the mount, because that is
859
905
  // the first moment the content knows what it can offer — a table
860
906
  // does not know it has settings until it has a definition.
861
- _paintContentActions(contentActions, content.chromeActions);
907
+ // C42. Unless the content already said, through
908
+ // `ctx.setChromeActions` during the mount, which wins.
909
+ if (!liveActionsSet) _paintContentActions(contentActions, content.chromeActions);
862
910
  // C20. A content factory may also VETO structural verbs:
863
911
  // `chrome: { promote: false, close: false }`. Read after the mount
864
912
  // for the same reason `chromeActions` is — it is the first moment
@@ -877,6 +925,10 @@ export class TileRenderer {
877
925
  }
878
926
  entry = { wrapEl: wrap, bodyEl: body, chromeEl: chrome,
879
927
  titleEl: title, iconEl: icon, content,
928
+ // C42. The strip `ctx.setChromeActions` repaints after the
929
+ // mount. A cached repaint never rebuilds it, which is what
930
+ // makes a list set later survive one.
931
+ contentActionsEl: contentActions,
880
932
  // DERIVED HERE, NOT REUSED FROM ABOVE. `kindKey` was computed
881
933
  // before the mount; content that records its sub-tab or its
882
934
  // scroll offset WHILE mounting has already written to the
@@ -1374,7 +1426,7 @@ export class TileRenderer {
1374
1426
  _topTabCallbacks(leafId) {
1375
1427
  return {
1376
1428
  // C33. `DragReorder` calls this through `NotebookTabBar`, which
1377
- // sets its own `application/x-ecosim-tab` first and unchanged — so
1429
+ // sets its own tab MIME (getTabDragMime()) first and unchanged — so
1378
1430
  // an editor pane sharing the page cannot notice that tiles are
1379
1431
  // dragging tabs too.
1380
1432
  onDragStart: (ev, key, item) => {
@@ -1849,7 +1901,7 @@ export class TileRenderer {
1849
1901
  const px = totalPx - SPLITTER_PX * (split.children.length - 1);
1850
1902
  if (px <= 0) return;
1851
1903
  const deltaFrac = (delta / px) * totalSize;
1852
- const minFrac = totalSize * 0.05;
1904
+ const minFrac = totalSize * MIN_SLOT_FRACTION;
1853
1905
  const newSizes = startSizes.slice();
1854
1906
  newSizes[slotIdx] = Math.max(minFrac, startSizes[slotIdx] + deltaFrac);
1855
1907
  newSizes[slotIdx + 1] = Math.max(minFrac, startSizes[slotIdx + 1] - deltaFrac);
@@ -1869,8 +1921,17 @@ export class TileRenderer {
1869
1921
  }
1870
1922
 
1871
1923
  _onMouseUp() {
1924
+ const drag = this._drag;
1872
1925
  this._drag = null;
1873
1926
  document.body.classList.remove('twm-dragging');
1927
+ if (!drag) return;
1928
+ // Only a drag that LEFT the split at other sizes is a resize. Pointer
1929
+ // jitter along the splitter, or a drag that ends where it began,
1930
+ // moves the mouse but not the layout, and reports nothing.
1931
+ const sizes = this.tree.get(drag.splitId)?.sizes;
1932
+ if (!sizes || sizes.every((v, i) => v === drag.startSizes[i])) return;
1933
+ try { this.onSplitResized(drag.splitId); }
1934
+ catch (err) { console.error('[tile-renderer] onSplitResized threw', err); }
1874
1935
  }
1875
1936
  }
1876
1937
 
@@ -1927,19 +1988,54 @@ function _paintContentActions(hostEl, specs) {
1927
1988
  }
1928
1989
  hostEl.hidden = false;
1929
1990
  for (const spec of specs) {
1930
- if (!spec || !spec.icon) continue;
1991
+ if (!spec) continue;
1992
+ // C42. A divider between groups of content actions.
1993
+ if (spec.separator) {
1994
+ const sep = document.createElement('span');
1995
+ sep.className = 'twm-leaf__sep';
1996
+ sep.setAttribute('role', 'separator');
1997
+ hostEl.appendChild(sep);
1998
+ continue;
1999
+ }
2000
+ const label = spec.label == null || spec.label === '' ? '' : String(spec.label);
2001
+ if (!spec.icon && !label) continue;
1931
2002
  const btn = document.createElement('button');
1932
2003
  btn.type = 'button';
1933
2004
  btn.className = 'twm-leaf__btn';
1934
2005
  btn.title = spec.title || '';
1935
- btn.setAttribute('aria-label', spec.title || '');
1936
- btn.innerHTML =
1937
- `<span class="material-symbols-outlined">${spec.icon}</span>`;
2006
+ btn.setAttribute('aria-label', spec.title || label);
2007
+ btn.innerHTML = spec.icon
2008
+ ? `<span class="material-symbols-outlined">${spec.icon}</span>` : '';
2009
+ // C42. A text label, beside the glyph or instead of it. Text, never
2010
+ // markup: it is the embedder's string.
2011
+ if (label) {
2012
+ const text = document.createElement('span');
2013
+ text.className = 'twm-leaf__btn-label';
2014
+ text.textContent = label;
2015
+ btn.appendChild(text);
2016
+ btn.classList.add('twm-leaf__btn--labelled');
2017
+ }
2018
+ // C42. A toggle says whether it is on. Absent means "not a toggle", so
2019
+ // no attribute at all — `false` is a toggle that is off.
2020
+ if (spec.pressed != null) {
2021
+ btn.setAttribute('aria-pressed', spec.pressed ? 'true' : 'false');
2022
+ if (spec.pressed) btn.classList.add('twm-leaf__btn--pressed');
2023
+ }
2024
+ // C42. Inert but still pointable at: `aria-disabled`, not `disabled`,
2025
+ // because a disabled button receives no pointer events and some engines
2026
+ // then never show its `title` — and the tooltip is how the user learns
2027
+ // why it is greyed.
2028
+ const disabled = !!spec.disabled;
2029
+ if (disabled) {
2030
+ btn.setAttribute('aria-disabled', 'true');
2031
+ btn.classList.add('twm-leaf__btn--disabled');
2032
+ }
1938
2033
  // `stopPropagation` so a click does not also reach the leaf's own
1939
2034
  // mousedown, which focuses the tile and then parks focus inside the
1940
2035
  // body — stealing it back from whatever the action just opened.
1941
2036
  btn.addEventListener('click', (e) => {
1942
2037
  e.stopPropagation();
2038
+ if (disabled) return;
1943
2039
  try { spec.onClick?.(); } catch (err) { console.error('[tile] action threw', err); }
1944
2040
  });
1945
2041
  hostEl.appendChild(btn);
File without changes
File without changes