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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DetailHeader — shared header bar for detail views.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { createDetailHeader, updateDetailHeader } from '../ui/components/detail_header.js';
|
|
6
|
+
*
|
|
7
|
+
* const header = createDetailHeader({
|
|
8
|
+
* title: 'My Scenario',
|
|
9
|
+
* badges: [{ text: 'Static' }],
|
|
10
|
+
* renameable: true,
|
|
11
|
+
* onRename: async (name) => ({ success: true }),
|
|
12
|
+
* actions: [
|
|
13
|
+
* { key: 'run', label: 'Run', icon: 'play_arrow', variant: 'primary', handler: () => {} },
|
|
14
|
+
* { key: 'delete', label: 'Delete', icon: 'delete', variant: 'danger', handler: () => {} },
|
|
15
|
+
* ],
|
|
16
|
+
* });
|
|
17
|
+
* container.appendChild(header);
|
|
18
|
+
*
|
|
19
|
+
* To update in place (e.g., running state changed):
|
|
20
|
+
* updateDetailHeader(header, { ...newConfig });
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { attachInlineRenamer } from './inline_renamer.js';
|
|
24
|
+
|
|
25
|
+
const ESC = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} DetailHeaderBadge
|
|
29
|
+
* @property {string} text
|
|
30
|
+
* @property {string} [className] - Extra CSS modifier (e.g., 'detail-header__badge--mc')
|
|
31
|
+
* @property {string} [icon] - Material Symbols icon name
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} DetailHeaderAction
|
|
36
|
+
* @property {string} key - data-action value
|
|
37
|
+
* @property {string} label - Button text
|
|
38
|
+
* @property {string} icon - Material Symbols icon name
|
|
39
|
+
* @property {'primary'|'secondary'|'danger'} [variant='secondary']
|
|
40
|
+
* @property {boolean} [disabled=false]
|
|
41
|
+
* @property {string} [title] - Tooltip
|
|
42
|
+
* @property {Function} handler - Click callback
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} DetailHeaderConfig
|
|
47
|
+
* @property {string} title
|
|
48
|
+
* @property {string} [subtitle]
|
|
49
|
+
* @property {string} [icon] - Material icon before title
|
|
50
|
+
* @property {DetailHeaderBadge[]} [badges]
|
|
51
|
+
* @property {DetailHeaderAction[]} [actions]
|
|
52
|
+
* @property {HTMLElement[]} [extraElements] - Custom elements appended to actions area
|
|
53
|
+
* @property {boolean} [renameable=false]
|
|
54
|
+
* @property {Function} [onRename] - (newName) => Promise<{success, newValue?}>
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a detail header element.
|
|
59
|
+
* @param {DetailHeaderConfig} config
|
|
60
|
+
* @returns {HTMLElement}
|
|
61
|
+
*/
|
|
62
|
+
export function createDetailHeader(config) {
|
|
63
|
+
const header = document.createElement('header');
|
|
64
|
+
header.className = 'twm-detail-header';
|
|
65
|
+
_populate(header, config);
|
|
66
|
+
return header;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update an existing detail header in place.
|
|
71
|
+
* Destroys previous inline renamer and rebuilds inner DOM.
|
|
72
|
+
* @param {HTMLElement} headerEl
|
|
73
|
+
* @param {DetailHeaderConfig} config
|
|
74
|
+
*/
|
|
75
|
+
export function updateDetailHeader(headerEl, config) {
|
|
76
|
+
// Destroy previous renamer if any
|
|
77
|
+
headerEl._renamer?.destroy();
|
|
78
|
+
headerEl._renamer = null;
|
|
79
|
+
headerEl.innerHTML = '';
|
|
80
|
+
_populate(headerEl, config);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function _populate(header, config) {
|
|
86
|
+
const { title, subtitle, icon, badges = [], actions = [], extraElements = [], renameable = false, onRename } = config;
|
|
87
|
+
|
|
88
|
+
// Title row
|
|
89
|
+
const titleRow = document.createElement('div');
|
|
90
|
+
titleRow.className = 'twm-detail-header__title-row';
|
|
91
|
+
|
|
92
|
+
if (icon) {
|
|
93
|
+
const iconEl = document.createElement('span');
|
|
94
|
+
iconEl.className = 'twm-detail-header__icon material-symbols-outlined';
|
|
95
|
+
iconEl.textContent = icon;
|
|
96
|
+
titleRow.appendChild(iconEl);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nameEl = document.createElement('h2');
|
|
100
|
+
nameEl.className = 'twm-detail-header__name';
|
|
101
|
+
nameEl.textContent = title || '';
|
|
102
|
+
if (renameable) {
|
|
103
|
+
nameEl.dataset.renameable = '';
|
|
104
|
+
nameEl.title = 'Double-click to rename';
|
|
105
|
+
}
|
|
106
|
+
titleRow.appendChild(nameEl);
|
|
107
|
+
|
|
108
|
+
for (const badge of badges) {
|
|
109
|
+
const badgeEl = document.createElement('span');
|
|
110
|
+
badgeEl.className = `detail-header__badge${badge.className ? ` ${badge.className}` : ''}`;
|
|
111
|
+
if (badge.icon) {
|
|
112
|
+
const bi = document.createElement('span');
|
|
113
|
+
bi.className = 'material-symbols-outlined';
|
|
114
|
+
bi.textContent = badge.icon;
|
|
115
|
+
badgeEl.appendChild(bi);
|
|
116
|
+
}
|
|
117
|
+
badgeEl.appendChild(document.createTextNode(badge.text));
|
|
118
|
+
titleRow.appendChild(badgeEl);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
header.appendChild(titleRow);
|
|
122
|
+
|
|
123
|
+
// Subtitle
|
|
124
|
+
if (subtitle) {
|
|
125
|
+
const sub = document.createElement('div');
|
|
126
|
+
sub.className = 'twm-detail-header__subtitle';
|
|
127
|
+
sub.innerHTML = subtitle; // may contain <strong> etc.
|
|
128
|
+
header.appendChild(sub);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Actions
|
|
132
|
+
if (actions.length > 0 || extraElements.length > 0) {
|
|
133
|
+
const actionsEl = document.createElement('div');
|
|
134
|
+
actionsEl.className = 'twm-detail-header__actions';
|
|
135
|
+
|
|
136
|
+
for (const extra of extraElements) {
|
|
137
|
+
actionsEl.appendChild(extra);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const action of actions) {
|
|
141
|
+
const btn = document.createElement('button');
|
|
142
|
+
btn.type = 'button';
|
|
143
|
+
const variant = action.variant || 'secondary';
|
|
144
|
+
btn.className = `twm-detail-header__btn detail-header__btn--${variant}`;
|
|
145
|
+
btn.dataset.action = action.key;
|
|
146
|
+
if (action.disabled) btn.disabled = true;
|
|
147
|
+
if (action.title) btn.title = action.title;
|
|
148
|
+
|
|
149
|
+
const iconSpan = document.createElement('span');
|
|
150
|
+
iconSpan.className = 'material-symbols-outlined';
|
|
151
|
+
iconSpan.textContent = action.icon;
|
|
152
|
+
btn.appendChild(iconSpan);
|
|
153
|
+
btn.appendChild(document.createTextNode(` ${action.label}`));
|
|
154
|
+
|
|
155
|
+
btn.addEventListener('click', () => action.handler?.());
|
|
156
|
+
actionsEl.appendChild(btn);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
header.appendChild(actionsEl);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Inline rename
|
|
163
|
+
if (renameable && onRename) {
|
|
164
|
+
const renamer = attachInlineRenamer({
|
|
165
|
+
label: nameEl,
|
|
166
|
+
onCommit: async (newName) => {
|
|
167
|
+
if (!newName?.trim()) return { success: false };
|
|
168
|
+
return await onRename(newName.trim());
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
nameEl.addEventListener('dblclick', (e) => {
|
|
172
|
+
e.stopPropagation();
|
|
173
|
+
renamer.start(e);
|
|
174
|
+
});
|
|
175
|
+
header._renamer = renamer;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gallery_picker.js — single-select card grid for the starter catalog.
|
|
3
|
+
*
|
|
4
|
+
* G1.3 in docs/STARTER_GALLERY_REQUIREMENTS.md. Used by every
|
|
5
|
+
* "New X" modal to let the user pick a starter shape (formerly
|
|
6
|
+
* called a "template"). The picker is mounted into a host element
|
|
7
|
+
* and fires `onChange(entry)` whenever the selection changes.
|
|
8
|
+
*
|
|
9
|
+
* Each entry is the raw gallery dict from `pywebview.api.gallery_list`:
|
|
10
|
+
*
|
|
11
|
+
* { id, kind, label, description, fields }
|
|
12
|
+
*
|
|
13
|
+
* The picker shows label + description as a card. `fields` is
|
|
14
|
+
* passed through to the caller untouched on select — the New-X
|
|
15
|
+
* handler decides what to do with it.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
*
|
|
19
|
+
* const picker = await mountGalleryPicker(hostEl, {
|
|
20
|
+
* kind: 'agent',
|
|
21
|
+
* selectedId: 'blank',
|
|
22
|
+
* onChange: (entry) => { … },
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // later …
|
|
26
|
+
* picker.getSelected(); // → { id, kind, label, description, fields }
|
|
27
|
+
* picker.dispose();
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
function esc(s) {
|
|
31
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
32
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
33
|
+
}[c]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Mount a starter-gallery picker into `hostEl`.
|
|
39
|
+
*
|
|
40
|
+
* @param {HTMLElement} hostEl
|
|
41
|
+
* @param {object} opts
|
|
42
|
+
* @param {string} opts.kind — gallery entries to load (e.g. 'agent').
|
|
43
|
+
* When undefined, every kind shows up.
|
|
44
|
+
* @param {string} [opts.selectedId] — id to pre-select. If absent, the
|
|
45
|
+
* first entry wins.
|
|
46
|
+
* @param {function} [opts.onChange] — `(entry) => void` fired on selection
|
|
47
|
+
* change AND on initial mount.
|
|
48
|
+
* @param {object[]} [opts.entries] — explicit entries (skips bridge fetch).
|
|
49
|
+
* Useful for tests + offline previews.
|
|
50
|
+
* @param {string} [opts.emptyLabel] — copy shown when the catalog is empty
|
|
51
|
+
* for that kind.
|
|
52
|
+
* @returns {Promise<{getSelected, setSelectedById, dispose}>}
|
|
53
|
+
*/
|
|
54
|
+
export async function mountGalleryPicker(hostEl, opts = {}) {
|
|
55
|
+
const {
|
|
56
|
+
kind = null,
|
|
57
|
+
selectedId = null,
|
|
58
|
+
onChange = () => {},
|
|
59
|
+
entries = null,
|
|
60
|
+
emptyLabel = 'No starters available for this kind.',
|
|
61
|
+
} = opts;
|
|
62
|
+
if (!hostEl) throw new Error('mountGalleryPicker: hostEl is required');
|
|
63
|
+
|
|
64
|
+
let catalog = entries;
|
|
65
|
+
if (!catalog) {
|
|
66
|
+
const api = window.pywebview?.api;
|
|
67
|
+
try {
|
|
68
|
+
catalog = (await api?.gallery_list?.(kind)) || [];
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn('[gallery-picker] gallery_list failed', err);
|
|
71
|
+
catalog = [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catalog = Array.isArray(catalog) ? catalog : [];
|
|
75
|
+
|
|
76
|
+
hostEl.classList.add('twm-gallery-picker');
|
|
77
|
+
if (catalog.length === 0) {
|
|
78
|
+
hostEl.innerHTML = `<div class="twm-gallery-picker__empty">${esc(emptyLabel)}</div>`;
|
|
79
|
+
return {
|
|
80
|
+
getSelected: () => null,
|
|
81
|
+
setSelectedById: () => false,
|
|
82
|
+
dispose: () => { hostEl.classList.remove('twm-gallery-picker'); },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let selected = catalog.find((e) => e?.id === selectedId)
|
|
87
|
+
|| catalog[0];
|
|
88
|
+
|
|
89
|
+
const render = () => {
|
|
90
|
+
hostEl.innerHTML = catalog.map((e, i) => {
|
|
91
|
+
const isSel = e === selected;
|
|
92
|
+
const desc = e?.description
|
|
93
|
+
? `<p class="twm-gallery-card__desc">${esc(e.description)}</p>`
|
|
94
|
+
: '';
|
|
95
|
+
return `
|
|
96
|
+
<button type="button"
|
|
97
|
+
class="twm-gallery-card ${isSel ? 'is-selected' : ''}"
|
|
98
|
+
data-idx="${i}"
|
|
99
|
+
title="${esc(e?.id || '')}">
|
|
100
|
+
<span class="twm-gallery-card__label">${esc(e?.label || e?.id || '')}</span>
|
|
101
|
+
${desc}
|
|
102
|
+
</button>
|
|
103
|
+
`;
|
|
104
|
+
}).join('');
|
|
105
|
+
};
|
|
106
|
+
render();
|
|
107
|
+
|
|
108
|
+
const onClick = (ev) => {
|
|
109
|
+
const card = ev.target.closest?.('.twm-gallery-card');
|
|
110
|
+
if (!card || !hostEl.contains(card)) return;
|
|
111
|
+
const idx = Number(card.dataset.idx);
|
|
112
|
+
const next = catalog[idx];
|
|
113
|
+
if (!next || next === selected) return;
|
|
114
|
+
selected = next;
|
|
115
|
+
render();
|
|
116
|
+
try { onChange(selected); } catch (err) {
|
|
117
|
+
console.warn('[gallery-picker] onChange threw', err);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
hostEl.addEventListener('click', onClick);
|
|
121
|
+
|
|
122
|
+
// Fire the initial selection so the caller can prime any
|
|
123
|
+
// downstream UI (preview pane, defaults, etc.) without a
|
|
124
|
+
// special case for first-render.
|
|
125
|
+
try { onChange(selected); } catch (err) {
|
|
126
|
+
console.warn('[gallery-picker] initial onChange threw', err);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
getSelected: () => (selected ? { ...selected } : null),
|
|
131
|
+
setSelectedById: (id) => {
|
|
132
|
+
const next = catalog.find((e) => e?.id === id);
|
|
133
|
+
if (!next || next === selected) return false;
|
|
134
|
+
selected = next;
|
|
135
|
+
render();
|
|
136
|
+
try { onChange(selected); } catch (err) {
|
|
137
|
+
console.warn('[gallery-picker] onChange threw', err);
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
},
|
|
141
|
+
dispose: () => {
|
|
142
|
+
hostEl.removeEventListener('click', onClick);
|
|
143
|
+
hostEl.innerHTML = '';
|
|
144
|
+
hostEl.classList.remove('twm-gallery-picker');
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InlineRenamer (js_new)
|
|
3
|
+
* Minimal inline editing helper used by node labels and scenario titles.
|
|
4
|
+
* Attaches an input on demand, commits through provided callback, and cleans up deterministically.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function assertLabel(label) {
|
|
8
|
+
if (!label || typeof label.appendChild !== 'function') {
|
|
9
|
+
throw new Error('[InlineRenamer] options.label must be an HTMLElement');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildInput(label, options) {
|
|
14
|
+
const input = document.createElement('input');
|
|
15
|
+
input.type = 'text';
|
|
16
|
+
input.className = ['twm-node-edit-input', options?.inputClass || ''].join(' ').trim();
|
|
17
|
+
input.placeholder = options?.placeholder || label.textContent || 'Name';
|
|
18
|
+
input.value = options?.initialValue ?? label.textContent ?? '';
|
|
19
|
+
input.spellcheck = false;
|
|
20
|
+
input.autocomplete = 'off';
|
|
21
|
+
input.setAttribute('aria-label', 'Edit name');
|
|
22
|
+
return input;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function attachInlineRenamer(options = {}) {
|
|
26
|
+
const label = options.label;
|
|
27
|
+
assertLabel(label);
|
|
28
|
+
|
|
29
|
+
let input = null;
|
|
30
|
+
let editing = false;
|
|
31
|
+
let destroyed = false;
|
|
32
|
+
let originalText = label.textContent || '';
|
|
33
|
+
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
if (input && input.parentNode === label) {
|
|
36
|
+
input.remove();
|
|
37
|
+
}
|
|
38
|
+
input = null;
|
|
39
|
+
editing = false;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const cancel = () => {
|
|
43
|
+
if (destroyed) return;
|
|
44
|
+
cleanup();
|
|
45
|
+
label.textContent = originalText;
|
|
46
|
+
options.onCancel?.({ value: originalText });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const commit = async (reason = 'commit') => {
|
|
50
|
+
if (destroyed || !input) return;
|
|
51
|
+
const raw = input.value;
|
|
52
|
+
let next = raw;
|
|
53
|
+
try {
|
|
54
|
+
next = options.transformInput ? options.transformInput(raw, { reason }) : raw;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
window.logger?.warn('inline-renamer', 'transformInput failed', err);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = (await options.onCommit?.(next, { reason })) || {};
|
|
61
|
+
if (result.success === false) {
|
|
62
|
+
if (result.keepEditing) {
|
|
63
|
+
input.focus();
|
|
64
|
+
input.select();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
cancel();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const finalValue = result.newValue ?? next ?? '';
|
|
71
|
+
label.textContent = finalValue;
|
|
72
|
+
cleanup();
|
|
73
|
+
options.onFinish?.({ value: finalValue });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
window.logger?.warn('inline-renamer', 'onCommit failed', err);
|
|
76
|
+
cancel();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const start = (triggerEvent = null) => {
|
|
81
|
+
if (destroyed || editing) return;
|
|
82
|
+
editing = true;
|
|
83
|
+
originalText = label.textContent || '';
|
|
84
|
+
label.textContent = '';
|
|
85
|
+
input = buildInput(label, { ...options, initialValue: originalText });
|
|
86
|
+
label.appendChild(input);
|
|
87
|
+
|
|
88
|
+
const stopEvents = (ev) => ev.stopPropagation();
|
|
89
|
+
input.addEventListener('mousedown', stopEvents);
|
|
90
|
+
input.addEventListener('click', stopEvents);
|
|
91
|
+
|
|
92
|
+
input.addEventListener('keydown', (ev) => {
|
|
93
|
+
if (ev.key === 'Enter') {
|
|
94
|
+
ev.preventDefault();
|
|
95
|
+
commit('enter');
|
|
96
|
+
} else if (ev.key === 'Escape') {
|
|
97
|
+
ev.preventDefault();
|
|
98
|
+
cancel();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
input.addEventListener('blur', () => commit('blur'));
|
|
103
|
+
|
|
104
|
+
options.onStart?.({ value: originalText, triggerEvent });
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
input?.focus();
|
|
107
|
+
input?.select?.();
|
|
108
|
+
}, 0);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const destroy = () => {
|
|
112
|
+
destroyed = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return { start, cancel, commit, destroy };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// `window.InlineRenamer` used to be installed here, at module scope, "for existing
|
|
120
|
+
// callers". There are none: every caller in the tree — EcoAgent's and the
|
|
121
|
+
// library's — uses the `attachInlineRenamer` named export. It was a global that
|
|
122
|
+
// nothing read.
|
|
123
|
+
//
|
|
124
|
+
// It also made this module UNIMPORTABLE outside a browser, because the assignment
|
|
125
|
+
// ran on import. That is how it was found: a node test that does nothing but
|
|
126
|
+
// `import '@flexdesk/widgets'` died with `ReferenceError: window is not defined`. A
|
|
127
|
+
// library whose entry point cannot be imported without a DOM is a library that
|
|
128
|
+
// cannot be unit-tested, server-rendered, or loaded in a worker — and
|
|
129
|
+
// package.json's `"sideEffects": ["*.css"]` was a lie while this line existed.
|