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.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tile_tab_menu.js — pop-out menu mounted by the tile's hamburger
|
|
3
|
+
* button. Surfaces the same content the tile's *home page* would
|
|
4
|
+
* list, grouped into one section per source table, with a built-in
|
|
5
|
+
* search, per-section pagination, and full keyboard navigation.
|
|
6
|
+
*
|
|
7
|
+
* Click (or Enter on the active row) to open the entity in a NEW TAB
|
|
8
|
+
* on the same tile. The menu only exists when the tab bar is visible
|
|
9
|
+
* (≥2 tabs).
|
|
10
|
+
*
|
|
11
|
+
* Source resolution: the menu asks the injected taxonomy which entity
|
|
12
|
+
* sources a tile on this page should list (`taxonomy.sourcesFor`), then
|
|
13
|
+
* asks the injected catalog to fetch and shape them. It knows the name
|
|
14
|
+
* of neither. Which sources a page offers is one field on the
|
|
15
|
+
* embedder's KindDef; it used to be a hardcoded table right here.
|
|
16
|
+
*
|
|
17
|
+
* Keyboard interactions:
|
|
18
|
+
* ↑ ↓ move the row cursor across ALL visible items (the
|
|
19
|
+
* cursor flows section-to-section, not just within one).
|
|
20
|
+
* Home/End jump to the first / last visible row.
|
|
21
|
+
* PageDown/ flip the focused section's pager when no search is active.
|
|
22
|
+
* PageUp
|
|
23
|
+
* Enter pick the cursor row.
|
|
24
|
+
* Esc close.
|
|
25
|
+
*
|
|
26
|
+
* Search semantics:
|
|
27
|
+
* - Match runs against the shaped (label, id, hint) of every raw row,
|
|
28
|
+
* so the menu's search behaves the same as the command palette's.
|
|
29
|
+
* - Pagination is OFF when a query is active — all matches show flat
|
|
30
|
+
* within each section. Without a query, each section caps to
|
|
31
|
+
* `PAGE_SIZE` rows with prev/next controls.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Items per category in browse mode (no search). Tuned small so the
|
|
35
|
+
* menu stays compact; users page through with prev/next or type to
|
|
36
|
+
* search the entire set. */
|
|
37
|
+
const PAGE_SIZE = 10;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/** Open the menu anchored at (x, y). Closes on Escape, click-outside,
|
|
41
|
+
* or selection. Returns a teardown closure for the caller, but the
|
|
42
|
+
* menu also self-disposes on close. */
|
|
43
|
+
export function openTileTabMenu({
|
|
44
|
+
x, y, leafKind, onPick, api, taxonomy, entities,
|
|
45
|
+
}) {
|
|
46
|
+
if (!taxonomy || !entities) {
|
|
47
|
+
console.error('[tile-tab-menu] taxonomy + entity catalog are required');
|
|
48
|
+
return () => {};
|
|
49
|
+
}
|
|
50
|
+
// Resolve which sources to list for the leaf's current page. An
|
|
51
|
+
// unknown kind falls back to the root's list, which is the
|
|
52
|
+
// "everything" default.
|
|
53
|
+
const topNav = taxonomy.topNavFor(leafKind) || leafKind;
|
|
54
|
+
const sources = taxonomy.sourcesFor(topNav)
|
|
55
|
+
.map((nav) => entities.get(nav))
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
// Build the overlay + panel chrome.
|
|
59
|
+
const overlay = document.createElement('div');
|
|
60
|
+
overlay.className = 'twm-tile-tabmenu-overlay';
|
|
61
|
+
overlay.innerHTML = `
|
|
62
|
+
<div class="twm-tile-tabmenu" role="dialog" aria-label="Open in new tab">
|
|
63
|
+
<header class="twm-tile-tabmenu__head">
|
|
64
|
+
<span class="material-symbols-outlined twm-tile-tabmenu__head-icon">tab</span>
|
|
65
|
+
<span class="twm-tile-tabmenu__head-label">Open in new tab</span>
|
|
66
|
+
<button type="button" class="twm-tile-tabmenu__close"
|
|
67
|
+
data-action="close"
|
|
68
|
+
aria-label="Close">×</button>
|
|
69
|
+
</header>
|
|
70
|
+
<div class="twm-tile-tabmenu__search">
|
|
71
|
+
<span class="material-symbols-outlined">search</span>
|
|
72
|
+
<input type="search"
|
|
73
|
+
data-role="search"
|
|
74
|
+
placeholder="Filter rows across all sections…"
|
|
75
|
+
autocomplete="off" />
|
|
76
|
+
</div>
|
|
77
|
+
<div class="twm-tile-tabmenu__body" data-role="body">
|
|
78
|
+
<div class="twm-tile-tabmenu__loading">Loading…</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
document.body.appendChild(overlay);
|
|
83
|
+
|
|
84
|
+
// Anchor below the hamburger. clip to viewport so it never opens
|
|
85
|
+
// off-screen when the tile sits at the right edge.
|
|
86
|
+
const panel = overlay.querySelector('.twm-tile-tabmenu');
|
|
87
|
+
panel.style.position = 'fixed';
|
|
88
|
+
panel.style.left = `${Math.max(8, Math.min(window.innerWidth - 360, x))}px`;
|
|
89
|
+
panel.style.top = `${Math.max(8, Math.min(window.innerHeight - 480, y - 480 + 22))}px`;
|
|
90
|
+
|
|
91
|
+
let alive = true;
|
|
92
|
+
let _query = '';
|
|
93
|
+
let _cursor = 0; // index into _visibleItems
|
|
94
|
+
let _visibleItems = []; // flat list rebuilt on every render
|
|
95
|
+
// navKind → { rows, page, shaped (pre-shaped per row in source order) }
|
|
96
|
+
const sectionState = new Map(sources.map((s) =>
|
|
97
|
+
[s.navKind, { rows: [], page: 0, shaped: [] }]));
|
|
98
|
+
|
|
99
|
+
const close = () => {
|
|
100
|
+
if (!alive) return;
|
|
101
|
+
alive = false;
|
|
102
|
+
document.removeEventListener('mousedown', onOutside, true);
|
|
103
|
+
document.removeEventListener('keydown', onKey, true);
|
|
104
|
+
overlay.remove();
|
|
105
|
+
};
|
|
106
|
+
const onOutside = (ev) => {
|
|
107
|
+
if (!overlay.contains(ev.target)) close();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const moveCursor = (delta) => {
|
|
111
|
+
if (_visibleItems.length === 0) return;
|
|
112
|
+
_cursor = Math.max(0, Math.min(_visibleItems.length - 1, _cursor + delta));
|
|
113
|
+
_paintCursor();
|
|
114
|
+
};
|
|
115
|
+
const jumpCursor = (toEnd) => {
|
|
116
|
+
if (_visibleItems.length === 0) return;
|
|
117
|
+
_cursor = toEnd ? _visibleItems.length - 1 : 0;
|
|
118
|
+
_paintCursor();
|
|
119
|
+
};
|
|
120
|
+
const pickCursor = () => {
|
|
121
|
+
const cur = _visibleItems[_cursor];
|
|
122
|
+
if (!cur) return;
|
|
123
|
+
close();
|
|
124
|
+
try { onPick?.(cur.navKind, cur.shaped); }
|
|
125
|
+
catch (err) { console.warn('[tile-tab-menu] pick failed', err); }
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const onKey = (ev) => {
|
|
129
|
+
if (!alive) return;
|
|
130
|
+
// Ignore arrow/Enter typed inside the search box's IME, etc.
|
|
131
|
+
if (ev.isComposing) return;
|
|
132
|
+
switch (ev.key) {
|
|
133
|
+
case 'Escape':
|
|
134
|
+
ev.preventDefault(); close(); return;
|
|
135
|
+
case 'ArrowDown':
|
|
136
|
+
ev.preventDefault(); moveCursor(+1); return;
|
|
137
|
+
case 'ArrowUp':
|
|
138
|
+
ev.preventDefault(); moveCursor(-1); return;
|
|
139
|
+
case 'Home':
|
|
140
|
+
ev.preventDefault(); jumpCursor(false); return;
|
|
141
|
+
case 'End':
|
|
142
|
+
ev.preventDefault(); jumpCursor(true); return;
|
|
143
|
+
case 'Enter':
|
|
144
|
+
ev.preventDefault(); pickCursor(); return;
|
|
145
|
+
case 'PageDown': {
|
|
146
|
+
if (_query) return;
|
|
147
|
+
ev.preventDefault();
|
|
148
|
+
const cur = _visibleItems[_cursor];
|
|
149
|
+
const st = cur ? sectionState.get(cur.navKind) : null;
|
|
150
|
+
if (st) {
|
|
151
|
+
const pages = _pageCountFor(cur.navKind);
|
|
152
|
+
if (st.page < pages - 1) { st.page += 1; _render(); }
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case 'PageUp': {
|
|
157
|
+
if (_query) return;
|
|
158
|
+
ev.preventDefault();
|
|
159
|
+
const cur = _visibleItems[_cursor];
|
|
160
|
+
const st = cur ? sectionState.get(cur.navKind) : null;
|
|
161
|
+
if (st && st.page > 0) { st.page -= 1; _render(); }
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
document.addEventListener('mousedown', onOutside, true);
|
|
167
|
+
document.addEventListener('keydown', onKey, true);
|
|
168
|
+
overlay.querySelector('[data-action="close"]')
|
|
169
|
+
.addEventListener('click', close);
|
|
170
|
+
|
|
171
|
+
const searchInput = overlay.querySelector('[data-role="search"]');
|
|
172
|
+
searchInput.addEventListener('input', () => {
|
|
173
|
+
_query = searchInput.value.trim().toLowerCase();
|
|
174
|
+
for (const st of sectionState.values()) st.page = 0;
|
|
175
|
+
_cursor = 0;
|
|
176
|
+
_render();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const bodyEl = overlay.querySelector('[data-role="body"]');
|
|
180
|
+
|
|
181
|
+
const _pageCountFor = (navKind) => {
|
|
182
|
+
const st = sectionState.get(navKind);
|
|
183
|
+
if (!st) return 1;
|
|
184
|
+
const total = (_query
|
|
185
|
+
? st.shaped.filter((s) => _matchesShaped(s, _query))
|
|
186
|
+
: st.shaped).length;
|
|
187
|
+
return Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const _render = () => {
|
|
191
|
+
if (!alive) return;
|
|
192
|
+
_visibleItems = [];
|
|
193
|
+
const sections = [];
|
|
194
|
+
for (const src of sources) {
|
|
195
|
+
const st = sectionState.get(src.navKind);
|
|
196
|
+
const meta = taxonomy.meta(src.navKind);
|
|
197
|
+
const all = st.shaped;
|
|
198
|
+
const filtered = _query
|
|
199
|
+
? all.filter((s) => _matchesShaped(s, _query))
|
|
200
|
+
: all;
|
|
201
|
+
const total = filtered.length;
|
|
202
|
+
// Pagination is OFF during search — show every match. In
|
|
203
|
+
// browse mode each section caps to PAGE_SIZE.
|
|
204
|
+
let view;
|
|
205
|
+
let page = 0;
|
|
206
|
+
let pages = 1;
|
|
207
|
+
if (_query) {
|
|
208
|
+
view = filtered;
|
|
209
|
+
} else {
|
|
210
|
+
pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
211
|
+
page = Math.min(st.page, pages - 1);
|
|
212
|
+
const start = page * PAGE_SIZE;
|
|
213
|
+
view = filtered.slice(start, start + PAGE_SIZE);
|
|
214
|
+
}
|
|
215
|
+
// Append to the flat cursor list, in render order.
|
|
216
|
+
for (const shaped of view) {
|
|
217
|
+
_visibleItems.push({ navKind: src.navKind, shaped });
|
|
218
|
+
}
|
|
219
|
+
sections.push({ src, meta, view, total, page, pages });
|
|
220
|
+
}
|
|
221
|
+
if (_cursor >= _visibleItems.length) {
|
|
222
|
+
_cursor = Math.max(0, _visibleItems.length - 1);
|
|
223
|
+
}
|
|
224
|
+
let flatIdx = 0;
|
|
225
|
+
bodyEl.innerHTML = sections.map((s) => {
|
|
226
|
+
const items = s.view.map((shaped) => {
|
|
227
|
+
const idx = flatIdx++;
|
|
228
|
+
const label = shaped.label || shaped.id || '';
|
|
229
|
+
const hint = shaped.hint || '';
|
|
230
|
+
return `
|
|
231
|
+
<li class="twm-tile-tabmenu__item${idx === _cursor ? ' twm-tile-tabmenu__item--cursor' : ''}"
|
|
232
|
+
data-flat-idx="${idx}">
|
|
233
|
+
<span class="twm-tile-tabmenu__item-label">${_esc(label)}</span>
|
|
234
|
+
${hint ? `<span class="twm-tile-tabmenu__item-hint">${_esc(hint)}</span>` : ''}
|
|
235
|
+
</li>
|
|
236
|
+
`;
|
|
237
|
+
}).join('');
|
|
238
|
+
return `
|
|
239
|
+
<section class="twm-tile-tabmenu__section"
|
|
240
|
+
data-section="${_esc(s.src.navKind)}">
|
|
241
|
+
<header class="twm-tile-tabmenu__section-head">
|
|
242
|
+
<span class="material-symbols-outlined">${_esc(s.meta?.icon || 'arrow_right')}</span>
|
|
243
|
+
<span class="twm-tile-tabmenu__section-label">${_esc(s.meta?.label || s.src.navKind)}</span>
|
|
244
|
+
<span class="twm-tile-tabmenu__section-count">${s.total}</span>
|
|
245
|
+
</header>
|
|
246
|
+
${s.view.length === 0 ? `
|
|
247
|
+
<div class="twm-tile-tabmenu__empty">${
|
|
248
|
+
_query ? 'No matches.' : '— none —'
|
|
249
|
+
}</div>
|
|
250
|
+
` : `<ul class="twm-tile-tabmenu__list">${items}</ul>`}
|
|
251
|
+
${(!_query && s.pages > 1) ? `
|
|
252
|
+
<div class="twm-tile-tabmenu__pager">
|
|
253
|
+
<button type="button" data-action="prev"
|
|
254
|
+
data-section="${_esc(s.src.navKind)}"
|
|
255
|
+
${s.page === 0 ? 'disabled' : ''}><</button>
|
|
256
|
+
<span>${s.page + 1} / ${s.pages}</span>
|
|
257
|
+
<button type="button" data-action="next"
|
|
258
|
+
data-section="${_esc(s.src.navKind)}"
|
|
259
|
+
${s.page >= s.pages - 1 ? 'disabled' : ''}>></button>
|
|
260
|
+
</div>
|
|
261
|
+
` : ''}
|
|
262
|
+
</section>
|
|
263
|
+
`;
|
|
264
|
+
}).join('');
|
|
265
|
+
bodyEl.querySelectorAll('.twm-tile-tabmenu__item').forEach((li) => {
|
|
266
|
+
const idx = Number(li.dataset.flatIdx);
|
|
267
|
+
li.addEventListener('mousemove', () => {
|
|
268
|
+
if (_cursor !== idx) { _cursor = idx; _paintCursor(); }
|
|
269
|
+
});
|
|
270
|
+
li.addEventListener('click', () => {
|
|
271
|
+
_cursor = idx;
|
|
272
|
+
pickCursor();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
bodyEl.querySelectorAll('[data-action="prev"], [data-action="next"]').forEach((btn) => {
|
|
276
|
+
btn.addEventListener('click', () => {
|
|
277
|
+
const navKind = btn.dataset.section;
|
|
278
|
+
const st = sectionState.get(navKind);
|
|
279
|
+
if (!st) return;
|
|
280
|
+
st.page += (btn.dataset.action === 'next') ? 1 : -1;
|
|
281
|
+
_render();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const _paintCursor = () => {
|
|
287
|
+
bodyEl.querySelectorAll('.twm-tile-tabmenu__item').forEach((li) => {
|
|
288
|
+
const idx = Number(li.dataset.flatIdx);
|
|
289
|
+
const on = idx === _cursor;
|
|
290
|
+
li.classList.toggle('twm-tile-tabmenu__item--cursor', on);
|
|
291
|
+
if (on) li.scrollIntoView({ block: 'nearest' });
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Kick off the fetch — in parallel, and only for the sources this
|
|
296
|
+
// tile cares about. `loadOne` + `shapeRows` are the catalog's own
|
|
297
|
+
// primitives, so this menu and the command palette cannot drift
|
|
298
|
+
// apart on what a row is or which rows get dropped (they used to:
|
|
299
|
+
// this loop was a hand-rolled copy of the palette's).
|
|
300
|
+
(async () => {
|
|
301
|
+
await Promise.all(sources.map(async (src) => {
|
|
302
|
+
const st = sectionState.get(src.navKind);
|
|
303
|
+
if (!st) return;
|
|
304
|
+
st.rows = await entities.loadOne(src.navKind, api);
|
|
305
|
+
// Pre-shape once and stash so search/render don't re-shape
|
|
306
|
+
// on every keystroke. Rows that shape() rejects are dropped.
|
|
307
|
+
st.shaped = entities.shapeRows(src.navKind, st.rows);
|
|
308
|
+
}));
|
|
309
|
+
_render();
|
|
310
|
+
// Park the cursor on the first visible row so Enter picks
|
|
311
|
+
// something immediately after open.
|
|
312
|
+
if (_visibleItems.length > 0) {
|
|
313
|
+
_cursor = 0;
|
|
314
|
+
_paintCursor();
|
|
315
|
+
}
|
|
316
|
+
})();
|
|
317
|
+
|
|
318
|
+
searchInput.focus();
|
|
319
|
+
return close;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
/** Match a shaped record (`{id, label, hint}`) against `query`.
|
|
324
|
+
* Case-insensitive substring against any field. Mirrors the command
|
|
325
|
+
* palette's matching so search semantics are consistent across both
|
|
326
|
+
* surfaces. */
|
|
327
|
+
function _matchesShaped(shaped, query) {
|
|
328
|
+
if (!shaped) return false;
|
|
329
|
+
const id = String(shaped.id ?? '').toLowerCase();
|
|
330
|
+
const label = String(shaped.label ?? '').toLowerCase();
|
|
331
|
+
const hint = String(shaped.hint ?? '').toLowerCase();
|
|
332
|
+
return id.includes(query) || label.includes(query) || hint.includes(query);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
function _esc(s) {
|
|
337
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
338
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
339
|
+
}[c]));
|
|
340
|
+
}
|