flexdesk 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. package/src/ui/utils/tooltip_service.js +406 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * tile_registry.js
3
+ *
4
+ * Registry for widget tile types.
5
+ * Widgets register themselves here, and the grid uses this to instantiate tiles by type.
6
+ */
7
+
8
+ /** @type {Map<string, typeof import('./tile_base.js').TileBase>} */
9
+ const registry = new Map();
10
+
11
+ /**
12
+ * Register a widget class.
13
+ * @param {typeof import('./tile_base.js').TileBase} WidgetClass - Widget class to register
14
+ */
15
+ // The registry used to trace registrations to `window.logger` — a global belonging
16
+ // to one application. Give it a logger or it stays quiet.
17
+ let _logger = null;
18
+
19
+ /** @param {{info?: Function, warn?: Function}|null} logger */
20
+ export function setTileLogger(logger) {
21
+ _logger = logger;
22
+ }
23
+
24
+ export function registerWidget(WidgetClass) {
25
+ if (!WidgetClass.TYPE) {
26
+ console.warn('[TileRegistry] Widget class missing TYPE property:', WidgetClass);
27
+ return;
28
+ }
29
+ registry.set(WidgetClass.TYPE, WidgetClass);
30
+ _logger?.info?.(`[TileRegistry] Registered widget: ${WidgetClass.TYPE}`);
31
+ }
32
+
33
+ /**
34
+ * Get a widget class by type.
35
+ * @param {string} type - Widget type identifier
36
+ * @returns {typeof import('./tile_base.js').TileBase | undefined} Widget class or undefined
37
+ */
38
+ export function getWidget(type) {
39
+ return registry.get(type);
40
+ }
41
+
42
+ /**
43
+ * Check if a widget type is registered.
44
+ * @param {string} type - Widget type identifier
45
+ * @returns {boolean}
46
+ */
47
+ export function hasWidget(type) {
48
+ return registry.has(type);
49
+ }
50
+
51
+ /**
52
+ * Get all registered widget types.
53
+ * @returns {Array<{type: string, title: string, defaultSize: {w: number, h: number}}>}
54
+ */
55
+ export function getAllWidgetTypes() {
56
+ const types = [];
57
+ registry.forEach((WidgetClass, type) => {
58
+ types.push({
59
+ type,
60
+ title: WidgetClass.TITLE || type,
61
+ defaultSize: WidgetClass.DEFAULT_SIZE || { w: 4, h: 3 },
62
+ constraints: WidgetClass.SIZE_CONSTRAINTS || { minW: 2, minH: 2, maxW: 12, maxH: 8 }
63
+ });
64
+ });
65
+ return types;
66
+ }
67
+
68
+ /**
69
+ * Create a widget instance.
70
+ * @param {string} type - Widget type identifier
71
+ * @param {Object} options - Constructor options for the widget
72
+ * @returns {import('./tile_base.js').TileBase | null} Widget instance or null if type not found
73
+ */
74
+ export function createWidget(type, options) {
75
+ const WidgetClass = registry.get(type);
76
+ if (!WidgetClass) {
77
+ console.warn(`[TileRegistry] Unknown widget type: ${type}`);
78
+ return null;
79
+ }
80
+ return new WidgetClass(options);
81
+ }
82
+
83
+ /**
84
+ * Get widget metadata for the "Add Widget" menu.
85
+ * @returns {Array<{type: string, title: string, icon: string, description: string}>}
86
+ */
87
+ export function getWidgetCatalog({ runType } = {}) {
88
+ const catalog = [];
89
+ registry.forEach((WidgetClass, type) => {
90
+ // Filter out MC-only widgets for static runs
91
+ if (runType === 'static' && WidgetClass.REQUIRES_MC) return;
92
+
93
+ catalog.push({
94
+ type,
95
+ title: WidgetClass.TITLE || type,
96
+ icon: WidgetClass.ICON || 'widgets',
97
+ description: WidgetClass.DESCRIPTION || '',
98
+ defaultSize: WidgetClass.DEFAULT_SIZE || { w: 4, h: 3 },
99
+ requiresMC: !!WidgetClass.REQUIRES_MC,
100
+ });
101
+ });
102
+ return catalog;
103
+ }
104
+
105
+ /**
106
+ * Get metadata for a specific widget type.
107
+ * @param {string} type - Widget type identifier
108
+ * @returns {Object|null} Widget metadata or null if not found
109
+ */
110
+ export function getWidgetMetadata(type) {
111
+ const WidgetClass = registry.get(type);
112
+ if (!WidgetClass) return null;
113
+
114
+ return {
115
+ type,
116
+ title: WidgetClass.TITLE || type,
117
+ icon: WidgetClass.ICON || 'widgets',
118
+ description: WidgetClass.DESCRIPTION || '',
119
+ defaultSize: WidgetClass.DEFAULT_SIZE || { w: 4, h: 3 },
120
+ requiresMC: !!WidgetClass.REQUIRES_MC,
121
+ };
122
+ }
123
+
124
+ // Export registry for debugging
125
+ export const TileRegistry = {
126
+ register: registerWidget,
127
+ get: getWidget,
128
+ has: hasWidget,
129
+ getAll: getAllWidgetTypes,
130
+ create: createWidget,
131
+ getCatalog: getWidgetCatalog,
132
+ getMetadata: getWidgetMetadata,
133
+ _registry: registry
134
+ };
135
+
136
+ export default TileRegistry;
@@ -0,0 +1,328 @@
1
+ /**
2
+ * command_palette.js — Ctrl+K center popup.
3
+ *
4
+ * ┌─────────────────────────────────────────┐
5
+ * │ search… │
6
+ * ├─────────────────────────────────────────┤
7
+ * │ [chip] [chip] [chip] [chip] │ top-nav shortcut chips
8
+ * │ [chip] [chip] │
9
+ * │ │
10
+ * │ ☐ left nav ☐ right panel ☐ bottom │ panel toggles
11
+ * ├─────────────────────────────────────────┤
12
+ * │ matched entity rows… │ search results
13
+ * └─────────────────────────────────────────┘
14
+ *
15
+ * The chips are the injected taxonomy's top-nav kinds; clicking one
16
+ * loads it into the primary tile. The toggles flip the corresponding
17
+ * panel-tile on/off. Typing searches the injected entity catalog
18
+ * (fetched lazily on first open).
19
+ */
20
+
21
+ import { HelpModal } from '../help/help_modal.js';
22
+
23
+ const ROOT_ID = 'twm-cmdpal';
24
+
25
+ // Non-entity commands surfaced in the palette's search results. They
26
+ // carry an `action` so `commit()` runs them instead of opening a tile.
27
+ const STATIC_COMMANDS = [
28
+ { kind: 'command', id: 'shortcuts', label: 'Keyboard shortcuts',
29
+ hint: 'Show all key bindings (?)', icon: 'keyboard',
30
+ action: () => HelpModal.open('keyboard-shortcuts') },
31
+ ];
32
+
33
+ /** @param taxonomy injected ontology — supplies the chip strip + row icons
34
+ * @param catalog injected entity catalog — supplies the searchable rows
35
+ * @param placeholder input placeholder. It names the embedder's entity types
36
+ * ("try foo:bar"), so the embedder owns the string. */
37
+ export function createCommandPalette({ wm, api, taxonomy, catalog, placeholder = 'Search…' }) {
38
+ if (!taxonomy) throw new Error('createCommandPalette: a taxonomy is required');
39
+ if (!catalog) throw new Error('createCommandPalette: an entity catalog is required');
40
+ let overlay = null;
41
+ let entities = [];
42
+
43
+ const isOpen = () => !!overlay;
44
+ const close = () => {
45
+ if (!overlay) return;
46
+ overlay.remove();
47
+ overlay = null;
48
+ };
49
+ const toggle = () => { isOpen() ? close() : open(); };
50
+
51
+ const open = async () => {
52
+ if (overlay) return;
53
+ overlay = document.createElement('div');
54
+ overlay.id = ROOT_ID;
55
+ overlay.className = 'twm-cmdpal-overlay';
56
+ overlay.innerHTML = _markup(taxonomy, placeholder);
57
+ document.body.appendChild(overlay);
58
+ overlay.addEventListener('click', (e) => {
59
+ if (e.target === overlay) close();
60
+ });
61
+
62
+ const input = overlay.querySelector('.twm-cmdpal__input');
63
+ const list = overlay.querySelector('[data-role="list"]');
64
+ const chipSlot = overlay.querySelector('[data-role="type-chip"]');
65
+ const chips = overlay.querySelectorAll('[data-shortcut]');
66
+ const toggles = overlay.querySelectorAll('[data-toggle]');
67
+
68
+ // Reflect current panel state in toggle buttons.
69
+ for (const btn of toggles) {
70
+ const which = btn.dataset.toggle;
71
+ btn.classList.toggle('twm-chip--on', wm.isPanelOpen(which));
72
+ }
73
+
74
+ chips.forEach((btn) => btn.addEventListener('click', () => {
75
+ const kind = btn.dataset.shortcut;
76
+ wm.openInPrimary(kind);
77
+ close();
78
+ }));
79
+ toggles.forEach((btn) => btn.addEventListener('click', () => {
80
+ const which = btn.dataset.toggle;
81
+ wm.togglePanel(which);
82
+ btn.classList.toggle('twm-chip--on', wm.isPanelOpen(which));
83
+ }));
84
+
85
+ // Scoped-search state. Typing `<prefix>:` converts the prefix
86
+ // into a visual chip that lives to the left of the input;
87
+ // subsequent typing only matches entities whose navKind is in
88
+ // the chip's set. Cleared by clicking the chip's ×, by pressing
89
+ // Backspace when the input is empty, or by Esc (which closes the
90
+ // palette). A prefix can resolve to MULTIPLE navKinds, so the
91
+ // user's mental model can win over the embedder's internal
92
+ // taxonomy. The resolver lives on the injected catalog, next to
93
+ // the alias table it consults.
94
+ let filtered = [];
95
+ let active = 0;
96
+ let typeFilter = null; // null | string[] (navKinds)
97
+ let typeFilterLabel = null; // chip text, derived from the resolved kinds
98
+
99
+ const renderChip = () => {
100
+ if (!typeFilter) {
101
+ chipSlot.innerHTML = '';
102
+ return;
103
+ }
104
+ // Display: first kind's icon/label from the taxonomy. A
105
+ // multi-kind alias gets the leading kind's label, which
106
+ // reads as narrower than the alias actually is — so we
107
+ // override with the typed alias (passed from `refilter`)
108
+ // when it carries information the taxonomy can't infer.
109
+ const head = typeFilter[0];
110
+ const meta = taxonomy.meta(head);
111
+ const label = typeFilterLabel || meta?.label || head;
112
+ chipSlot.innerHTML = `
113
+ <span class="twm-cmdpal__type-chip" title="Filtering by ${_esc(label)} (${typeFilter.length} kind${typeFilter.length === 1 ? '' : 's'})">
114
+ <span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${meta?.icon || 'arrow_right'}</span>
115
+ <span class="twm-cmdpal__type-chip-label">${_esc(label)}</span>
116
+ <button type="button" class="twm-cmdpal__type-chip-x"
117
+ data-action="clear-type"
118
+ aria-label="Clear type filter">×</button>
119
+ </span>
120
+ `;
121
+ chipSlot.querySelector('[data-action="clear-type"]')
122
+ ?.addEventListener('click', (ev) => {
123
+ ev.preventDefault();
124
+ typeFilter = null;
125
+ typeFilterLabel = null;
126
+ renderChip();
127
+ input.focus();
128
+ refilter();
129
+ });
130
+ };
131
+
132
+ const render = () => {
133
+ list.innerHTML = filtered.slice(0, 200).map((e, i) => `
134
+ <div class="twm-cmdpal__item${i === active ? ' twm-cmdpal__item--active' : ''}"
135
+ data-idx="${i}">
136
+ <span class="material-symbols-outlined twm-cmdpal__icon">${e.icon || 'arrow_right'}</span>
137
+ <span class="twm-cmdpal__label">${_esc(e.label || e.id)}</span>
138
+ <span class="twm-cmdpal__kind">${e.kind}</span>
139
+ <span class="twm-cmdpal__hint">${_esc(e.hint || '')}</span>
140
+ </div>
141
+ `).join('') || '';
142
+ list.querySelectorAll('[data-idx]').forEach((el) => {
143
+ el.addEventListener('click', () => commit(Number(el.dataset.idx)));
144
+ });
145
+ };
146
+ const commit = (i) => {
147
+ const pick = filtered[i];
148
+ if (!pick) return;
149
+ close();
150
+ if (pick.action) { try { pick.action(); } catch (err) { console.error(err); } return; }
151
+ wm.openInPrimary(pick.kind, pick.props || { id: pick.id, label: pick.label });
152
+ };
153
+
154
+ /** Look for a `<prefix>:` at the start of the raw input. If the
155
+ * prefix resolves to one or more known kinds, promote it to
156
+ * the visual chip and strip it from the input field. Returns
157
+ * true when a chip was set so the caller can decide whether
158
+ * to re-render. */
159
+ const tryConsumePrefix = () => {
160
+ if (typeFilter) return false;
161
+ const m = input.value.match(/^([a-zA-Z][a-zA-Z0-9_-]*):/);
162
+ if (!m) return false;
163
+ const kinds = catalog.resolveTypePrefix(m[1]);
164
+ if (!kinds || kinds.length === 0) return false;
165
+ typeFilter = kinds;
166
+ typeFilterLabel = _titleCase(m[1]);
167
+ input.value = input.value.slice(m[0].length);
168
+ renderChip();
169
+ return true;
170
+ };
171
+
172
+ const refilter = () => {
173
+ tryConsumePrefix();
174
+ const q = input.value.trim().toLowerCase();
175
+ const filterSet = typeFilter ? new Set(typeFilter) : null;
176
+ const pool = filterSet
177
+ ? entities.filter((e) => filterSet.has(e.kind))
178
+ : entities;
179
+ if (!q) {
180
+ // Scoped + empty query → list every entity of that
181
+ // kind so the user sees what's available. Unscoped +
182
+ // empty query → blank list (matches the old behavior
183
+ // where typing nothing meant nothing).
184
+ filtered = typeFilter
185
+ ? pool.slice().sort((a, b) =>
186
+ String(a.label || a.id).localeCompare(b.label || b.id))
187
+ : [];
188
+ } else {
189
+ filtered = _search(pool, q);
190
+ }
191
+ active = 0;
192
+ render();
193
+ };
194
+ input.addEventListener('input', refilter);
195
+ input.addEventListener('keydown', (e) => {
196
+ if (e.key === 'ArrowDown') {
197
+ e.preventDefault();
198
+ active = Math.min(filtered.length - 1, active + 1);
199
+ render();
200
+ } else if (e.key === 'ArrowUp') {
201
+ e.preventDefault();
202
+ active = Math.max(0, active - 1);
203
+ render();
204
+ } else if (e.key === 'Enter') {
205
+ e.preventDefault();
206
+ commit(active);
207
+ } else if (e.key === 'Escape') {
208
+ close();
209
+ } else if (e.key === 'Backspace' && input.value === '' && typeFilter) {
210
+ // Empty input + active chip → consume Backspace to drop
211
+ // the chip. Same idiom as Gmail / Linear / VS Code's
212
+ // command palette: the chip behaves like a token before
213
+ // the caret.
214
+ e.preventDefault();
215
+ typeFilter = null;
216
+ typeFilterLabel = null;
217
+ renderChip();
218
+ refilter();
219
+ }
220
+ });
221
+ input.focus();
222
+
223
+ // Commands are searchable instantly; entity rows are lazy-loaded
224
+ // — the palette stays usable for shortcuts/commands while we wait.
225
+ // The set is rebuilt on every open so the palette reflects
226
+ // entities the user just added this session (cheap; the catalog
227
+ // runs every source in parallel).
228
+ entities = _decorateIcons(STATIC_COMMANDS.slice(), taxonomy);
229
+ if (api) {
230
+ try {
231
+ entities = _decorateIcons(
232
+ [...STATIC_COMMANDS, ...await catalog.loadAll(api)], taxonomy);
233
+ if (input.value) refilter();
234
+ } catch (err) {
235
+ console.warn('[cmdpal] entity load failed', err);
236
+ }
237
+ }
238
+ };
239
+
240
+ return { open, close, toggle, isOpen };
241
+ }
242
+
243
+ function _markup(taxonomy, placeholder) {
244
+ const chips = taxonomy.topNavEntries().map((k) => `
245
+ <button class="twm-chip" data-shortcut="${k.kind}">
246
+ <span class="material-symbols-outlined">${k.icon}</span>
247
+ ${k.label}
248
+ </button>
249
+ `).join('');
250
+ return `
251
+ <div class="twm-cmdpal" role="dialog" aria-label="Command palette">
252
+ <div class="twm-cmdpal__inputrow">
253
+ <span class="twm-cmdpal__type-chip-slot" data-role="type-chip"></span>
254
+ <input type="text" class="twm-cmdpal__input"
255
+ placeholder="${_esc(placeholder)}"
256
+ autocomplete="off" />
257
+ </div>
258
+ <div class="twm-cmdpal__chips">${chips}</div>
259
+ <div class="twm-cmdpal__toggles">
260
+ <button class="twm-chip" data-toggle="left">
261
+ <span class="material-symbols-outlined">menu</span>
262
+ Left nav
263
+ </button>
264
+ <button class="twm-chip" data-toggle="right">
265
+ <span class="material-symbols-outlined">dock_to_left</span>
266
+ Right panel
267
+ </button>
268
+ <button class="twm-chip" data-toggle="bottom">
269
+ <span class="material-symbols-outlined">dock_to_bottom</span>
270
+ Bottom panel
271
+ </button>
272
+ </div>
273
+ <div class="twm-cmdpal__list" data-role="list"></div>
274
+ <div class="twm-cmdpal__footer">
275
+ <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
276
+ <span><kbd>Enter</kbd> open</span>
277
+ <span><kbd>Esc</kbd> close</span>
278
+ </div>
279
+ </div>
280
+ `;
281
+ }
282
+
283
+ /** Attach an icon per row by consulting the injected taxonomy. Keeps the
284
+ * palette and breadcrumb visually consistent: one source of truth for
285
+ * what icon represents a given kind. */
286
+ function _decorateIcons(rows, taxonomy) {
287
+ return rows.map((r) => {
288
+ if (r.icon) return r;
289
+ const meta = taxonomy.meta(r.kind);
290
+ return { ...r, icon: meta?.icon || 'arrow_right' };
291
+ });
292
+ }
293
+
294
+ function _search(entities, q) {
295
+ const scored = [];
296
+ for (const e of entities) {
297
+ const id = String(e.id || '').toLowerCase();
298
+ const label = String(e.label || '').toLowerCase();
299
+ const hint = String(e.hint || '').toLowerCase();
300
+ let r;
301
+ if (id === q || label === q) r = 0;
302
+ else if (id.startsWith(q)) r = 1;
303
+ else if (label.startsWith(q)) r = 2;
304
+ else if (id.includes(q)) r = 3;
305
+ else if (label.includes(q)) r = 4;
306
+ else if (hint.includes(q)) r = 5;
307
+ else continue;
308
+ scored.push({ e, r });
309
+ }
310
+ scored.sort((a, b) => a.r - b.r || a.e.label.localeCompare(b.e.label));
311
+ return scored.map((x) => x.e);
312
+ }
313
+
314
+ function _esc(s) {
315
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({
316
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
317
+ }[c]));
318
+ }
319
+
320
+ /** Display form for a typed prefix: capitalise the first letter, and
321
+ * swap any underscores/hyphens for spaces so a multi-word alias reads
322
+ * as prose. Used for the scope chip's label, so it reflects what the
323
+ * user actually typed rather than the kind it resolved to. */
324
+ function _titleCase(s) {
325
+ const str = String(s || '').replace(/[_-]+/g, ' ').trim();
326
+ if (!str) return '';
327
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
328
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * content_registry.js — maps a content kind string to a factory that
3
+ * mounts that content into a tile body element.
4
+ *
5
+ * A content factory has the shape:
6
+ *
7
+ * (hostEl, props, ctx) -> { destroy?(), title?, ... }
8
+ *
9
+ * The tile renderer calls the factory once when the leaf is first
10
+ * mounted with a given (kind, props) pair, and `destroy()` on unmount.
11
+ *
12
+ * ── ONE REGISTRY PER SHELL ────────────────────────────────────────────
13
+ * This module used to hold a module-level `Map` plus a `register(kind, fn)`
14
+ * export. Two problems, both structural:
15
+ *
16
+ * 1. Two shells on one page shared their kind -> factory table, so a
17
+ * foreign-ontology shell silently inherited the other embedder's
18
+ * factories. A module singleton is the global we are removing,
19
+ * wearing a hat.
20
+ * 2. `register()` could be called AFTER the first mount. A leaf mounted
21
+ * against a missing factory renders the placeholder and the renderer
22
+ * caches that DOM per (kind, props) — a later registration invalidates
23
+ * nothing, so an entire restored session stays wrong.
24
+ *
25
+ * `createContentRegistry(map)` fixes both: the map is a REQUIRED
26
+ * constructor argument of `createShell()`, validated up-front, and frozen
27
+ * before the WM is built. There is nothing left to call late.
28
+ *
29
+ * This module knows no kind names but one — `window-placeholder`, which is
30
+ * the WM's OWN content (shown in a tile whose content was promoted into a
31
+ * managed window). Defaulting it here means an embedder cannot forget it
32
+ * and get a "no factory registered" tile the first time it promotes.
33
+ */
34
+
35
+ export const PLACEHOLDER_KIND = 'window-placeholder';
36
+
37
+ /** Shown in a tile while its content lives in a managed window. Pure
38
+ * `twm-*` markup; clicking the button demotes the window back. */
39
+ function windowPlaceholderFactory(hostEl, props, ctx) {
40
+ hostEl.innerHTML = `
41
+ <div class="twm-window-placeholder">
42
+ <span class="material-symbols-outlined twm-window-placeholder__icon">open_in_new</span>
43
+ <div class="twm-window-placeholder__title">${_esc(props.originalTitle || props.originalKind || 'content')}</div>
44
+ <div class="twm-window-placeholder__hint">is open in a managed window</div>
45
+ <button class="twm-window-placeholder__btn" data-action="bring-back">
46
+ Bring back to this tile
47
+ </button>
48
+ </div>
49
+ `;
50
+ hostEl.querySelector('[data-action="bring-back"]')?.addEventListener('click', () => {
51
+ ctx.wm?.bringBackWindow?.(props.windowId);
52
+ });
53
+ return { title: `${props.originalTitle || props.originalKind} (window)` };
54
+ }
55
+
56
+ /**
57
+ * Build a shell-scoped content registry.
58
+ *
59
+ * @param {Object<string, Function>} map kind -> content factory
60
+ * @returns {{ has(kind): boolean, mount(kind, hostEl, props, ctx): object }}
61
+ */
62
+ export function createContentRegistry(map = {}) {
63
+ if (!map || typeof map !== 'object') {
64
+ throw new TypeError('createContentRegistry: `content` must be an object of kind -> factory');
65
+ }
66
+ for (const [kind, factory] of Object.entries(map)) {
67
+ if (typeof factory !== 'function') {
68
+ throw new TypeError(
69
+ `createContentRegistry: factory for "${kind}" must be a function`);
70
+ }
71
+ }
72
+ const factories = new Map(Object.entries(map));
73
+ if (!factories.has(PLACEHOLDER_KIND)) {
74
+ factories.set(PLACEHOLDER_KIND, windowPlaceholderFactory);
75
+ }
76
+
77
+ return Object.freeze({
78
+ has: (kind) => factories.has(kind),
79
+
80
+ mount: (kind, hostEl, props, ctx) => {
81
+ const factory = factories.get(kind) || _placeholder(kind);
82
+ try {
83
+ return factory(hostEl, props || {}, ctx || {}) || {};
84
+ } catch (err) {
85
+ console.error('[content_registry] mount failed for', kind, err);
86
+ hostEl.innerHTML =
87
+ `<div class="twm-tile-error">Mount failed: ${err && err.message || err}</div>`;
88
+ return {};
89
+ }
90
+ },
91
+ });
92
+ }
93
+
94
+ function _placeholder(kind) {
95
+ return (hostEl) => {
96
+ hostEl.innerHTML = `
97
+ <div class="tile-placeholder">
98
+ <div class="tile-placeholder__title">${kind}</div>
99
+ <div class="tile-placeholder__hint">no factory registered yet</div>
100
+ </div>
101
+ `;
102
+ return { title: kind };
103
+ };
104
+ }
105
+
106
+ function _esc(s) {
107
+ return String(s ?? '').replace(/[&<>"']/g, (c) => (
108
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
109
+ ));
110
+ }