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,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree View Component
|
|
3
|
+
*
|
|
4
|
+
* A reusable hierarchical tree view component for displaying nested data.
|
|
5
|
+
* Used by Functions Page (modules/functions) and Data Page (datasets/series).
|
|
6
|
+
*
|
|
7
|
+
* Structure:
|
|
8
|
+
* - TreeView: Container for multiple categories
|
|
9
|
+
* - TreeCategory: Top-level collapsible group (e.g., "Built-in", "Project")
|
|
10
|
+
* - TreeNode: Expandable node with children (e.g., module, dataset)
|
|
11
|
+
* - TreeItem: Leaf item (e.g., function, series)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a tree category (top-level collapsible section)
|
|
16
|
+
* @param {Object} options - Category options
|
|
17
|
+
* @param {string} options.id - Unique category ID
|
|
18
|
+
* @param {string} options.label - Display label
|
|
19
|
+
* @param {string} [options.icon] - Material icon name
|
|
20
|
+
* @param {string} [options.iconClass] - Additional icon CSS class
|
|
21
|
+
* @param {number} [options.count] - Item count to display
|
|
22
|
+
* @param {boolean} [options.expanded=false] - Initial expanded state
|
|
23
|
+
* @param {boolean} [options.readonly=false] - Whether items are readonly
|
|
24
|
+
* @returns {HTMLElement} - Category element
|
|
25
|
+
*/
|
|
26
|
+
export function createTreeCategory(options) {
|
|
27
|
+
const {
|
|
28
|
+
id,
|
|
29
|
+
label,
|
|
30
|
+
icon = 'folder',
|
|
31
|
+
iconClass = '',
|
|
32
|
+
count = 0,
|
|
33
|
+
expanded = false,
|
|
34
|
+
readonly = false,
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
const node = document.createElement('div');
|
|
38
|
+
node.className = 'twm-collapsible-box twm-tree-category';
|
|
39
|
+
node.dataset.categoryId = id;
|
|
40
|
+
node.dataset.collapsibleId = `tree-category-${id}`;
|
|
41
|
+
node.dataset.collapsibleDefault = expanded ? 'expanded' : 'collapsed';
|
|
42
|
+
if (readonly) node.dataset.readonly = 'true';
|
|
43
|
+
|
|
44
|
+
node.innerHTML = `
|
|
45
|
+
<div class="twm-collapsible-header" data-collapsible-header="true">
|
|
46
|
+
<button class="twm-arrow-toggle" type="button">
|
|
47
|
+
<span class="twm-collapsible-arrow material-symbols-outlined${expanded ? '' : ' collapsed'}">expand_more</span>
|
|
48
|
+
</button>
|
|
49
|
+
<span class="twm-tree-category__icon ${iconClass}">
|
|
50
|
+
<span class="material-symbols-outlined">${icon}</span>
|
|
51
|
+
</span>
|
|
52
|
+
<span class="twm-tree-category__label">${label}</span>
|
|
53
|
+
<span class="twm-tree-category__count">(${count})</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="twm-collapsible-content${expanded ? ' visible' : ''}" data-collapsible-content="true"></div>
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a tree node (expandable container with children)
|
|
63
|
+
* @param {Object} options - Node options
|
|
64
|
+
* @param {string} options.id - Unique node ID
|
|
65
|
+
* @param {string} options.label - Display label
|
|
66
|
+
* @param {string} [options.icon] - Material icon name (optional)
|
|
67
|
+
* @param {number} [options.count] - Child count to display
|
|
68
|
+
* @param {boolean} [options.expanded=false] - Initial expanded state
|
|
69
|
+
* @param {boolean} [options.readonly=false] - Whether node is readonly
|
|
70
|
+
* @param {Object} [options.actions] - Action buttons config
|
|
71
|
+
* @returns {HTMLElement} - Node element
|
|
72
|
+
*/
|
|
73
|
+
export function createTreeNode(options) {
|
|
74
|
+
const {
|
|
75
|
+
id,
|
|
76
|
+
label,
|
|
77
|
+
icon = null,
|
|
78
|
+
count = 0,
|
|
79
|
+
expanded = false,
|
|
80
|
+
readonly = false,
|
|
81
|
+
actions = null,
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
const node = document.createElement('div');
|
|
85
|
+
node.className = `twm-tree-node${readonly ? ' readonly' : ''}`;
|
|
86
|
+
node.dataset.nodeId = id;
|
|
87
|
+
|
|
88
|
+
const iconHtml = icon
|
|
89
|
+
? `<span class="twm-tree-node__icon"><span class="material-symbols-outlined">${icon}</span></span>`
|
|
90
|
+
: '';
|
|
91
|
+
|
|
92
|
+
const actionsHtml = actions ? '<span class="twm-tree-node__actions"></span>' : '';
|
|
93
|
+
|
|
94
|
+
node.innerHTML = `
|
|
95
|
+
<div class="twm-tree-node__header">
|
|
96
|
+
<span class="twm-tree-node__toggle">
|
|
97
|
+
<span class="material-symbols-outlined">${expanded ? 'expand_more' : 'chevron_right'}</span>
|
|
98
|
+
</span>
|
|
99
|
+
${iconHtml}
|
|
100
|
+
<span class="twm-tree-node__label">${label}</span>
|
|
101
|
+
<span class="twm-tree-node__count">(${count})</span>
|
|
102
|
+
${actionsHtml}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="twm-tree-node__children" style="display: ${expanded ? 'block' : 'none'};"></div>
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
// Add action buttons if specified
|
|
108
|
+
if (actions) {
|
|
109
|
+
const actionsEl = node.querySelector('.twm-tree-node__actions');
|
|
110
|
+
Object.entries(actions).forEach(([actionId, actionConfig]) => {
|
|
111
|
+
const btn = document.createElement('button');
|
|
112
|
+
btn.className = 'twm-tree-node__action-btn';
|
|
113
|
+
btn.dataset.action = actionId;
|
|
114
|
+
btn.title = actionConfig.title || actionId;
|
|
115
|
+
btn.innerHTML = `<span class="material-symbols-outlined">${actionConfig.icon}</span>`;
|
|
116
|
+
if (actionConfig.onClick) {
|
|
117
|
+
btn.addEventListener('click', (e) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
actionConfig.onClick(id, e);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
actionsEl.appendChild(btn);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return node;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a tree item (leaf node)
|
|
131
|
+
* @param {Object} options - Item options
|
|
132
|
+
* @param {string} options.id - Unique item ID
|
|
133
|
+
* @param {string} options.label - Display label
|
|
134
|
+
* @param {string} [options.icon] - Material icon or symbol (e.g., 'ƒ', 'insights')
|
|
135
|
+
* @param {boolean} [options.isMaterialIcon=true] - Whether icon is Material icon
|
|
136
|
+
* @param {boolean} [options.readonly=false] - Whether item is readonly
|
|
137
|
+
* @param {boolean} [options.selected=false] - Whether item is selected
|
|
138
|
+
* @param {string} [options.subtitle] - Optional subtitle text
|
|
139
|
+
* @param {Object} [options.data] - Additional data to attach
|
|
140
|
+
* @param {Object} [options.badge] - Optional badge config
|
|
141
|
+
* @param {string} [options.badge.text] - Badge text (if empty, renders as status dot)
|
|
142
|
+
* @param {string} [options.badge.variant] - 'muted'|'pass'|'fail'|'error'
|
|
143
|
+
* @returns {HTMLElement} - Item element
|
|
144
|
+
*/
|
|
145
|
+
export function createTreeItem(options) {
|
|
146
|
+
const {
|
|
147
|
+
id,
|
|
148
|
+
label,
|
|
149
|
+
icon = null,
|
|
150
|
+
isMaterialIcon = true,
|
|
151
|
+
readonly = false,
|
|
152
|
+
selected = false,
|
|
153
|
+
subtitle = null,
|
|
154
|
+
data = null,
|
|
155
|
+
badge = null,
|
|
156
|
+
} = options;
|
|
157
|
+
|
|
158
|
+
const item = document.createElement('div');
|
|
159
|
+
item.className = `twm-tree-item${readonly ? ' readonly' : ''}${selected ? ' selected' : ''}`;
|
|
160
|
+
item.dataset.itemId = id;
|
|
161
|
+
|
|
162
|
+
// Attach custom data
|
|
163
|
+
if (data) {
|
|
164
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
165
|
+
item.dataset[key] = value;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const iconHtml = icon
|
|
170
|
+
? isMaterialIcon
|
|
171
|
+
? `<span class="twm-tree-item__icon"><span class="material-symbols-outlined">${icon}</span></span>`
|
|
172
|
+
: `<span class="twm-tree-item__icon twm-tree-item__icon--symbol">${icon}</span>`
|
|
173
|
+
: '';
|
|
174
|
+
|
|
175
|
+
const subtitleHtml = subtitle
|
|
176
|
+
? `<span class="twm-tree-item__subtitle">${subtitle}</span>`
|
|
177
|
+
: '';
|
|
178
|
+
|
|
179
|
+
let badgeHtml = '';
|
|
180
|
+
if (badge) {
|
|
181
|
+
const variant = badge.variant || 'muted';
|
|
182
|
+
if (badge.text) {
|
|
183
|
+
badgeHtml = `<span class="twm-tree-item__badge tree-item__badge--${variant}">${badge.text}</span>`;
|
|
184
|
+
} else {
|
|
185
|
+
badgeHtml = `<span class="twm-tree-item__status-dot tree-item__status-dot--${variant}"></span>`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
item.innerHTML = `
|
|
190
|
+
${iconHtml}
|
|
191
|
+
<span class="twm-tree-item__label">${label}</span>
|
|
192
|
+
${subtitleHtml}
|
|
193
|
+
${badgeHtml}
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
return item;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Toggle a collapsible category
|
|
201
|
+
* @param {HTMLElement} categoryEl - Category element
|
|
202
|
+
* @param {boolean} [force] - Force specific state (true = expand, false = collapse)
|
|
203
|
+
*/
|
|
204
|
+
export function toggleCategory(categoryEl, force) {
|
|
205
|
+
if (!categoryEl) return;
|
|
206
|
+
|
|
207
|
+
const arrow = categoryEl.querySelector(':scope > .twm-collapsible-header .twm-collapsible-arrow');
|
|
208
|
+
const content = categoryEl.querySelector(':scope > .twm-collapsible-content');
|
|
209
|
+
|
|
210
|
+
if (!content) return;
|
|
211
|
+
|
|
212
|
+
// Determine whether we should expand
|
|
213
|
+
// force=true means expand, force=false means collapse, undefined means toggle
|
|
214
|
+
const isCurrentlyExpanded = content.classList.contains('visible');
|
|
215
|
+
const shouldExpand = force !== undefined ? force : !isCurrentlyExpanded;
|
|
216
|
+
|
|
217
|
+
if (shouldExpand) {
|
|
218
|
+
// Expand
|
|
219
|
+
if (arrow) {
|
|
220
|
+
arrow.classList.remove('collapsed');
|
|
221
|
+
}
|
|
222
|
+
content.classList.add('visible');
|
|
223
|
+
} else {
|
|
224
|
+
// Collapse
|
|
225
|
+
if (arrow) {
|
|
226
|
+
arrow.classList.add('collapsed');
|
|
227
|
+
}
|
|
228
|
+
content.classList.remove('visible');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Toggle a tree node
|
|
234
|
+
* @param {HTMLElement} nodeEl - Node element
|
|
235
|
+
* @param {boolean} [force] - Force specific state
|
|
236
|
+
*/
|
|
237
|
+
export function toggleNode(nodeEl, force) {
|
|
238
|
+
if (!nodeEl) return;
|
|
239
|
+
|
|
240
|
+
const toggle = nodeEl.querySelector(':scope > .twm-tree-node__header .twm-tree-node__toggle .material-symbols-outlined');
|
|
241
|
+
const children = nodeEl.querySelector(':scope > .twm-tree-node__children');
|
|
242
|
+
|
|
243
|
+
if (!children) return;
|
|
244
|
+
|
|
245
|
+
const isExpanded = force !== undefined
|
|
246
|
+
? force
|
|
247
|
+
: children.style.display === 'none';
|
|
248
|
+
|
|
249
|
+
if (isExpanded) {
|
|
250
|
+
children.style.display = 'block';
|
|
251
|
+
if (toggle) toggle.textContent = 'expand_more';
|
|
252
|
+
} else {
|
|
253
|
+
children.style.display = 'none';
|
|
254
|
+
if (toggle) toggle.textContent = 'chevron_right';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Filter tree items by text match
|
|
260
|
+
* @param {HTMLElement} container - Tree container
|
|
261
|
+
* @param {string} filter - Filter text (case-insensitive)
|
|
262
|
+
* @param {Object} [options] - Filter options
|
|
263
|
+
* @param {boolean} [options.expandMatches=true] - Auto-expand nodes with matches
|
|
264
|
+
*/
|
|
265
|
+
export function filterTree(container, filter, options = {}) {
|
|
266
|
+
const { expandMatches = true } = options;
|
|
267
|
+
const normalizedFilter = (filter || '').toLowerCase().trim();
|
|
268
|
+
|
|
269
|
+
const items = container.querySelectorAll('.twm-tree-item');
|
|
270
|
+
const nodes = container.querySelectorAll('.twm-tree-node');
|
|
271
|
+
const categories = container.querySelectorAll('.twm-tree-category');
|
|
272
|
+
|
|
273
|
+
// Reset visibility
|
|
274
|
+
items.forEach((item) => {
|
|
275
|
+
const label = item.querySelector('.twm-tree-item__label')?.textContent || '';
|
|
276
|
+
const matches = !normalizedFilter || label.toLowerCase().includes(normalizedFilter);
|
|
277
|
+
item.style.display = matches ? '' : 'none';
|
|
278
|
+
item.classList.toggle('twm-filter-match', matches && !!normalizedFilter);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Show/hide nodes based on visible children
|
|
282
|
+
nodes.forEach((node) => {
|
|
283
|
+
const visibleChildren = node.querySelectorAll('.twm-tree-item:not([style*="display: none"])');
|
|
284
|
+
const hasVisibleChildren = visibleChildren.length > 0;
|
|
285
|
+
node.style.display = hasVisibleChildren ? '' : 'none';
|
|
286
|
+
|
|
287
|
+
// Auto-expand nodes with matches
|
|
288
|
+
if (expandMatches && hasVisibleChildren && normalizedFilter) {
|
|
289
|
+
toggleNode(node, true);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Show/hide categories based on visible nodes
|
|
294
|
+
categories.forEach((category) => {
|
|
295
|
+
const visibleNodes = category.querySelectorAll('.twm-tree-node:not([style*="display: none"])');
|
|
296
|
+
const hasVisibleNodes = visibleNodes.length > 0;
|
|
297
|
+
category.style.display = hasVisibleNodes ? '' : 'none';
|
|
298
|
+
|
|
299
|
+
// Auto-expand categories with matches
|
|
300
|
+
if (expandMatches && hasVisibleNodes && normalizedFilter) {
|
|
301
|
+
toggleCategory(category, true);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Select a tree item
|
|
308
|
+
* @param {HTMLElement} container - Tree container
|
|
309
|
+
* @param {string} itemId - Item ID to select
|
|
310
|
+
* @param {Object} [options] - Selection options
|
|
311
|
+
* @param {boolean} [options.scrollIntoView=true] - Scroll item into view
|
|
312
|
+
* @param {boolean} [options.expandParents=true] - Expand parent nodes
|
|
313
|
+
*/
|
|
314
|
+
export function selectItem(container, itemId, options = {}) {
|
|
315
|
+
const { scrollIntoView = true, expandParents = true } = options;
|
|
316
|
+
|
|
317
|
+
// Deselect all
|
|
318
|
+
container.querySelectorAll('.twm-tree-item.selected').forEach((el) => {
|
|
319
|
+
el.classList.remove('selected');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Find and select item
|
|
323
|
+
const item = container.querySelector(`.twm-tree-item[data-item-id="${itemId}"]`);
|
|
324
|
+
if (!item) return null;
|
|
325
|
+
|
|
326
|
+
item.classList.add('selected');
|
|
327
|
+
|
|
328
|
+
// Expand parent nodes
|
|
329
|
+
if (expandParents) {
|
|
330
|
+
let parent = item.parentElement;
|
|
331
|
+
while (parent && parent !== container) {
|
|
332
|
+
if (parent.classList.contains('twm-tree-node__children')) {
|
|
333
|
+
const node = parent.closest('.twm-tree-node');
|
|
334
|
+
if (node) toggleNode(node, true);
|
|
335
|
+
}
|
|
336
|
+
if (parent.classList.contains('twm-collapsible-content')) {
|
|
337
|
+
const category = parent.closest('.twm-tree-category');
|
|
338
|
+
if (category) toggleCategory(category, true);
|
|
339
|
+
}
|
|
340
|
+
parent = parent.parentElement;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Scroll into view
|
|
345
|
+
if (scrollIntoView) {
|
|
346
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return item;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* TreeView class for managing a complete tree structure
|
|
354
|
+
*/
|
|
355
|
+
export class TreeView {
|
|
356
|
+
constructor(container, options = {}) {
|
|
357
|
+
this.container = container;
|
|
358
|
+
this.options = {
|
|
359
|
+
onItemClick: null,
|
|
360
|
+
onItemDoubleClick: null,
|
|
361
|
+
onNodeToggle: null,
|
|
362
|
+
onCategoryToggle: null,
|
|
363
|
+
...options,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
this._boundHandleClick = this._handleClick.bind(this);
|
|
367
|
+
this._boundHandleDblClick = this._handleDoubleClick.bind(this);
|
|
368
|
+
|
|
369
|
+
if (container) {
|
|
370
|
+
container.classList.add('twm-tree-view');
|
|
371
|
+
container.addEventListener('click', this._boundHandleClick);
|
|
372
|
+
container.addEventListener('dblclick', this._boundHandleDblClick);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_handleClick(e) {
|
|
377
|
+
// Handle category toggle
|
|
378
|
+
const collapsibleHeader = e.target.closest('.twm-collapsible-header');
|
|
379
|
+
if (collapsibleHeader) {
|
|
380
|
+
const category = collapsibleHeader.closest('.twm-tree-category');
|
|
381
|
+
if (category) {
|
|
382
|
+
toggleCategory(category);
|
|
383
|
+
this.options.onCategoryToggle?.(category.dataset.categoryId, category);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Handle node toggle
|
|
389
|
+
const nodeHeader = e.target.closest('.twm-tree-node__header');
|
|
390
|
+
if (nodeHeader) {
|
|
391
|
+
const node = nodeHeader.closest('.twm-tree-node');
|
|
392
|
+
if (node) {
|
|
393
|
+
toggleNode(node);
|
|
394
|
+
this.options.onNodeToggle?.(node.dataset.nodeId, node);
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Handle item click
|
|
400
|
+
const item = e.target.closest('.twm-tree-item');
|
|
401
|
+
if (item) {
|
|
402
|
+
this.select(item.dataset.itemId);
|
|
403
|
+
this.options.onItemClick?.(item.dataset.itemId, item, e);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_handleDoubleClick(e) {
|
|
408
|
+
const item = e.target.closest('.twm-tree-item');
|
|
409
|
+
if (item) {
|
|
410
|
+
this.options.onItemDoubleClick?.(item.dataset.itemId, item, e);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
addCategory(options) {
|
|
415
|
+
const category = createTreeCategory(options);
|
|
416
|
+
this.container.appendChild(category);
|
|
417
|
+
return category;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
addNode(categoryId, options) {
|
|
421
|
+
const category = this.container.querySelector(`.twm-tree-category[data-category-id="${categoryId}"]`);
|
|
422
|
+
const content = category?.querySelector('.twm-collapsible-content');
|
|
423
|
+
if (!content) return null;
|
|
424
|
+
|
|
425
|
+
const node = createTreeNode(options);
|
|
426
|
+
content.appendChild(node);
|
|
427
|
+
return node;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
addItem(nodeId, options) {
|
|
431
|
+
const node = this.container.querySelector(`.twm-tree-node[data-node-id="${nodeId}"]`);
|
|
432
|
+
const children = node?.querySelector('.twm-tree-node__children');
|
|
433
|
+
if (!children) return null;
|
|
434
|
+
|
|
435
|
+
const item = createTreeItem(options);
|
|
436
|
+
children.appendChild(item);
|
|
437
|
+
return item;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
select(itemId) {
|
|
441
|
+
return selectItem(this.container, itemId);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
filter(text) {
|
|
445
|
+
filterTree(this.container, text);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
clear() {
|
|
449
|
+
this.container.innerHTML = '';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
dispose() {
|
|
453
|
+
if (this.container) {
|
|
454
|
+
this.container.removeEventListener('click', this._boundHandleClick);
|
|
455
|
+
this.container.removeEventListener('dblclick', this._boundHandleDblClick);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export default TreeView;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WindowTaskbar - Singleton component that manages minimized windows in the global bottom bar.
|
|
3
|
+
*
|
|
4
|
+
* Listens for window events and renders taskbar buttons for minimized windows.
|
|
5
|
+
* Integrates with the existing global-bottom-bar element.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ManagedWindow } from './managed_window.js';
|
|
9
|
+
|
|
10
|
+
class WindowTaskbarManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this._minimizedWindows = new Map(); // id -> { title, icon }
|
|
13
|
+
this._containerEl = null;
|
|
14
|
+
this._leftDivider = null;
|
|
15
|
+
this._rightDivider = null;
|
|
16
|
+
this._initialized = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the taskbar. Call once after DOM is ready.
|
|
21
|
+
*/
|
|
22
|
+
init() {
|
|
23
|
+
if (this._initialized) return;
|
|
24
|
+
|
|
25
|
+
// Find or create the container in the global bottom bar
|
|
26
|
+
const bottomBar = document.querySelector('.twm-global-bottom-bar');
|
|
27
|
+
if (!bottomBar) {
|
|
28
|
+
console.warn('[WindowTaskbar] twm-global-bottom-bar not found');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create left divider (always present but hidden when no windows)
|
|
33
|
+
this._leftDivider = document.createElement('div');
|
|
34
|
+
this._leftDivider.className = 'twm-bar-windows__divider twm-bar-windows__divider--left';
|
|
35
|
+
|
|
36
|
+
// Insert bar-windows container between bar-left and bar-center
|
|
37
|
+
this._containerEl = document.createElement('div');
|
|
38
|
+
this._containerEl.className = 'twm-bar-windows';
|
|
39
|
+
|
|
40
|
+
// Create right divider (shown when there are windows)
|
|
41
|
+
this._rightDivider = document.createElement('div');
|
|
42
|
+
this._rightDivider.className = 'twm-bar-windows__divider twm-bar-windows__divider--right';
|
|
43
|
+
|
|
44
|
+
const barCenter = bottomBar.querySelector('.twm-bar-center');
|
|
45
|
+
if (barCenter) {
|
|
46
|
+
bottomBar.insertBefore(this._rightDivider, barCenter);
|
|
47
|
+
bottomBar.insertBefore(this._containerEl, this._rightDivider);
|
|
48
|
+
bottomBar.insertBefore(this._leftDivider, this._containerEl);
|
|
49
|
+
} else {
|
|
50
|
+
bottomBar.appendChild(this._leftDivider);
|
|
51
|
+
bottomBar.appendChild(this._containerEl);
|
|
52
|
+
bottomBar.appendChild(this._rightDivider);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Listen for window events
|
|
56
|
+
window.addEventListener('managed-window-minimized', (e) => this._onWindowMinimized(e));
|
|
57
|
+
window.addEventListener('managed-window-restored', (e) => this._onWindowRestored(e));
|
|
58
|
+
window.addEventListener('managed-window-closed', (e) => this._onWindowClosed(e));
|
|
59
|
+
|
|
60
|
+
this._initialized = true;
|
|
61
|
+
this._updateDividers();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle window minimized event.
|
|
66
|
+
*/
|
|
67
|
+
_onWindowMinimized(e) {
|
|
68
|
+
const { id, title, icon } = e.detail;
|
|
69
|
+
this._minimizedWindows.set(id, { title, icon });
|
|
70
|
+
this._render();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle window restored event.
|
|
75
|
+
*/
|
|
76
|
+
_onWindowRestored(e) {
|
|
77
|
+
const { id } = e.detail;
|
|
78
|
+
this._minimizedWindows.delete(id);
|
|
79
|
+
this._render();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle window closed event.
|
|
84
|
+
*/
|
|
85
|
+
_onWindowClosed(e) {
|
|
86
|
+
const { id } = e.detail;
|
|
87
|
+
this._minimizedWindows.delete(id);
|
|
88
|
+
this._render();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update divider visibility based on window count.
|
|
93
|
+
*/
|
|
94
|
+
_updateDividers() {
|
|
95
|
+
const hasWindows = this._minimizedWindows.size > 0;
|
|
96
|
+
if (this._leftDivider) {
|
|
97
|
+
this._leftDivider.style.display = hasWindows ? '' : 'none';
|
|
98
|
+
}
|
|
99
|
+
if (this._rightDivider) {
|
|
100
|
+
this._rightDivider.style.display = hasWindows ? '' : 'none';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render the taskbar buttons.
|
|
106
|
+
*/
|
|
107
|
+
_render() {
|
|
108
|
+
if (!this._containerEl) return;
|
|
109
|
+
|
|
110
|
+
this._containerEl.innerHTML = '';
|
|
111
|
+
|
|
112
|
+
for (const [id, { title, icon }] of this._minimizedWindows) {
|
|
113
|
+
const btn = document.createElement('button');
|
|
114
|
+
btn.className = 'twm-bar-windows__item';
|
|
115
|
+
btn.setAttribute('data-window-id', id);
|
|
116
|
+
btn.title = `Restore: ${title}`;
|
|
117
|
+
|
|
118
|
+
// Add icon
|
|
119
|
+
if (icon) {
|
|
120
|
+
const iconEl = document.createElement('span');
|
|
121
|
+
iconEl.className = 'twm-bar-windows__item-icon material-symbols-outlined';
|
|
122
|
+
iconEl.textContent = icon;
|
|
123
|
+
btn.appendChild(iconEl);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add title
|
|
127
|
+
const titleEl = document.createElement('span');
|
|
128
|
+
titleEl.className = 'twm-bar-windows__item-title';
|
|
129
|
+
titleEl.textContent = title;
|
|
130
|
+
btn.appendChild(titleEl);
|
|
131
|
+
|
|
132
|
+
btn.addEventListener('click', () => {
|
|
133
|
+
ManagedWindow.restore(id);
|
|
134
|
+
});
|
|
135
|
+
this._containerEl.appendChild(btn);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this._updateDividers();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Singleton instance
|
|
143
|
+
export const WindowTaskbar = new WindowTaskbarManager();
|