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,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel State Machine
|
|
3
|
+
*
|
|
4
|
+
* Reusable FSM for panel visibility management. Formalizes a four-state
|
|
5
|
+
* model used by the right panel and bottom panel:
|
|
6
|
+
*
|
|
7
|
+
* disabled – Panel hidden, toggle button disabled. Cannot be shown.
|
|
8
|
+
* hidden – Panel hidden, toggle button enabled. User can show it.
|
|
9
|
+
* on-demand – Panel hidden by default, auto-shown by external triggers
|
|
10
|
+
* (e.g. node selection). Toggle button enabled.
|
|
11
|
+
* pinned – Panel always visible. Toggle button shows pinned indicator.
|
|
12
|
+
*
|
|
13
|
+
* Different panels use different subsets of these states:
|
|
14
|
+
* Right panel: disabled ↔ on-demand ↔ pinned (trigger-based visibility)
|
|
15
|
+
* Bottom panel: disabled ↔ hidden ↔ pinned (simple show/hide toggle)
|
|
16
|
+
*
|
|
17
|
+
* Uses the composition pattern (like AiStateMachine): wraps a StateMachine
|
|
18
|
+
* instance in a private field and exposes a panel-domain public API.
|
|
19
|
+
*
|
|
20
|
+
* DOM manipulation is delegated to callback functions passed at construction
|
|
21
|
+
* time, keeping the FSM itself pure and testable.
|
|
22
|
+
*
|
|
23
|
+
* @module ui/controllers/panel_state_machine
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { StateMachine } from '../../core/state_machine.js';
|
|
27
|
+
|
|
28
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export const PANEL_STATES = Object.freeze({
|
|
31
|
+
DISABLED: 'disabled',
|
|
32
|
+
HIDDEN: 'hidden',
|
|
33
|
+
ON_DEMAND: 'on-demand',
|
|
34
|
+
PINNED: 'pinned',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const PANEL_TRANSITIONS = Object.freeze({
|
|
38
|
+
ENABLE: 'panel:mode:enable',
|
|
39
|
+
ENABLE_PINNED: 'panel:mode:enable-pinned',
|
|
40
|
+
ENABLE_HIDDEN: 'panel:mode:enable-hidden',
|
|
41
|
+
DISABLE: 'panel:mode:disable',
|
|
42
|
+
PIN: 'panel:pin',
|
|
43
|
+
UNPIN: 'panel:unpin',
|
|
44
|
+
SHOW: 'panel:show',
|
|
45
|
+
HIDE: 'panel:hide',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
// ─── State Machine ──────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export class PanelStateMachine {
|
|
52
|
+
|
|
53
|
+
/** @type {StateMachine} */
|
|
54
|
+
#machine;
|
|
55
|
+
|
|
56
|
+
/** @type {import('../../core/event_bus.js').default} */
|
|
57
|
+
#eventBus;
|
|
58
|
+
|
|
59
|
+
/** @type {object|null} */
|
|
60
|
+
#logger;
|
|
61
|
+
|
|
62
|
+
// ── Identity ────────────────────────────────────────────────────────────
|
|
63
|
+
#name;
|
|
64
|
+
|
|
65
|
+
// ── DOM delegation ──────────────────────────────────────────────────────
|
|
66
|
+
#callbacks;
|
|
67
|
+
|
|
68
|
+
// ── Persistence / config ────────────────────────────────────────────────
|
|
69
|
+
#persistenceKey;
|
|
70
|
+
#supportsCollapse = false;
|
|
71
|
+
|
|
72
|
+
// ── Trigger-based visibility (on-demand state) ──────────────────────────
|
|
73
|
+
#triggerCount = 0;
|
|
74
|
+
|
|
75
|
+
// ── Orthogonal state ────────────────────────────────────────────────────
|
|
76
|
+
// null initial value ensures the first #applyVisibility call always fires
|
|
77
|
+
// onShow/onHide to synchronize DOM with FSM state (the DOM starts without
|
|
78
|
+
// any hidden class, so we must not assume it matches #visible = false).
|
|
79
|
+
#visible = null;
|
|
80
|
+
#collapsed = false;
|
|
81
|
+
#height = null;
|
|
82
|
+
|
|
83
|
+
// ── Overrides ───────────────────────────────────────────────────────────
|
|
84
|
+
#overrideHidden = false;
|
|
85
|
+
#widgetConfigActive = false;
|
|
86
|
+
|
|
87
|
+
// ── Persisted pin (read before first enable) ────────────────────────────
|
|
88
|
+
#persistedPinned = false;
|
|
89
|
+
|
|
90
|
+
// ── Config: which state to enter when enable() is called and not pinned ──
|
|
91
|
+
#unpinnedState;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {object} config
|
|
95
|
+
* @param {string} config.name Panel identity ('right', 'bottom', etc.)
|
|
96
|
+
* @param {import('../../core/event_bus.js').default} config.eventBus
|
|
97
|
+
* @param {object} [config.logger]
|
|
98
|
+
* @param {string} config.persistenceKey localStorage key
|
|
99
|
+
* @param {boolean} [config.defaultPinned] Default pin state when no persisted data exists
|
|
100
|
+
* @param {string} [config.unpinnedState] State for enable() when not pinned: 'on-demand' (default) or 'hidden'
|
|
101
|
+
* @param {object} config.callbacks DOM manipulation hooks:
|
|
102
|
+
* @param {Function} config.callbacks.onShow (reason) => void
|
|
103
|
+
* @param {Function} config.callbacks.onHide (reason) => void
|
|
104
|
+
* @param {Function} config.callbacks.onDisable () => void
|
|
105
|
+
* @param {Function} config.callbacks.onEnable () => void
|
|
106
|
+
* @param {Function} [config.callbacks.onPinChanged] (pinned) => void
|
|
107
|
+
* @param {Function} [config.callbacks.onVisibilityChanged] (visible) => void
|
|
108
|
+
* @param {Function} [config.callbacks.onCollapseChanged] (collapsed, height) => void
|
|
109
|
+
*/
|
|
110
|
+
constructor(config) {
|
|
111
|
+
this.#name = config.name;
|
|
112
|
+
this.#eventBus = config.eventBus;
|
|
113
|
+
this.#logger = config.logger ?? null;
|
|
114
|
+
this.#callbacks = config.callbacks;
|
|
115
|
+
this.#persistenceKey = config.persistenceKey;
|
|
116
|
+
this.#persistedPinned = config.defaultPinned ?? false;
|
|
117
|
+
this.#unpinnedState = config.unpinnedState ?? PANEL_STATES.ON_DEMAND;
|
|
118
|
+
|
|
119
|
+
const self = this;
|
|
120
|
+
|
|
121
|
+
this.#machine = new StateMachine({
|
|
122
|
+
name: `panel-${config.name}`,
|
|
123
|
+
initialState: PANEL_STATES.DISABLED,
|
|
124
|
+
states: {
|
|
125
|
+
[PANEL_STATES.DISABLED]: {
|
|
126
|
+
onEnter() { self.#onEnterDisabled(); },
|
|
127
|
+
transitions: {
|
|
128
|
+
[PANEL_TRANSITIONS.ENABLE]: { target: PANEL_STATES.ON_DEMAND },
|
|
129
|
+
[PANEL_TRANSITIONS.ENABLE_PINNED]: { target: PANEL_STATES.PINNED },
|
|
130
|
+
[PANEL_TRANSITIONS.ENABLE_HIDDEN]: { target: PANEL_STATES.HIDDEN },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
[PANEL_STATES.HIDDEN]: {
|
|
134
|
+
onEnter() { self.#onEnterHidden(); },
|
|
135
|
+
transitions: {
|
|
136
|
+
[PANEL_TRANSITIONS.DISABLE]: { target: PANEL_STATES.DISABLED },
|
|
137
|
+
[PANEL_TRANSITIONS.SHOW]: { target: PANEL_STATES.PINNED },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
[PANEL_STATES.ON_DEMAND]: {
|
|
141
|
+
onEnter() { self.#onEnterOnDemand(); },
|
|
142
|
+
onExit() { self.#onExitOnDemand(); },
|
|
143
|
+
transitions: {
|
|
144
|
+
[PANEL_TRANSITIONS.DISABLE]: { target: PANEL_STATES.DISABLED },
|
|
145
|
+
[PANEL_TRANSITIONS.PIN]: { target: PANEL_STATES.PINNED },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
[PANEL_STATES.PINNED]: {
|
|
149
|
+
onEnter() { self.#onEnterPinned(); },
|
|
150
|
+
transitions: {
|
|
151
|
+
[PANEL_TRANSITIONS.DISABLE]: { target: PANEL_STATES.DISABLED },
|
|
152
|
+
[PANEL_TRANSITIONS.UNPIN]: { target: PANEL_STATES.ON_DEMAND },
|
|
153
|
+
[PANEL_TRANSITIONS.HIDE]: { target: PANEL_STATES.HIDDEN },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
eventBus: config.eventBus,
|
|
158
|
+
logger: config.logger,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
163
|
+
// Queries
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
/** Current FSM state. */
|
|
167
|
+
getState() { return this.#machine.getState(); }
|
|
168
|
+
|
|
169
|
+
/** True when in pinned state. */
|
|
170
|
+
isPinned() { return this.getState() === PANEL_STATES.PINNED; }
|
|
171
|
+
|
|
172
|
+
/** True when in disabled state. */
|
|
173
|
+
isDisabled() { return this.getState() === PANEL_STATES.DISABLED; }
|
|
174
|
+
|
|
175
|
+
/** True when in hidden state (not visible, but toggle enabled). */
|
|
176
|
+
isHidden() { return this.getState() === PANEL_STATES.HIDDEN; }
|
|
177
|
+
|
|
178
|
+
/** True when the panel is currently visible to the user. */
|
|
179
|
+
isVisible() { return this.#visible; }
|
|
180
|
+
|
|
181
|
+
/** True when the panel is collapsed (bottom panel only). */
|
|
182
|
+
isCollapsed() { return this.#collapsed; }
|
|
183
|
+
|
|
184
|
+
/** Current stored height (for collapse restore). */
|
|
185
|
+
getHeight() { return this.#height; }
|
|
186
|
+
|
|
187
|
+
/** Whether widget config override is active. */
|
|
188
|
+
get widgetConfigActive() { return this.#widgetConfigActive; }
|
|
189
|
+
|
|
190
|
+
/** Whether collapse operations are supported (runtime-configurable per mode). */
|
|
191
|
+
get supportsCollapse() { return this.#supportsCollapse; }
|
|
192
|
+
set supportsCollapse(value) { this.#supportsCollapse = Boolean(value); }
|
|
193
|
+
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
195
|
+
// Mode transitions (called by ApplicationShell._switchMode)
|
|
196
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Enable the panel for the current mode.
|
|
200
|
+
*
|
|
201
|
+
* With no argument, transitions from disabled → on-demand, hidden, or
|
|
202
|
+
* pinned based on persisted pin state and unpinnedState config.
|
|
203
|
+
*
|
|
204
|
+
* With an explicit `initialState`, forces that specific target state
|
|
205
|
+
* regardless of persistence. Useful when a mode always needs a specific
|
|
206
|
+
* starting state (e.g. ETL designer always starts as on-demand).
|
|
207
|
+
*
|
|
208
|
+
* If already enabled (hidden, on-demand, or pinned), this is a no-op.
|
|
209
|
+
*
|
|
210
|
+
* Note: getState() can return null during the StateMachine's async init
|
|
211
|
+
* microtask. We treat null the same as disabled — the transition() call
|
|
212
|
+
* internally awaits _ready, so the machine will be in 'disabled' by the
|
|
213
|
+
* time the transition executes.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} [initialState] Force a specific target state: 'on-demand', 'hidden', or 'pinned'
|
|
216
|
+
*/
|
|
217
|
+
enable(initialState) {
|
|
218
|
+
const state = this.getState();
|
|
219
|
+
if (state !== PANEL_STATES.DISABLED && state !== null) return;
|
|
220
|
+
|
|
221
|
+
if (initialState === PANEL_STATES.ON_DEMAND) {
|
|
222
|
+
this.#machine.transition(PANEL_TRANSITIONS.ENABLE);
|
|
223
|
+
} else if (initialState === PANEL_STATES.HIDDEN) {
|
|
224
|
+
this.#machine.transition(PANEL_TRANSITIONS.ENABLE_HIDDEN);
|
|
225
|
+
} else if (initialState === PANEL_STATES.PINNED) {
|
|
226
|
+
this.#machine.transition(PANEL_TRANSITIONS.ENABLE_PINNED);
|
|
227
|
+
} else if (this.#persistedPinned) {
|
|
228
|
+
this.#machine.transition(PANEL_TRANSITIONS.ENABLE_PINNED);
|
|
229
|
+
} else if (this.#unpinnedState === PANEL_STATES.HIDDEN) {
|
|
230
|
+
this.#machine.transition(PANEL_TRANSITIONS.ENABLE_HIDDEN);
|
|
231
|
+
} else {
|
|
232
|
+
this.#machine.transition(PANEL_TRANSITIONS.ENABLE);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Disable the panel for the current mode.
|
|
238
|
+
* Transitions from any enabled state → disabled.
|
|
239
|
+
* If already disabled (or null during async init), this is a no-op.
|
|
240
|
+
*/
|
|
241
|
+
disable() {
|
|
242
|
+
const state = this.getState();
|
|
243
|
+
if (state === PANEL_STATES.DISABLED || state === null) return;
|
|
244
|
+
this.#machine.transition(PANEL_TRANSITIONS.DISABLE);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
248
|
+
// Pin (called by toggle button click)
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
250
|
+
|
|
251
|
+
/** Toggle between on-demand and pinned. No-op if disabled. */
|
|
252
|
+
togglePin() {
|
|
253
|
+
const state = this.getState();
|
|
254
|
+
if (state === PANEL_STATES.ON_DEMAND) {
|
|
255
|
+
this.pin();
|
|
256
|
+
} else if (state === PANEL_STATES.PINNED) {
|
|
257
|
+
this.unpin();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** on-demand → pinned. */
|
|
262
|
+
pin() {
|
|
263
|
+
if (this.getState() !== PANEL_STATES.ON_DEMAND) return;
|
|
264
|
+
this.#machine.transition(PANEL_TRANSITIONS.PIN);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** pinned → on-demand. */
|
|
268
|
+
unpin() {
|
|
269
|
+
if (this.getState() !== PANEL_STATES.PINNED) return;
|
|
270
|
+
this.#machine.transition(PANEL_TRANSITIONS.UNPIN);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
274
|
+
// Show / Hide (hidden ↔ pinned, for panels without trigger-based visibility)
|
|
275
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
276
|
+
|
|
277
|
+
/** hidden → pinned. No-op if not in hidden state. */
|
|
278
|
+
show() {
|
|
279
|
+
if (this.getState() !== PANEL_STATES.HIDDEN) return;
|
|
280
|
+
this.#machine.transition(PANEL_TRANSITIONS.SHOW);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** pinned → hidden. No-op if not in pinned state. */
|
|
284
|
+
hide() {
|
|
285
|
+
if (this.getState() !== PANEL_STATES.PINNED) return;
|
|
286
|
+
this.#machine.transition(PANEL_TRANSITIONS.HIDE);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Toggle between hidden and pinned. No-op if disabled or on-demand. */
|
|
290
|
+
toggleShow() {
|
|
291
|
+
const state = this.getState();
|
|
292
|
+
if (state === PANEL_STATES.HIDDEN) {
|
|
293
|
+
this.show();
|
|
294
|
+
} else if (state === PANEL_STATES.PINNED) {
|
|
295
|
+
this.hide();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
300
|
+
// Smart toggle (toolbar click handler)
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The default action for a toolbar toggle click.
|
|
305
|
+
* Delegates to the appropriate operation based on FSM state and config:
|
|
306
|
+
* - Pinned + supportsCollapse → toggleCollapse
|
|
307
|
+
* - On-demand or pinned (no collapse) → togglePin
|
|
308
|
+
* - Hidden → show
|
|
309
|
+
*/
|
|
310
|
+
toggle() {
|
|
311
|
+
const state = this.getState();
|
|
312
|
+
if (state === PANEL_STATES.DISABLED) return;
|
|
313
|
+
if (this.#supportsCollapse && state === PANEL_STATES.PINNED) {
|
|
314
|
+
this.toggleCollapse();
|
|
315
|
+
} else if (state === PANEL_STATES.HIDDEN) {
|
|
316
|
+
this.show();
|
|
317
|
+
} else {
|
|
318
|
+
this.togglePin();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
323
|
+
// On-demand triggers
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Increment or decrement the trigger count and sync visibility.
|
|
328
|
+
* In on-demand state, the panel is visible when triggerCount > 0.
|
|
329
|
+
* In pinned state, triggers are tracked but don't affect visibility.
|
|
330
|
+
* In disabled state, triggers are tracked but have no effect.
|
|
331
|
+
*/
|
|
332
|
+
setTriggerActive(active) {
|
|
333
|
+
if (active) {
|
|
334
|
+
this.#triggerCount++;
|
|
335
|
+
} else {
|
|
336
|
+
this.#triggerCount = Math.max(0, this.#triggerCount - 1);
|
|
337
|
+
}
|
|
338
|
+
this.#syncVisibility('trigger');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Reset all triggers to 0 and sync visibility. */
|
|
342
|
+
clearTriggers() {
|
|
343
|
+
this.#triggerCount = 0;
|
|
344
|
+
this.#syncVisibility('triggers-cleared');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
348
|
+
// Re-apply (mode switch support)
|
|
349
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Force re-apply DOM callbacks from the current FSM state.
|
|
353
|
+
*
|
|
354
|
+
* Use after mode switches where external code changed the DOM (swapped
|
|
355
|
+
* panel content, cleared tabs) while the FSM remained in an enabled
|
|
356
|
+
* state. The cached `#visible` flag is invalidated so `#applyVisibility`
|
|
357
|
+
* is guaranteed to fire `onShow`/`onHide` + `onCollapseChanged`.
|
|
358
|
+
*
|
|
359
|
+
* No-op when the FSM is disabled — disable() handles its own cleanup.
|
|
360
|
+
*/
|
|
361
|
+
reapply() {
|
|
362
|
+
if (this.isDisabled()) return;
|
|
363
|
+
this.#visible = !this.#visible;
|
|
364
|
+
this.#syncVisibility('reapply');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
368
|
+
// Collapse (bottom panel only)
|
|
369
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
370
|
+
|
|
371
|
+
collapse() {
|
|
372
|
+
if (!this.#supportsCollapse || !this.isVisible() || this.#collapsed) return;
|
|
373
|
+
this.#collapsed = true;
|
|
374
|
+
this.#callbacks.onCollapseChanged?.(true, this.#height);
|
|
375
|
+
this.#emitBus(`panel:${this.#name}:collapse:changed`, { collapsed: true });
|
|
376
|
+
this.#persist();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
expand() {
|
|
380
|
+
if (!this.#supportsCollapse || !this.isVisible() || !this.#collapsed) return;
|
|
381
|
+
this.#collapsed = false;
|
|
382
|
+
this.#callbacks.onCollapseChanged?.(false, this.#height);
|
|
383
|
+
this.#emitBus(`panel:${this.#name}:collapse:changed`, { collapsed: false });
|
|
384
|
+
this.#persist();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
toggleCollapse() {
|
|
388
|
+
if (this.#collapsed) {
|
|
389
|
+
this.expand();
|
|
390
|
+
} else {
|
|
391
|
+
this.collapse();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Store the last expanded height (called during resize or before collapse). */
|
|
396
|
+
setHeight(px) {
|
|
397
|
+
if (typeof px === 'number' && px > 40) {
|
|
398
|
+
this.#height = px;
|
|
399
|
+
this.#persist();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
404
|
+
// Overrides
|
|
405
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Force-hide the panel regardless of FSM state (welcome screen).
|
|
409
|
+
* When lifted, visibility re-derives from current state + triggers.
|
|
410
|
+
*/
|
|
411
|
+
setOverrideHidden(hidden) {
|
|
412
|
+
this.#overrideHidden = hidden;
|
|
413
|
+
if (hidden) {
|
|
414
|
+
this.#applyVisibility(false, 'override-hidden');
|
|
415
|
+
this.#callbacks.onDisable?.();
|
|
416
|
+
} else {
|
|
417
|
+
// Re-derive: if not disabled, re-enable toggle
|
|
418
|
+
if (!this.isDisabled()) {
|
|
419
|
+
this.#callbacks.onEnable?.();
|
|
420
|
+
}
|
|
421
|
+
this.#syncVisibility('override-lifted');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Widget config special case (right panel).
|
|
427
|
+
* When active, the toggle button stays enabled even in disabled state
|
|
428
|
+
* so the user can click it to close the config panel.
|
|
429
|
+
*/
|
|
430
|
+
setWidgetConfigActive(active) {
|
|
431
|
+
this.#widgetConfigActive = active;
|
|
432
|
+
// In disabled state, temporarily enable/disable the toggle
|
|
433
|
+
if (this.isDisabled()) {
|
|
434
|
+
if (active) {
|
|
435
|
+
this.#callbacks.onEnable?.();
|
|
436
|
+
} else {
|
|
437
|
+
this.#callbacks.onDisable?.();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
this.#callbacks.onVisibilityChanged?.(this.#visible);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
444
|
+
// Persistence
|
|
445
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Read persisted state from localStorage and apply it.
|
|
449
|
+
* Must be called after construction, before the first enable().
|
|
450
|
+
*/
|
|
451
|
+
hydrate() {
|
|
452
|
+
try {
|
|
453
|
+
if (typeof localStorage === 'undefined') return;
|
|
454
|
+
const raw = localStorage.getItem(this.#persistenceKey);
|
|
455
|
+
if (raw == null) return;
|
|
456
|
+
const parsed = JSON.parse(raw);
|
|
457
|
+
if (!parsed || typeof parsed !== 'object') return;
|
|
458
|
+
|
|
459
|
+
if (typeof parsed.pinned === 'boolean') {
|
|
460
|
+
this.#persistedPinned = parsed.pinned;
|
|
461
|
+
}
|
|
462
|
+
// Always restore collapse/height — supportsCollapse is set per mode
|
|
463
|
+
// and may not be configured yet at hydrate() time.
|
|
464
|
+
if (typeof parsed.collapsed === 'boolean') {
|
|
465
|
+
this.#collapsed = parsed.collapsed;
|
|
466
|
+
}
|
|
467
|
+
if (typeof parsed.height === 'number' && parsed.height > 40) {
|
|
468
|
+
this.#height = parsed.height;
|
|
469
|
+
}
|
|
470
|
+
this.#log('debug', 'Hydrated persisted state', {
|
|
471
|
+
pinned: this.#persistedPinned,
|
|
472
|
+
collapsed: this.#collapsed,
|
|
473
|
+
height: this.#height,
|
|
474
|
+
});
|
|
475
|
+
} catch (err) {
|
|
476
|
+
this.#log('warn', 'Failed to hydrate persisted state', { error: err });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
481
|
+
// Lifecycle
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
dispose() {
|
|
485
|
+
// StateMachine doesn't hold subscriptions, but future-proof
|
|
486
|
+
this.#machine = null;
|
|
487
|
+
this.#eventBus = null;
|
|
488
|
+
this.#callbacks = null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
492
|
+
// Private: state entry/exit hooks
|
|
493
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
494
|
+
|
|
495
|
+
#onEnterDisabled() {
|
|
496
|
+
this.#applyVisibility(false, 'disabled');
|
|
497
|
+
this.#callbacks.onDisable?.();
|
|
498
|
+
this.#callbacks.onPinChanged?.(false);
|
|
499
|
+
this.#emitStateChanged();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#onEnterHidden() {
|
|
503
|
+
this.#callbacks.onEnable?.();
|
|
504
|
+
this.#callbacks.onPinChanged?.(false);
|
|
505
|
+
this.#applyVisibility(false, 'hidden');
|
|
506
|
+
this.#persistedPinned = false;
|
|
507
|
+
this.#persist();
|
|
508
|
+
this.#emitStateChanged();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#onEnterOnDemand() {
|
|
512
|
+
this.#callbacks.onEnable?.();
|
|
513
|
+
this.#callbacks.onPinChanged?.(false);
|
|
514
|
+
this.#persistedPinned = false;
|
|
515
|
+
this.#persist();
|
|
516
|
+
// Derive visibility from current trigger count
|
|
517
|
+
this.#syncVisibility('on-demand-entered');
|
|
518
|
+
this.#emitStateChanged();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#onExitOnDemand() {
|
|
522
|
+
// Nothing to clean up — triggers are preserved across states
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#onEnterPinned() {
|
|
526
|
+
this.#callbacks.onEnable?.();
|
|
527
|
+
this.#callbacks.onPinChanged?.(true);
|
|
528
|
+
this.#persistedPinned = true;
|
|
529
|
+
this.#persist();
|
|
530
|
+
// Pinned = always visible (unless override).
|
|
531
|
+
// #applyVisibility handles onShow → onVisibilityChanged → onCollapseChanged.
|
|
532
|
+
this.#applyVisibility(true, 'pinned');
|
|
533
|
+
this.#emitStateChanged();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
537
|
+
// Private: visibility logic
|
|
538
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Derive whether the panel should be visible based on FSM state, triggers,
|
|
542
|
+
* and overrides. Call the appropriate show/hide callback if changed.
|
|
543
|
+
*/
|
|
544
|
+
#syncVisibility(reason) {
|
|
545
|
+
const state = this.getState();
|
|
546
|
+
let shouldBeVisible;
|
|
547
|
+
|
|
548
|
+
if (state === PANEL_STATES.DISABLED || state === PANEL_STATES.HIDDEN) {
|
|
549
|
+
shouldBeVisible = false;
|
|
550
|
+
} else if (state === PANEL_STATES.PINNED) {
|
|
551
|
+
shouldBeVisible = true;
|
|
552
|
+
} else {
|
|
553
|
+
// on-demand: visible when triggers are active
|
|
554
|
+
shouldBeVisible = this.#triggerCount > 0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Override takes precedence
|
|
558
|
+
if (this.#overrideHidden) {
|
|
559
|
+
shouldBeVisible = false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.#applyVisibility(shouldBeVisible, reason);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Apply visibility change if it differs from current state.
|
|
567
|
+
*
|
|
568
|
+
* Callback ordering matters:
|
|
569
|
+
* 1. onShow/onHide — basic DOM setup/teardown
|
|
570
|
+
* 2. onVisibilityChanged — toggle button active state (active = visible)
|
|
571
|
+
* 3. onCollapseChanged — override toggle active if collapsed (active = !collapsed)
|
|
572
|
+
*
|
|
573
|
+
* This ensures collapse-aware panels (flow mode) get the correct toggle state
|
|
574
|
+
* while trigger-based panels (ETL) where supportsCollapse=false just use visibility.
|
|
575
|
+
*/
|
|
576
|
+
#applyVisibility(visible, reason) {
|
|
577
|
+
if (visible === this.#visible) return;
|
|
578
|
+
this.#visible = visible;
|
|
579
|
+
|
|
580
|
+
if (visible) {
|
|
581
|
+
this.#callbacks.onShow?.(reason);
|
|
582
|
+
} else {
|
|
583
|
+
this.#callbacks.onHide?.(reason);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
this.#callbacks.onVisibilityChanged?.(visible);
|
|
587
|
+
|
|
588
|
+
// Restore collapse state when showing (fires after onVisibilityChanged
|
|
589
|
+
// so it can override the toggle active state for collapsed panels).
|
|
590
|
+
if (visible && this.#supportsCollapse) {
|
|
591
|
+
this.#callbacks.onCollapseChanged?.(this.#collapsed, this.#height);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.#emitBus(`panel:${this.#name}:visibility:changed`, { visible, reason });
|
|
595
|
+
this.#log('debug', `Panel ${visible ? 'shown' : 'hidden'}`, { reason });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
599
|
+
// Private: persistence
|
|
600
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
601
|
+
|
|
602
|
+
#persist() {
|
|
603
|
+
try {
|
|
604
|
+
if (typeof localStorage === 'undefined') return;
|
|
605
|
+
const payload = {
|
|
606
|
+
pinned: this.#persistedPinned,
|
|
607
|
+
collapsed: this.#collapsed,
|
|
608
|
+
height: this.#height,
|
|
609
|
+
};
|
|
610
|
+
localStorage.setItem(this.#persistenceKey, JSON.stringify(payload));
|
|
611
|
+
} catch (err) {
|
|
612
|
+
this.#log('warn', 'Failed to persist panel state', { error: err });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
617
|
+
// Private: EventBus + logging
|
|
618
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
619
|
+
|
|
620
|
+
#emitStateChanged() {
|
|
621
|
+
const state = this.getState();
|
|
622
|
+
const previous = this.#machine.previousState;
|
|
623
|
+
this.#emitBus(`panel:${this.#name}:state:changed`, { state, previousState: previous });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#emitBus(event, detail) {
|
|
627
|
+
try { this.#eventBus?.emit?.(event, detail); }
|
|
628
|
+
catch { /* swallow event handler errors */ }
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
#log(level, msg, data) {
|
|
632
|
+
this.#logger?.[level]?.('panel-fsm', `[${this.#name}] ${msg}`, data);
|
|
633
|
+
}
|
|
634
|
+
}
|