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
@@ -0,0 +1,422 @@
1
+ /**
2
+ * list_tiles.js — a LIST tile and a PEOPLE tile for a `TileGrid` (0.16.1).
3
+ *
4
+ * Both are rows you hand over through `setData()`, the way the chart tile takes
5
+ * series: which records are in the list, how they sort and what a row says is
6
+ * the embedder's job, because only the embedder knows what a record is. What a
7
+ * tile here owns is everything a list on a board needs and every embedder would
8
+ * otherwise write again: the count, the empty line, the "open all of these"
9
+ * button, a click, the keyboard, and a right-click menu per row.
10
+ *
11
+ * import { registerListTiles } from '@flexdesk/tiles';
12
+ * registerListTiles(); // idempotent; nothing registers at import
13
+ * const tile = grid.addTile('flexdesk.list', {x: 0, y: 0, w: 6, h: 4},
14
+ * { title: 'Overdue', empty_text: 'Nothing is late.' });
15
+ * tile.onRowActivate = ({ row, event }) => open(row.key, event.ctrlKey);
16
+ * tile.rowMenu = ({ row }) => [{ label: 'Open', icon: 'open_in_new', action: 'open' }];
17
+ * tile.onRowMenuAction = ({ action, row }) => { … };
18
+ * tile.setData({
19
+ * rows: [{ key: '7', icon: 'task', lead: 'TASK-0007', title: 'Sign-off',
20
+ * cells: [{ text: 'priya' }, { text: '9 days late', tone: 'danger' }] }],
21
+ * total: 12, // "7 of 12" when only 7 are handed over
22
+ * action: { label: 'Open as a list' }, // the button in the bar; see onAction
23
+ * });
24
+ *
25
+ * ══ THE DATA ════════════════════════════════════════════════════════════
26
+ *
27
+ * list { rows: [Row], total?, note?, empty?, action?: {label, icon?} } (an array is {rows})
28
+ * Row = { key, title, icon?, iconTitle?, lead?, className?, tip?,
29
+ * cells?: [{ text, title?, tone?, icon?, className? }] }
30
+ * people { people: [Person], total?, note?, empty?, action? } (an array is {people})
31
+ * Person = { key, name, icon?, muted?, count, countLabel?, title?,
32
+ * pills?: [{ text, tone?, title? }] }
33
+ *
34
+ * `tone` is one of `danger`, `warning`, `ok`, `muted` and only picks a colour;
35
+ * the text says the same thing, because colour alone does not survive a
36
+ * monochrome display. `className` is the embedder's own class, added as it is,
37
+ * so a status pill can keep the app's look.
38
+ *
39
+ * AN EMPTY LIST IS NOT THE EMPTY STATE. `{rows: []}` keeps the bar (count 0,
40
+ * the button) and says `empty` (or `config.empty_text`) where the rows would be:
41
+ * "nothing is late" is the most useful thing a list of late work can say, and a
42
+ * tile that shows a grey inbox icon instead cannot say it. `setData(null)` is
43
+ * the empty state; `showEmpty(message)` is how an embedder says "not set up".
44
+ *
45
+ * ══ INPUT ═══════════════════════════════════════════════════════════════
46
+ *
47
+ * A row is `role="button"` with a tab stop. Click, Enter and Space call
48
+ * `onRowActivate({row, event, tile})` with the event, so the modifier keys
49
+ * decide how it opens. Up and Down move between rows, Home and End to the ends.
50
+ * Right-click, the context-menu key and Shift+F10 open the row's menu:
51
+ * `rowMenu({row, event, tile})` answers the items (`showContextMenu`'s; none or
52
+ * `[]` means no menu, and the browser's own is left alone) and the choice goes
53
+ * to `onRowMenuAction({action, row, tile})`. The bar's button calls
54
+ * `onAction({tile, event})`. Every hook can also be passed to the constructor.
55
+ * A row carries `data-row-key`, so an embedder can find it for its own
56
+ * listeners (a drag source, say); `rowsDraggable` makes rows `draggable`.
57
+ *
58
+ * The header reads `config.title`, else the type's title; the count is in the
59
+ * bar, not the header, so a title the embedder sets is never rewritten.
60
+ */
61
+
62
+ import { TileBase } from './tile_base.js';
63
+ import { registerWidget, getWidget } from './tile_registry.js';
64
+ import { showContextMenu } from '../ui/components/context_menu.js';
65
+
66
+ const TONES = new Set(['danger', 'warning', 'ok', 'muted']);
67
+
68
+ /** `{items, total, note, empty, action}` from what `setData` was handed under
69
+ * `key` (`rows` or `people`), or null when there is nothing at all. */
70
+ function normalize(data, key) {
71
+ if (data == null) return null;
72
+ const d = Array.isArray(data) ? { [key]: data } : data;
73
+ if (typeof d !== 'object' || !Array.isArray(d[key])) return null;
74
+ const items = d[key].filter((r) => r && typeof r === 'object' && r.key != null);
75
+ const total = Number.isFinite(Number(d.total)) && d.total !== null && d.total !== '' ? Number(d.total) : items.length;
76
+ return {
77
+ items,
78
+ total: Math.max(total, items.length),
79
+ note: typeof d.note === 'string' ? d.note : '',
80
+ empty: typeof d.empty === 'string' ? d.empty : '',
81
+ action: d.action && typeof d.action === 'object' && d.action.label ? d.action : null,
82
+ };
83
+ }
84
+
85
+ /** Exported for tests and for an embedder that wants to know what the tile will make of its data. */
86
+ export const normalizeListData = (data) => normalize(data, 'rows');
87
+ export const normalizePeopleData = (data) => normalize(data, 'people');
88
+
89
+ function el(tag, cls, text) {
90
+ const e = document.createElement(tag);
91
+ if (cls) e.className = cls;
92
+ if (text != null) e.textContent = String(text);
93
+ return e;
94
+ }
95
+
96
+ function glyph(name, cls = '') {
97
+ const g = el('span', `material-symbols-outlined${cls ? ` ${cls}` : ''}`, name);
98
+ g.setAttribute('aria-hidden', 'true');
99
+ return g;
100
+ }
101
+
102
+ const toneClass = (base, tone) => (TONES.has(tone) ? ` ${base}--${tone}` : '');
103
+
104
+ /**
105
+ * The rows, the bar and the input, shared by both tiles. A subclass says what
106
+ * its items are called in the data (`_KEY`) and draws one (`_renderItem`).
107
+ * @abstract
108
+ */
109
+ class RowsTile extends TileBase {
110
+ static _KEY = 'rows';
111
+
112
+ constructor(options = {}) {
113
+ super(options);
114
+ this.onRowActivate = typeof options.onRowActivate === 'function' ? options.onRowActivate : null;
115
+ this.rowMenu = typeof options.rowMenu === 'function' ? options.rowMenu : null;
116
+ this.onRowMenuAction = typeof options.onRowMenuAction === 'function' ? options.onRowMenuAction : null;
117
+ this.onAction = typeof options.onAction === 'function' ? options.onAction : null;
118
+ this.rowsDraggable = options.rowsDraggable === true;
119
+ this._items = new Map(); // key → the item drawn for it
120
+ this._onClick = (e) => this._click(e);
121
+ this._onKey = (e) => this._key(e);
122
+ this._onMenu = (e) => this._menu(e);
123
+ }
124
+
125
+ /** Pure and static-only — it runs inside `TileBase`'s constructor. */
126
+ getDefaultConfig() {
127
+ return { title: '', empty_text: '', max_rows: 0 };
128
+ }
129
+
130
+ getConfigSchema() {
131
+ return {
132
+ fields: [
133
+ { key: 'title', type: 'text', label: 'Tile title' },
134
+ { key: 'empty_text', type: 'text', label: 'When there is nothing',
135
+ description: 'What the tile says when the list is empty, e.g. "Nothing is late."' },
136
+ { key: 'max_rows', type: 'number', label: 'Rows at most', min: 0,
137
+ description: '0 shows them all; the tile scrolls.' },
138
+ ],
139
+ };
140
+ }
141
+
142
+ mount(container) {
143
+ super.mount(container);
144
+ applyTitle(this);
145
+ const c = this.contentElement;
146
+ if (!c) return;
147
+ // On the content element, which outlives every render (they replace
148
+ // its children, never it), so one set of listeners serves them all.
149
+ c.addEventListener('click', this._onClick);
150
+ c.addEventListener('keydown', this._onKey);
151
+ c.addEventListener('contextmenu', this._onMenu);
152
+ }
153
+
154
+ /** `null` is "the grid has no data" — see `ChartTile.update`. */
155
+ update(data, config) {
156
+ if (this._disposed) return;
157
+ super.update(data === null ? undefined : data, config);
158
+ if (config) applyTitle(this);
159
+ }
160
+
161
+ /** Hand this tile its rows; null is the empty state. */
162
+ setData(data) {
163
+ if (this._disposed) return;
164
+ this.data = data ?? null;
165
+ if (this.contentElement) this.render(this.data);
166
+ }
167
+
168
+ /** The item drawn under `key`, or null. */
169
+ itemFor(key) { return this._items.get(String(key)) ?? null; }
170
+
171
+ /** The element of the item drawn under `key`, or null. */
172
+ rowElement(key) {
173
+ const want = String(key);
174
+ return [...(this.contentElement?.querySelectorAll('[data-row-key]') || [])]
175
+ .find((r) => r.dataset.rowKey === want) || null;
176
+ }
177
+
178
+ render(data) {
179
+ const content = this.contentElement;
180
+ if (!content) return;
181
+ const norm = normalize(data, this.constructor._KEY);
182
+ this._items.clear();
183
+ if (!norm) { this.showEmpty('Nothing to show.'); return; }
184
+
185
+ const max = Math.max(0, Math.floor(Number(this.config.max_rows) || 0));
186
+ const shown = max ? norm.items.slice(0, max) : norm.items;
187
+ const box = el('div', `fd-list-tile fd-list-tile--${this.constructor._KEY}`);
188
+
189
+ const bar = el('div', 'fd-list-tile__bar');
190
+ const count = el('span', 'fd-list-tile__count',
191
+ shown.length < norm.total ? `${shown.length} of ${norm.total}` : String(norm.total));
192
+ count.title = `${norm.total} in all`;
193
+ bar.append(count, el('span', 'fd-list-tile__spacer'));
194
+ if (norm.action) {
195
+ const b = el('button', 'fd-list-tile__action');
196
+ b.type = 'button';
197
+ b.dataset.listAction = 'action';
198
+ if (norm.action.icon) b.append(glyph(norm.action.icon));
199
+ b.append(el('span', null, norm.action.label));
200
+ bar.append(b);
201
+ }
202
+ box.append(bar);
203
+
204
+ const list = el('div', 'fd-list-tile__rows');
205
+ list.setAttribute('role', 'list');
206
+ if (!shown.length) {
207
+ list.append(el('div', 'fd-list-tile__empty', norm.empty || this.config.empty_text || 'Nothing here.'));
208
+ }
209
+ for (const item of shown) {
210
+ const key = String(item.key);
211
+ if (this._items.has(key)) continue; // one row per key: a second would be unreachable
212
+ this._items.set(key, item);
213
+ const row = this._renderItem(item);
214
+ row.classList.add('fd-list-tile__row');
215
+ row.dataset.rowKey = key;
216
+ row.setAttribute('role', 'button');
217
+ row.tabIndex = 0;
218
+ if (this.rowsDraggable) row.draggable = true;
219
+ const wrap = el('div', 'fd-list-tile__item');
220
+ wrap.setAttribute('role', 'listitem');
221
+ wrap.append(row);
222
+ list.append(wrap);
223
+ }
224
+ box.append(list);
225
+ if (norm.note) {
226
+ const note = el('div', 'fd-list-tile__note', norm.note);
227
+ note.setAttribute('role', 'note');
228
+ note.title = norm.note;
229
+ box.append(note);
230
+ }
231
+ content.replaceChildren(box);
232
+ }
233
+
234
+ /** One item's row element. @abstract */
235
+ _renderItem(_item) { return el('div'); }
236
+
237
+ // ── input ───────────────────────────────────────────────────────────
238
+
239
+ _rowOf(target) {
240
+ const row = target?.closest?.('[data-row-key]');
241
+ return row && this.contentElement?.contains(row) ? row : null;
242
+ }
243
+
244
+ _click(e) {
245
+ if (e.target?.closest?.('[data-list-action]')) {
246
+ this._call(this.onAction, { tile: this, event: e });
247
+ return;
248
+ }
249
+ const row = this._rowOf(e.target);
250
+ if (row) this._activate(row, e);
251
+ }
252
+
253
+ _activate(rowEl, event) {
254
+ const row = this.itemFor(rowEl.dataset.rowKey);
255
+ if (row) this._call(this.onRowActivate, { row, event, tile: this });
256
+ }
257
+
258
+ _key(e) {
259
+ const rowEl = this._rowOf(e.target);
260
+ if (!rowEl) return;
261
+ if (e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)) {
262
+ const r = rowEl.getBoundingClientRect();
263
+ if (this._openMenu(rowEl, e, r.left + 16, r.bottom)) e.preventDefault();
264
+ return;
265
+ }
266
+ if ((e.key === 'Enter' || e.key === ' ') && !e.altKey) {
267
+ e.preventDefault();
268
+ this._activate(rowEl, e);
269
+ return;
270
+ }
271
+ const rows = [...this.contentElement.querySelectorAll('[data-row-key]')];
272
+ const i = rows.indexOf(rowEl);
273
+ const to = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: rows.length - 1 }[e.key];
274
+ if (to === undefined || !rows[to]) return;
275
+ e.preventDefault();
276
+ rows[to].focus();
277
+ }
278
+
279
+ _menu(e) {
280
+ const rowEl = this._rowOf(e.target);
281
+ if (rowEl && this._openMenu(rowEl, e, e.clientX, e.clientY)) e.preventDefault();
282
+ }
283
+
284
+ /** Show the row's menu, if it has one. → whether it did. */
285
+ _openMenu(rowEl, event, x, y) {
286
+ const row = this.itemFor(rowEl.dataset.rowKey);
287
+ if (!row || typeof this.rowMenu !== 'function') return false;
288
+ let items = null;
289
+ try { items = this.rowMenu({ row, event, tile: this }); }
290
+ catch (err) { console.error(`[${this.constructor.name}] rowMenu threw:`, err); return false; }
291
+ if (!Array.isArray(items) || !items.length) return false;
292
+ showContextMenu(x, y, items, (action) => this._call(this.onRowMenuAction, { action, row, tile: this }));
293
+ return true;
294
+ }
295
+
296
+ /** A hook, called so that one that throws is reported and the tile goes on. */
297
+ _call(fn, arg) {
298
+ if (typeof fn !== 'function') return;
299
+ try { fn(arg); } catch (err) { console.error(`[${this.constructor.name}] a callback threw:`, err); }
300
+ }
301
+
302
+ dispose() {
303
+ const c = this.contentElement;
304
+ if (c) {
305
+ c.removeEventListener('click', this._onClick);
306
+ c.removeEventListener('keydown', this._onKey);
307
+ c.removeEventListener('contextmenu', this._onMenu);
308
+ }
309
+ this._items.clear();
310
+ super.dispose();
311
+ }
312
+ }
313
+
314
+ /**
315
+ * A LIST OF RECORDS: an icon, a lead (a reference, say), the title, and cells.
316
+ * See the header for the data and the input.
317
+ */
318
+ export class ListTile extends RowsTile {
319
+ static TYPE = 'flexdesk.list';
320
+ static TITLE = 'List';
321
+ static ICON = 'list';
322
+ static DESCRIPTION = 'Rows you supply, with a count, a click and a right-click menu per row.';
323
+ static DEFAULT_SIZE = { w: 6, h: 4 };
324
+ static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
325
+ static _KEY = 'rows';
326
+
327
+ _renderItem(r) {
328
+ const row = el('div', r.className ? String(r.className) : null);
329
+ if (r.icon) {
330
+ const g = glyph(r.icon, 'fd-list-tile__icon');
331
+ if (r.iconTitle) { g.title = r.iconTitle; g.removeAttribute('aria-hidden'); }
332
+ row.append(g);
333
+ }
334
+ if (r.lead != null && r.lead !== '') row.append(el('span', 'fd-list-tile__lead', r.lead));
335
+ const title = el('span', 'fd-list-tile__title', r.title ?? '');
336
+ title.title = r.tip || String(r.title ?? '');
337
+ row.append(title);
338
+ for (const c of Array.isArray(r.cells) ? r.cells : []) {
339
+ if (!c) continue;
340
+ const cell = el('span', `fd-list-tile__cell${toneClass('fd-list-tile__cell', c.tone)}${c.className ? ` ${c.className}` : ''}`);
341
+ if (c.icon) cell.append(glyph(c.icon));
342
+ if (c.text != null && c.text !== '') cell.append(document.createTextNode(String(c.text)));
343
+ if (c.title) cell.title = c.title;
344
+ row.append(cell);
345
+ }
346
+ return row;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * WHO HAS WHAT: one row per person, with a count and pills. The order is the
352
+ * embedder's (the list is read top-down and stopped at, so the order should BE
353
+ * the priority); `groupByPerson` is a starting point for building it.
354
+ */
355
+ export class PeopleTile extends RowsTile {
356
+ static TYPE = 'flexdesk.people';
357
+ static TITLE = 'People';
358
+ static ICON = 'groups';
359
+ static DESCRIPTION = 'One row per person, with how much is on them.';
360
+ static DEFAULT_SIZE = { w: 6, h: 4 };
361
+ static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
362
+ static _KEY = 'people';
363
+
364
+ getDefaultConfig() {
365
+ return { ...super.getDefaultConfig(), count_label: 'open' };
366
+ }
367
+
368
+ getConfigSchema() {
369
+ const { fields } = super.getConfigSchema();
370
+ return { fields: [...fields, { key: 'count_label', type: 'text', label: 'Count reads',
371
+ description: 'After each person\'s number, e.g. "open" for "3 open".' }] };
372
+ }
373
+
374
+ _renderItem(p) {
375
+ const row = el('div', 'fd-people-tile__row');
376
+ row.append(glyph(p.icon || (p.muted ? 'person_off' : 'person'), 'fd-list-tile__icon'));
377
+ row.append(el('span', `fd-people-tile__name${p.muted ? ' fd-people-tile__name--muted' : ''}`, p.name ?? p.key));
378
+ const pills = el('span', 'fd-people-tile__pills');
379
+ for (const pill of Array.isArray(p.pills) ? p.pills : []) {
380
+ if (!pill?.text) continue;
381
+ const s = el('span', `fd-people-tile__pill${toneClass('fd-people-tile__pill', pill.tone)}`, pill.text);
382
+ if (pill.title) s.title = pill.title;
383
+ pills.append(s);
384
+ }
385
+ row.append(pills);
386
+ const label = p.countLabel ?? this.config.count_label ?? '';
387
+ row.append(el('span', 'fd-people-tile__count', `${Number(p.count) || 0}${label ? ` ${label}` : ''}`));
388
+ if (p.title) row.title = p.title;
389
+ return row;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Items grouped by person: `[{key, items}]`, most items first, then by key, the
395
+ * nobody group (key `''`) last among equals. `personOf(item)` answers the
396
+ * person's key, or a falsy value for nobody.
397
+ */
398
+ export function groupByPerson(items, personOf) {
399
+ const by = new Map();
400
+ for (const item of Array.isArray(items) ? items : []) {
401
+ const who = String(personOf(item) || '');
402
+ if (!by.has(who)) by.set(who, { key: who, items: [] });
403
+ by.get(who).items.push(item);
404
+ }
405
+ return [...by.values()].sort((a, b) => b.items.length - a.items.length
406
+ || (a.key === '') - (b.key === '') || a.key.localeCompare(b.key));
407
+ }
408
+
409
+ /**
410
+ * Register both tiles. Idempotent, and nothing registers at import time — see
411
+ * `registerChartTiles`.
412
+ */
413
+ export function registerListTiles() {
414
+ for (const Tile of [ListTile, PeopleTile]) {
415
+ if (getWidget(Tile.TYPE) !== Tile) registerWidget(Tile);
416
+ }
417
+ }
418
+
419
+ /** The header shows `config.title`, else the type's own title. */
420
+ function applyTitle(tile) {
421
+ tile.setTitle(tile.config.title || tile.constructor.TITLE);
422
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * scope_picker.js — ONE FILTER FOR THE WHOLE BOARD (0.16.1).
3
+ *
4
+ * A row of buttons, one per scope ("Everyone", "Assigned by me", …), exactly
5
+ * one of them pressed. It is the control half of a dashboard-level filter; the
6
+ * other half is `TileGrid.setScope()`, which hands the value to every tile
7
+ * (`tile.scope`, `tile.onScopeChange`) and to the grid's `onScopeChange`:
8
+ *
9
+ * const picker = createScopePicker({
10
+ * options: [{ value: 'all', label: 'Everyone' },
11
+ * { value: 'mine', label: 'Mine', hint: 'only what is on me' }],
12
+ * value: 'all',
13
+ * onChange: (value) => grid.setScope(value),
14
+ * });
15
+ * header.append(picker.element);
16
+ *
17
+ * What a scope MEANS is the embedder's: the grid only carries the value, and a
18
+ * tile that knows nothing about scopes is unaffected by one.
19
+ *
20
+ * Each button has `aria-pressed`, the group a label. Left and Right move the
21
+ * focus along the row and Enter or Space presses, as on any button; a click on
22
+ * the button already pressed changes nothing and calls nothing.
23
+ */
24
+
25
+ /**
26
+ * @param {Object} o
27
+ * @param {Array<{value: string, label: string, hint?: string}>} o.options
28
+ * @param {string} [o.value] - The pressed one; the first option when absent or unknown.
29
+ * @param {string} [o.label='Scope'] - The group's accessible name.
30
+ * @param {(value: string) => void} [o.onChange] - After the user presses another one.
31
+ * @returns {{element: HTMLElement, readonly value: string, setValue: (v: string) => void, dispose: () => void}}
32
+ */
33
+ export function createScopePicker({ options = [], value, label = 'Scope', onChange } = {}) {
34
+ const opts = options.filter((o) => o && o.value != null).map((o) => ({ ...o, value: String(o.value) }));
35
+ const known = (v) => opts.some((o) => o.value === String(v));
36
+ let current = known(value) ? String(value) : opts[0]?.value ?? '';
37
+
38
+ const element = document.createElement('span');
39
+ element.className = 'fd-scope-picker';
40
+ element.setAttribute('role', 'group');
41
+ element.setAttribute('aria-label', label);
42
+
43
+ const buttons = opts.map((o) => {
44
+ const b = document.createElement('button');
45
+ b.type = 'button';
46
+ b.className = 'fd-scope-picker__btn';
47
+ b.dataset.scope = o.value;
48
+ b.textContent = o.label ?? o.value;
49
+ if (o.hint) b.title = o.hint;
50
+ element.append(b);
51
+ return b;
52
+ });
53
+
54
+ const paint = () => {
55
+ for (const b of buttons) {
56
+ const on = b.dataset.scope === current;
57
+ b.classList.toggle('fd-scope-picker__btn--on', on);
58
+ b.setAttribute('aria-pressed', String(on));
59
+ }
60
+ };
61
+ paint();
62
+
63
+ const onClick = (e) => {
64
+ const b = e.target?.closest?.('[data-scope]');
65
+ if (!b || !element.contains(b) || b.dataset.scope === current) return;
66
+ current = b.dataset.scope;
67
+ paint();
68
+ try { onChange?.(current); } catch (err) { console.error('[ScopePicker] onChange threw:', err); }
69
+ };
70
+ const onKey = (e) => {
71
+ const i = buttons.indexOf(e.target);
72
+ if (i < 0) return;
73
+ const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: buttons.length - 1 }[e.key];
74
+ if (to === undefined || !buttons[to]) return;
75
+ e.preventDefault();
76
+ buttons[to].focus();
77
+ };
78
+ element.addEventListener('click', onClick);
79
+ element.addEventListener('keydown', onKey);
80
+
81
+ return {
82
+ element,
83
+ get value() { return current; },
84
+ /** Press `v` without calling `onChange` (a value restored from elsewhere). */
85
+ setValue(v) {
86
+ if (!known(v)) return;
87
+ current = String(v);
88
+ paint();
89
+ },
90
+ dispose() {
91
+ element.removeEventListener('click', onClick);
92
+ element.removeEventListener('keydown', onKey);
93
+ element.remove();
94
+ },
95
+ };
96
+ }
@@ -61,7 +61,7 @@ export class TileBase {
61
61
  // without one behaves exactly as it did before this line existed, and the
62
62
  // broadcast path is untouched.
63
63
  this.dataSource = dataSource;
64
- // `window.__ECOSIM_JS_NEW__?.stateGuard` — an application's global, reached for
64
+ // This used to be read off an application's window global — reached for
65
65
  // by a tile. The StateGuard is a FRAMEWORK service (@flexdesk/core); the INSTANCE is
66
66
  // the application's, and it is handed down through the grid. No guard => the
67
67
  // dataset writes happen directly, which is what the optional chain already did.
@@ -87,8 +87,19 @@ export class TileBase {
87
87
  this._loadSeq = 0;
88
88
  /** @type {Map<string, {name: string, analytics: Object, color: string}>|null} */
89
89
  this.comparisonData = null;
90
+ /** The board's scope (`TileGrid.setScope`), or null. Set before mount. */
91
+ this.scope = null;
90
92
  }
91
93
 
94
+ /**
95
+ * Called when the board's scope changes (`TileGrid.setScope`), after
96
+ * `this.scope` is set. A no-op here: a tile whose rows the grid's data
97
+ * source loads is reloaded by the grid, and one fed through `setData` is
98
+ * fed again by whoever owns its data.
99
+ * @param {*} _scope
100
+ */
101
+ onScopeChange(_scope) {}
102
+
92
103
  /**
93
104
  * Set comparison data for overlay rendering.
94
105
  * Only meaningful for widgets where SUPPORTS_COMPARISON = true.