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,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
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
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
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
109
|
+
));
|
|
110
|
+
}
|