flexdesk 0.16.1 → 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 (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  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-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  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 +3146 -164
  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 +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  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 +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  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 +0 -0
  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-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -27,7 +27,8 @@ const ESC = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').
27
27
  /**
28
28
  * @typedef {Object} DetailHeaderBadge
29
29
  * @property {string} text
30
- * @property {string} [className] - Extra CSS modifier (e.g., 'detail-header__badge--mc')
30
+ * @property {string} [className] - Extra class(es) of your own (e.g., 'my-badge--live'); the
31
+ * pill itself is `.twm-detail-header__badge`
31
32
  * @property {string} [icon] - Material Symbols icon name
32
33
  */
33
34
 
@@ -35,8 +36,10 @@ const ESC = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').
35
36
  * @typedef {Object} DetailHeaderAction
36
37
  * @property {string} key - data-action value
37
38
  * @property {string} label - Button text
38
- * @property {string} icon - Material Symbols icon name
39
- * @property {'primary'|'secondary'|'danger'} [variant='secondary']
39
+ * @property {string} [icon] - Material Symbols icon name (none: label only)
40
+ * @property {'primary'|'secondary'|'danger'} [variant='secondary'] - The button is a `.twm-btn`;
41
+ * 'primary' and 'danger' add `.twm-btn--primary` / `.twm-btn--danger`, and every
42
+ * variant is also named `.twm-detail-header__btn--<variant>`
40
43
  * @property {boolean} [disabled=false]
41
44
  * @property {string} [title] - Tooltip
42
45
  * @property {Function} handler - Click callback
@@ -107,10 +110,10 @@ function _populate(header, config) {
107
110
 
108
111
  for (const badge of badges) {
109
112
  const badgeEl = document.createElement('span');
110
- badgeEl.className = `detail-header__badge${badge.className ? ` ${badge.className}` : ''}`;
113
+ badgeEl.className = `twm-detail-header__badge${badge.className ? ` ${badge.className}` : ''}`;
111
114
  if (badge.icon) {
112
115
  const bi = document.createElement('span');
113
- bi.className = 'material-symbols-outlined';
116
+ bi.className = 'twm-detail-header__badge-icon material-symbols-outlined';
114
117
  bi.textContent = badge.icon;
115
118
  badgeEl.appendChild(bi);
116
119
  }
@@ -141,16 +144,24 @@ function _populate(header, config) {
141
144
  const btn = document.createElement('button');
142
145
  btn.type = 'button';
143
146
  const variant = action.variant || 'secondary';
144
- btn.className = `twm-detail-header__btn detail-header__btn--${variant}`;
147
+ // The shared `.twm-btn` look (BUG-0015): 'primary' and 'danger'
148
+ // wear its colour variants; 'secondary' is the plain button.
149
+ const look = variant === 'secondary' ? '' : ` twm-btn--${variant}`;
150
+ btn.className = `twm-btn${look} twm-detail-header__btn twm-detail-header__btn--${variant}`;
145
151
  btn.dataset.action = action.key;
146
152
  if (action.disabled) btn.disabled = true;
147
153
  if (action.title) btn.title = action.title;
148
154
 
149
- const iconSpan = document.createElement('span');
150
- iconSpan.className = 'material-symbols-outlined';
151
- iconSpan.textContent = action.icon;
152
- btn.appendChild(iconSpan);
153
- btn.appendChild(document.createTextNode(` ${action.label}`));
155
+ if (action.icon) {
156
+ const iconSpan = document.createElement('span');
157
+ iconSpan.className = 'material-symbols-outlined twm-btn__glyph';
158
+ iconSpan.textContent = action.icon;
159
+ btn.appendChild(iconSpan);
160
+ }
161
+ const labelSpan = document.createElement('span');
162
+ labelSpan.className = 'twm-btn__label';
163
+ labelSpan.textContent = action.label ?? '';
164
+ btn.appendChild(labelSpan);
154
165
 
155
166
  btn.addEventListener('click', () => action.handler?.());
156
167
  actionsEl.appendChild(btn);
@@ -0,0 +1,397 @@
1
+ /**
2
+ * document_outline.js — A LONG DOCUMENT'S OUTLINE: its sections listed and
3
+ * numbered, the one in view marked while the document scrolls, and a click
4
+ * that goes to a section. (0.17.0)
5
+ *
6
+ * ┌──────────────────────────┐
7
+ * │ 1 Introduction │
8
+ * │ 1.1 Scope │ ← the section in view
9
+ * │ 1.2 Terms │
10
+ * │ 2 Method │
11
+ * └──────────────────────────┘
12
+ *
13
+ * const outline = createDocumentOutline(sidebarEl, {
14
+ * entries: [{ id: 'intro', level: 1, label: 'Introduction' }, …],
15
+ * numbering: true,
16
+ * scrollContainer: articleEl,
17
+ * resolveTarget: (entry) => articleEl.querySelector(`[data-section="${entry.id}"]`),
18
+ * onSelect: (id) => { … },
19
+ * });
20
+ *
21
+ * Where the entries come from (headings in markup, cells in a notebook, a
22
+ * server's table of contents) is the embedder's business. The outline is told
23
+ * `{id, level, label}` and, through `resolveTarget`, which element in the
24
+ * scroll container each one stands for.
25
+ *
26
+ * NUMBERING (`numbering: true`) is hierarchical: levels 1, 2, 2, 1 are numbered
27
+ * 1, 1.1, 1.2, 2. An entry sits one deeper than the nearest entry above it of
28
+ * a lower level, so the shallowest level is depth 1 and a skipped level is one
29
+ * step, not two (1, 3, 3, 2 is 1, 1.1, 1.2, 1.3, never 1.0.1); and nothing goes
30
+ * deeper than `maxDepth` — a deeper entry is listed, indented and numbered at
31
+ * `maxDepth`.
32
+ *
33
+ * THE SCROLL-SPY. With a `scrollContainer`, every resolved target is watched by
34
+ * an IntersectionObserver rooted at that container, with `rootMargin` (default
35
+ * `'0px 0px -70% 0px'`: the top 30% of the container is "in view"). Of the
36
+ * targets in view, the one listed FIRST is the active section — the topmost
37
+ * visible one. When none is in view the active section stays what it was, so
38
+ * the long body of a section keeps its heading marked. An entry whose target
39
+ * cannot be resolved is listed, but it is never watched and never marked
40
+ * active, by the spy or by `setActive`.
41
+ *
42
+ * A CLICK keeps its entry active. A click on an entry marks it at once, scrolls
43
+ * its target into view (unless `scrollOnSelect: false`, when `onSelect` does
44
+ * the scrolling) and calls `onSelect(id, entry)`. A smooth scroll passes over
45
+ * every section between here and there, so the spy is SUPPRESSED while it runs:
46
+ * it resumes on the container's `scrollend`, or after `scrollEndTimeout` ms
47
+ * (default 1000) when no `scrollend` comes — an engine without the event, or a
48
+ * target that was already in place and so never scrolled.
49
+ *
50
+ * `refresh()` resolves the targets again (the document re-rendered);
51
+ * `setEntries(entries)` replaces the list. `dispose()` disconnects the observer,
52
+ * clears the timer and takes the outline off the page.
53
+ */
54
+
55
+ /**
56
+ * @typedef {Object} OutlineEntry
57
+ * @property {string} id - Unique among the entries.
58
+ * @property {number} [level=1] - 1 for a top-level section, 2 under it, …
59
+ * @property {string} label - The entry's words, drawn as text (never markup).
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} DocumentOutlineConfig
64
+ * @property {OutlineEntry[]} [entries]
65
+ * @property {boolean} [numbering=false] - Hierarchical numbers before the labels.
66
+ * @property {number} [maxDepth=3] - The deepest indentation and numbering depth.
67
+ * @property {Function} [onSelect] - `(id, entry) => void`, on a click on an entry.
68
+ * @property {boolean} [scrollOnSelect=true] - A click scrolls the entry's target
69
+ * into view (`scrollIntoView({block: 'start', behavior: scrollBehavior})`).
70
+ * @property {ScrollBehavior} [scrollBehavior='smooth']
71
+ * @property {HTMLElement|null} [scrollContainer] - The element that scrolls the
72
+ * document; the observer's root. Without it there is no scroll-spy.
73
+ * @property {Function} [resolveTarget] - `(entry, scrollContainer) => Element|null`.
74
+ * Defaults to the element inside the container whose `id` is the entry's id.
75
+ * @property {string} [rootMargin='0px 0px -70% 0px']
76
+ * @property {number} [scrollEndTimeout=1000] - ms before a click's suppression
77
+ * lifts when no `scrollend` arrives.
78
+ * @property {string|Node|Function|null} [emptyState='No headings'] - Shown with no entries.
79
+ * @property {string} [label='Outline'] - The navigation's accessible name.
80
+ * @property {string} [className] - Added to the root.
81
+ * @property {Function} [IntersectionObserver] - The observer class; defaults to
82
+ * the global one. Without either there is no scroll-spy.
83
+ */
84
+
85
+ export class DocumentOutline {
86
+ /**
87
+ * @param {HTMLElement|null} host appended to, if given
88
+ * @param {DocumentOutlineConfig} [config]
89
+ */
90
+ constructor(host, config = {}) {
91
+ this.config = {
92
+ entries: [],
93
+ numbering: false,
94
+ maxDepth: 3,
95
+ onSelect: null,
96
+ scrollOnSelect: true,
97
+ scrollBehavior: 'smooth',
98
+ scrollContainer: null,
99
+ resolveTarget: null,
100
+ rootMargin: '0px 0px -70% 0px',
101
+ scrollEndTimeout: 1000,
102
+ emptyState: 'No headings',
103
+ label: 'Outline',
104
+ className: null,
105
+ IntersectionObserver: null,
106
+ ...config,
107
+ };
108
+ this._entries = [];
109
+ this._rows = []; // [{id, entry, depth, number, li, target}] in list order
110
+ this._byId = new Map(); // id → row
111
+ this._byTarget = new Map(); // target element → row
112
+ this._inView = new Set(); // rows whose target intersects
113
+ this._activeId = null;
114
+ this._listeners = new Set();
115
+ this._observer = null;
116
+ this._suppressed = false;
117
+ this._timer = null;
118
+ this._disposed = false;
119
+
120
+ const root = document.createElement('nav');
121
+ root.className = 'twm-doc-outline'
122
+ + (this.config.className ? ` ${this.config.className}` : '');
123
+ if (this.config.label) root.setAttribute('aria-label', this.config.label);
124
+ const list = document.createElement('ol');
125
+ list.className = 'twm-doc-outline__items';
126
+ root.appendChild(list);
127
+ this.element = root;
128
+ this.listElement = list;
129
+
130
+ this._onClick = (ev) => this._click(ev);
131
+ this._onScrollEnd = () => this._resume();
132
+ list.addEventListener('click', this._onClick);
133
+ this.config.scrollContainer?.addEventListener?.('scrollend', this._onScrollEnd);
134
+
135
+ this._setEntries(this.config.entries);
136
+ if (host) host.appendChild(root);
137
+ }
138
+
139
+ // ── public ───────────────────────────────────────────────────────────
140
+
141
+ /** Replace the entries. The active entry stays active if it is still
142
+ * listed and resolved; otherwise there is none. */
143
+ setEntries(entries) {
144
+ if (this._disposed) return;
145
+ this._setEntries(entries);
146
+ }
147
+
148
+ /** The entries, as last given. */
149
+ getEntries() {
150
+ return this._entries.slice();
151
+ }
152
+
153
+ /** Resolve every target again and watch the new ones: the document under
154
+ * the outline was re-rendered. */
155
+ refresh() {
156
+ if (this._disposed) return;
157
+ this._observe();
158
+ this._keepActive();
159
+ }
160
+
161
+ /** Mark an entry active (`null` for none). An id that is not listed, or
162
+ * whose target is not resolved, is refused: returns false. */
163
+ setActive(id) {
164
+ if (this._disposed) return false;
165
+ if (id == null) { this._activate(null); return true; }
166
+ const row = this._byId.get(String(id));
167
+ if (!row?.target) return false;
168
+ this._activate(row.id);
169
+ return true;
170
+ }
171
+
172
+ /** The active entry's id, or null. */
173
+ getActive() {
174
+ return this._activeId;
175
+ }
176
+
177
+ /** `fn(id, entry)` on every change of the active entry (`(null, null)` when
178
+ * none). Returns the function that unsubscribes. */
179
+ onActiveChange(fn) {
180
+ if (typeof fn !== 'function' || this._disposed) return () => {};
181
+ this._listeners.add(fn);
182
+ return () => this._listeners.delete(fn);
183
+ }
184
+
185
+ /** The `<li>` drawn for an id, or null. */
186
+ getItem(id) {
187
+ return this._byId.get(String(id))?.li || null;
188
+ }
189
+
190
+ dispose() {
191
+ if (this._disposed) return;
192
+ this._disposed = true;
193
+ this._observer?.disconnect();
194
+ this._observer = null;
195
+ this._clearTimer();
196
+ this._suppressed = false;
197
+ this.config.scrollContainer?.removeEventListener?.('scrollend', this._onScrollEnd);
198
+ this.listElement.removeEventListener('click', this._onClick);
199
+ this._listeners.clear();
200
+ this._byId.clear();
201
+ this._byTarget.clear();
202
+ this._inView.clear();
203
+ this._rows = [];
204
+ this.element.remove();
205
+ }
206
+
207
+ // ── drawing ──────────────────────────────────────────────────────────
208
+
209
+ _setEntries(entries) {
210
+ this._entries = Array.isArray(entries) ? entries.slice() : [];
211
+ this._rows = numberEntries(this._entries, this.config.maxDepth);
212
+ this._byId = new Map(this._rows.map((row) => [row.id, row]));
213
+ this._draw();
214
+ this._observe();
215
+ this._keepActive();
216
+ }
217
+
218
+ /** The active entry survives a redraw only while it is listed and resolved. */
219
+ _keepActive() {
220
+ if (this._activeId !== null && !this._byId.get(this._activeId)?.target) this._activate(null);
221
+ else this._paint();
222
+ }
223
+
224
+ _draw() {
225
+ const list = this.listElement;
226
+ list.replaceChildren();
227
+ this.element.querySelector(':scope > .twm-doc-outline__empty')?.remove();
228
+ this.element.classList.toggle('twm-doc-outline--empty', this._rows.length === 0);
229
+ this.element.classList.toggle('twm-doc-outline--numbered', !!this.config.numbering);
230
+ if (!this._rows.length) { this._drawEmpty(); return; }
231
+ for (const row of this._rows) {
232
+ const li = document.createElement('li');
233
+ li.className = `twm-doc-outline__item twm-doc-outline__item--level-${row.depth}`;
234
+ li.style.setProperty('--twm-doc-outline-depth', String(row.depth - 1));
235
+ const button = document.createElement('button');
236
+ button.type = 'button';
237
+ button.className = 'twm-doc-outline__link';
238
+ if (this.config.numbering) {
239
+ const num = document.createElement('span');
240
+ num.className = 'twm-doc-outline__number';
241
+ num.textContent = row.number;
242
+ button.appendChild(num);
243
+ }
244
+ const label = document.createElement('span');
245
+ label.className = 'twm-doc-outline__label';
246
+ label.textContent = String(row.entry.label ?? '');
247
+ button.appendChild(label);
248
+ button.title = (this.config.numbering ? `${row.number} ` : '') + label.textContent;
249
+ li.appendChild(button);
250
+ row.li = li;
251
+ list.appendChild(li);
252
+ }
253
+ }
254
+
255
+ _drawEmpty() {
256
+ let content = this.config.emptyState;
257
+ if (typeof content === 'function') content = content();
258
+ if (content == null || content === false || content === '') return;
259
+ const p = document.createElement('p');
260
+ p.className = 'twm-doc-outline__empty';
261
+ if (typeof Node !== 'undefined' && content instanceof Node) p.appendChild(content);
262
+ else p.textContent = String(content);
263
+ this.element.appendChild(p);
264
+ }
265
+
266
+ _paint() {
267
+ for (const row of this._rows) {
268
+ if (!row.li) continue;
269
+ const active = row.id === this._activeId;
270
+ row.li.classList.toggle('twm-doc-outline__item--active', active);
271
+ row.li.classList.toggle('twm-doc-outline__item--unresolved', !row.target);
272
+ const button = row.li.firstChild;
273
+ if (active) button.setAttribute('aria-current', 'location');
274
+ else button.removeAttribute('aria-current');
275
+ }
276
+ }
277
+
278
+ _activate(id) {
279
+ const next = id ?? null;
280
+ if (next === this._activeId) { this._paint(); return; }
281
+ this._activeId = next;
282
+ this._paint();
283
+ const entry = next === null ? null : this._byId.get(next)?.entry ?? null;
284
+ for (const fn of [...this._listeners]) fn(next, entry);
285
+ }
286
+
287
+ // ── the scroll-spy ───────────────────────────────────────────────────
288
+
289
+ _resolve(entry) {
290
+ const container = this.config.scrollContainer;
291
+ if (typeof this.config.resolveTarget === 'function') {
292
+ return this.config.resolveTarget(entry, container) || null;
293
+ }
294
+ if (!container) return null;
295
+ const id = String(entry.id);
296
+ for (const el of container.querySelectorAll('[id]')) if (el.id === id) return el;
297
+ return null;
298
+ }
299
+
300
+ _observe() {
301
+ this._observer?.disconnect();
302
+ this._byTarget.clear();
303
+ this._inView.clear();
304
+ for (const row of this._rows) row.target = this._resolve(row.entry);
305
+ for (const row of this._rows) {
306
+ if (row.target && !this._byTarget.has(row.target)) this._byTarget.set(row.target, row);
307
+ }
308
+ const container = this.config.scrollContainer;
309
+ const IO = this.config.IntersectionObserver || globalThis.IntersectionObserver;
310
+ if (!container || typeof IO !== 'function') return;
311
+ if (!this._observer) {
312
+ this._observer = new IO((records) => this._intersect(records), {
313
+ root: container,
314
+ rootMargin: this.config.rootMargin,
315
+ threshold: 0,
316
+ });
317
+ }
318
+ for (const target of this._byTarget.keys()) this._observer.observe(target);
319
+ }
320
+
321
+ _intersect(records) {
322
+ if (this._disposed) return;
323
+ for (const rec of records) {
324
+ const row = this._byTarget.get(rec.target);
325
+ if (!row) continue;
326
+ if (rec.isIntersecting) this._inView.add(row);
327
+ else this._inView.delete(row);
328
+ }
329
+ if (this._suppressed) return;
330
+ // The topmost section in view: the first listed.
331
+ const top = this._rows.find((row) => this._inView.has(row));
332
+ if (top) this._activate(top.id);
333
+ }
334
+
335
+ _click(ev) {
336
+ const li = ev.target?.closest?.('.twm-doc-outline__item');
337
+ if (!li || !this.listElement.contains(li)) return;
338
+ const row = this._rows.find((r) => r.li === li);
339
+ if (!row) return;
340
+ if (row.target) {
341
+ this._activate(row.id);
342
+ this._suppress();
343
+ if (this.config.scrollOnSelect && typeof row.target.scrollIntoView === 'function') {
344
+ row.target.scrollIntoView({ block: 'start', behavior: this.config.scrollBehavior });
345
+ }
346
+ }
347
+ this.config.onSelect?.(row.id, row.entry);
348
+ }
349
+
350
+ _suppress() {
351
+ this._suppressed = true;
352
+ this._clearTimer();
353
+ this._timer = setTimeout(() => this._resume(), this.config.scrollEndTimeout);
354
+ }
355
+
356
+ _resume() {
357
+ this._clearTimer();
358
+ this._suppressed = false;
359
+ }
360
+
361
+ _clearTimer() {
362
+ if (this._timer !== null) clearTimeout(this._timer);
363
+ this._timer = null;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Build a document outline, append it to `host` (if given) and draw it.
369
+ *
370
+ * @param {HTMLElement|null} host
371
+ * @param {DocumentOutlineConfig} [config]
372
+ * @returns {DocumentOutline}
373
+ */
374
+ export function createDocumentOutline(host, config = {}) {
375
+ return new DocumentOutline(host, config);
376
+ }
377
+
378
+ /**
379
+ * Each entry's row: its id as a string, its depth and its hierarchical number.
380
+ * An entry's depth is one more than the number of open sections of a lower
381
+ * level above it, so the shallowest level is depth 1 and a skipped level is
382
+ * one deeper, never two; then at most `maxDepth`.
383
+ */
384
+ function numberEntries(entries, maxDepth) {
385
+ const cap = Math.max(1, Math.floor(Number(maxDepth) || 3));
386
+ const open = []; // the levels of the sections this entry may sit under
387
+ const counters = [];
388
+ return entries.map((entry) => {
389
+ const level = Math.max(1, Math.floor(Number(entry?.level) || 1));
390
+ while (open.length && open[open.length - 1] >= level) open.pop();
391
+ const depth = Math.min(cap, open.length + 1);
392
+ open.push(level);
393
+ counters.length = depth;
394
+ counters[depth - 1] = (counters[depth - 1] || 0) + 1;
395
+ return { id: String(entry?.id), entry, depth, number: counters.join('.'), li: null, target: null };
396
+ });
397
+ }
File without changes
File without changes