@panyam/tsappkit 0.0.3
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/README.md +98 -0
- package/dist/index.d.mts +866 -0
- package/dist/index.d.ts +866 -0
- package/dist/index.js +1736 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1718 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
- package/src/BasePage.ts +270 -0
- package/src/Component.ts +178 -0
- package/src/DOMUtils.ts +72 -0
- package/src/EventBus.ts +272 -0
- package/src/KeyboardShortcutManager.ts +435 -0
- package/src/LCMComponent.ts +140 -0
- package/src/LifecycleController.ts +213 -0
- package/src/MobileBottomDrawer.ts +236 -0
- package/src/Modal.ts +233 -0
- package/src/SplashScreen.ts +111 -0
- package/src/TemplateLoader.ts +101 -0
- package/src/ThemeManager.ts +100 -0
- package/src/ToastManager.ts +161 -0
- package/src/events.ts +5 -0
- package/src/index.ts +39 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
// src/EventBus.ts
|
|
2
|
+
var FunctionSubscriber = class {
|
|
3
|
+
constructor(handler) {
|
|
4
|
+
this.handler = handler;
|
|
5
|
+
}
|
|
6
|
+
handleBusEvent(eventType, data, subject, emitter) {
|
|
7
|
+
this.handler(data);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var EventBus = class {
|
|
11
|
+
constructor(debugMode = false) {
|
|
12
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
13
|
+
this.functionSubscribers = /* @__PURE__ */ new Map();
|
|
14
|
+
this.onceHandlers = /* @__PURE__ */ new Map();
|
|
15
|
+
this.debugMode = false;
|
|
16
|
+
this.debugMode = debugMode;
|
|
17
|
+
}
|
|
18
|
+
// ============== Simple Function-Based API ==============
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe to an event with a simple handler function
|
|
21
|
+
*/
|
|
22
|
+
on(eventType, handler) {
|
|
23
|
+
if (!this.functionSubscribers.has(eventType)) {
|
|
24
|
+
this.functionSubscribers.set(eventType, /* @__PURE__ */ new Map());
|
|
25
|
+
}
|
|
26
|
+
const handlersMap = this.functionSubscribers.get(eventType);
|
|
27
|
+
if (handlersMap.has(handler)) {
|
|
28
|
+
if (this.debugMode) {
|
|
29
|
+
console.log(`[EventBus] Handler already registered for '${eventType}'`);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const subscriber = new FunctionSubscriber(handler);
|
|
34
|
+
handlersMap.set(handler, subscriber);
|
|
35
|
+
this.addSubscription(eventType, null, subscriber);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Unsubscribe a handler function from an event
|
|
39
|
+
*/
|
|
40
|
+
off(eventType, handler) {
|
|
41
|
+
const handlersMap = this.functionSubscribers.get(eventType);
|
|
42
|
+
if (!handlersMap) return;
|
|
43
|
+
const subscriber = handlersMap.get(handler);
|
|
44
|
+
if (subscriber) {
|
|
45
|
+
handlersMap.delete(handler);
|
|
46
|
+
this.removeSubscription(eventType, null, subscriber);
|
|
47
|
+
if (handlersMap.size === 0) {
|
|
48
|
+
this.functionSubscribers.delete(eventType);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to an event for one-time execution
|
|
54
|
+
*/
|
|
55
|
+
once(eventType, handler) {
|
|
56
|
+
if (!this.onceHandlers.has(eventType)) {
|
|
57
|
+
this.onceHandlers.set(eventType, /* @__PURE__ */ new Set());
|
|
58
|
+
}
|
|
59
|
+
const subscriber = new FunctionSubscriber(handler);
|
|
60
|
+
this.onceHandlers.get(eventType).add(subscriber);
|
|
61
|
+
if (this.debugMode) {
|
|
62
|
+
console.log(`[EventBus] Added once handler for '${eventType}'`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ============== EventSubscriber-Based API ==============
|
|
66
|
+
/**
|
|
67
|
+
* Add a subscription using the EventSubscriber pattern
|
|
68
|
+
* Provides automatic idempotency - same subscriber object won't be added twice
|
|
69
|
+
*/
|
|
70
|
+
addSubscription(eventType, subject, subscriber) {
|
|
71
|
+
if (!this.subscribers.has(eventType)) {
|
|
72
|
+
this.subscribers.set(eventType, /* @__PURE__ */ new Set());
|
|
73
|
+
}
|
|
74
|
+
const subscribers = this.subscribers.get(eventType);
|
|
75
|
+
const wasAdded = !subscribers.has(subscriber);
|
|
76
|
+
if (wasAdded) {
|
|
77
|
+
subscribers.add(subscriber);
|
|
78
|
+
if (this.debugMode) {
|
|
79
|
+
console.log(`[EventBus] Added subscription to '${eventType}' for ${subscriber.constructor.name}`);
|
|
80
|
+
}
|
|
81
|
+
} else if (this.debugMode) {
|
|
82
|
+
console.log(`[EventBus] Subscription already exists for '${eventType}' and ${subscriber.constructor.name}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Remove a subscription using the EventSubscriber pattern
|
|
87
|
+
*/
|
|
88
|
+
removeSubscription(eventType, subject, subscriber) {
|
|
89
|
+
const subscribers = this.subscribers.get(eventType);
|
|
90
|
+
if (subscribers) {
|
|
91
|
+
const wasRemoved = subscribers.delete(subscriber);
|
|
92
|
+
if (this.debugMode && wasRemoved) {
|
|
93
|
+
console.log(`[EventBus] Removed subscription from '${eventType}' for ${subscriber.constructor.name}`);
|
|
94
|
+
}
|
|
95
|
+
if (subscribers.size === 0) {
|
|
96
|
+
this.subscribers.delete(eventType);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Emit an event to all subscribers
|
|
102
|
+
* @param eventType - The event type to emit
|
|
103
|
+
* @param data - The event data payload
|
|
104
|
+
* @param subject - The subject/subject entity that this event relates to (optional for simple API)
|
|
105
|
+
* @param emitter - The entity that emitted the event (optional for simple API)
|
|
106
|
+
*/
|
|
107
|
+
emit(eventType, data, subject, emitter) {
|
|
108
|
+
const subscribers = this.subscribers.get(eventType);
|
|
109
|
+
const onceHandlers = this.onceHandlers.get(eventType);
|
|
110
|
+
const subscriberCount = (subscribers?.size || 0) + (onceHandlers?.size || 0);
|
|
111
|
+
if (subscriberCount === 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (this.debugMode) {
|
|
115
|
+
console.log(`[EventBus] Emitting '${eventType}' to ${subscriberCount} subscribers`);
|
|
116
|
+
}
|
|
117
|
+
let successCount = 0;
|
|
118
|
+
let errorCount = 0;
|
|
119
|
+
if (subscribers) {
|
|
120
|
+
subscribers.forEach((subscriber) => {
|
|
121
|
+
try {
|
|
122
|
+
subscriber.handleBusEvent(eventType, data, subject, emitter);
|
|
123
|
+
successCount++;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
errorCount++;
|
|
126
|
+
console.error(
|
|
127
|
+
`[EventBus] Error in EventSubscriber handler for '${eventType}' in subscriber '${subscriber.constructor.name}':`,
|
|
128
|
+
error
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (onceHandlers && onceHandlers.size > 0) {
|
|
134
|
+
onceHandlers.forEach((subscriber) => {
|
|
135
|
+
try {
|
|
136
|
+
subscriber.handleBusEvent(eventType, data, subject, emitter);
|
|
137
|
+
successCount++;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
errorCount++;
|
|
140
|
+
console.error(
|
|
141
|
+
`[EventBus] Error in once handler for '${eventType}':`,
|
|
142
|
+
error
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
this.onceHandlers.delete(eventType);
|
|
147
|
+
}
|
|
148
|
+
if (this.debugMode) {
|
|
149
|
+
console.log(
|
|
150
|
+
`[EventBus] Event '${eventType}' completed: ${successCount} success, ${errorCount} errors`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get all event types that have subscribers
|
|
156
|
+
*/
|
|
157
|
+
getEventTypes() {
|
|
158
|
+
return Array.from(this.subscribers.keys());
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get subscriber count for an event type
|
|
162
|
+
*/
|
|
163
|
+
getSubscriberCount(eventType) {
|
|
164
|
+
return this.subscribers.get(eventType)?.size || 0;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Clear all subscriptions (useful for cleanup)
|
|
168
|
+
*/
|
|
169
|
+
clear() {
|
|
170
|
+
this.subscribers.clear();
|
|
171
|
+
this.functionSubscribers.clear();
|
|
172
|
+
this.onceHandlers.clear();
|
|
173
|
+
if (this.debugMode) {
|
|
174
|
+
console.log("[EventBus] All subscriptions cleared");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Enable or disable debug logging
|
|
179
|
+
*/
|
|
180
|
+
setDebugMode(enabled) {
|
|
181
|
+
this.debugMode = enabled;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var ComponentEventTypes = {
|
|
185
|
+
COMPONENT_INITIALIZED: "component-initialized",
|
|
186
|
+
COMPONENT_HYDRATED: "component-hydrated",
|
|
187
|
+
COMPONENT_ERROR: "component-error"
|
|
188
|
+
};
|
|
189
|
+
var LifecycleEventTypes = {
|
|
190
|
+
LOCAL_INIT_STARTED: "lifecycle-local-init-started",
|
|
191
|
+
LOCAL_INIT_FINISHED: "lifecycle-local-init-finished",
|
|
192
|
+
DEPENDENCIES_INJECTED: "lifecycle-dependencies-injected",
|
|
193
|
+
ACTIVATION_STARTED: "lifecycle-activation-started",
|
|
194
|
+
ACTIVATION_FINISHED: "lifecycle-activation-finished",
|
|
195
|
+
DEACTIVATION_STARTED: "lifecycle-deactivation-started",
|
|
196
|
+
DEACTIVATION_FINISHED: "lifecycle-deactivation-finished"
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/Component.ts
|
|
200
|
+
var BaseComponent = class {
|
|
201
|
+
constructor(componentId, rootElement, eventBus = null, debugMode = false) {
|
|
202
|
+
this.componentId = componentId;
|
|
203
|
+
this.rootElement = rootElement;
|
|
204
|
+
this.debugMode = debugMode;
|
|
205
|
+
this._eventBus = eventBus || new EventBus();
|
|
206
|
+
this.rootElement.setAttribute("data-component", this.componentId);
|
|
207
|
+
}
|
|
208
|
+
get eventBus() {
|
|
209
|
+
return this._eventBus;
|
|
210
|
+
}
|
|
211
|
+
contentUpdated(newHTML) {
|
|
212
|
+
this.log("Content updated, re-binding to DOM");
|
|
213
|
+
this.rootElement.innerHTML = newHTML;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Subscribe to an event using the new EventSubscriber pattern
|
|
217
|
+
*/
|
|
218
|
+
addSubscription(eventType, target = null) {
|
|
219
|
+
this.eventBus.addSubscription(eventType, target, this);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Unsubscribe from an event using the new EventSubscriber pattern
|
|
223
|
+
*/
|
|
224
|
+
removeSubscription(eventType, target = null) {
|
|
225
|
+
this.eventBus.removeSubscription(eventType, target, this);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Emit an event from this component
|
|
229
|
+
*/
|
|
230
|
+
emit(eventType, data, target, emitter = null) {
|
|
231
|
+
this.eventBus.emit(eventType, data, target, emitter || this);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Default implementation of EventSubscriber interface
|
|
235
|
+
* Components can override this to handle events
|
|
236
|
+
*/
|
|
237
|
+
handleBusEvent(eventType, data, target, emitter) {
|
|
238
|
+
if (this.debugMode) {
|
|
239
|
+
console.log(`[${this.componentId}] Received unhandled event: ${eventType}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Find elements within this component's root element only
|
|
244
|
+
* Enforces separation of concerns - no cross-component DOM access
|
|
245
|
+
*/
|
|
246
|
+
findElement(selector) {
|
|
247
|
+
return this.rootElement.querySelector(selector);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Find multiple elements within this component's root element only
|
|
251
|
+
*/
|
|
252
|
+
findElements(selector) {
|
|
253
|
+
return Array.from(this.rootElement.querySelectorAll(selector));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Log messages with component identification
|
|
257
|
+
*/
|
|
258
|
+
log(message, data = null) {
|
|
259
|
+
if (this.debugMode) {
|
|
260
|
+
console.log(`[${this.componentId}] ${message}`, data);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Default lifecycle method: discover and return child components
|
|
265
|
+
* Override this if your component creates child components that need lifecycle management
|
|
266
|
+
*/
|
|
267
|
+
performLocalInit() {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Default lifecycle method: inject dependencies
|
|
272
|
+
* Override this if your component needs dependencies from other components
|
|
273
|
+
*/
|
|
274
|
+
setupDependencies() {
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Default lifecycle method: activate component for coordination
|
|
278
|
+
* Override this if your component needs to coordinate with other components after initialization
|
|
279
|
+
*/
|
|
280
|
+
activate() {
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Default lifecycle method: deactivate component
|
|
284
|
+
* Override this if your component needs cleanup during lifecycle management
|
|
285
|
+
*/
|
|
286
|
+
deactivate() {
|
|
287
|
+
this.rootElement?.removeAttribute("data-component");
|
|
288
|
+
this.log("Component deactivated successfully");
|
|
289
|
+
}
|
|
290
|
+
set innerHTML(innerHTML) {
|
|
291
|
+
const [newHTML, allow] = this.shouldUpdateHtml(innerHTML);
|
|
292
|
+
if (allow) {
|
|
293
|
+
this.rootElement.innerHTML = newHTML;
|
|
294
|
+
this.htmlUpdated(newHTML);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Called BEFORE the HTML is about to be updated. This is an opportunity
|
|
298
|
+
// for subclasses to fix/tweak html being updated and also ignore an update
|
|
299
|
+
// just in case. Default behavior is to return the html as is
|
|
300
|
+
shouldUpdateHtml(html) {
|
|
301
|
+
return [html, true];
|
|
302
|
+
}
|
|
303
|
+
// Called after the HTML for the component has been updated.
|
|
304
|
+
// This is an opportunity to rebind any event listeners etc
|
|
305
|
+
htmlUpdated(html) {
|
|
306
|
+
}
|
|
307
|
+
// LCMComponent implementation with default empty methods
|
|
308
|
+
// Components can override these when they need coordination with other components
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// src/LifecycleController.ts
|
|
312
|
+
var LifecycleController = class {
|
|
313
|
+
constructor(eventBus, config = {}) {
|
|
314
|
+
this.eventBus = eventBus;
|
|
315
|
+
this.config = {
|
|
316
|
+
phaseTimeoutMs: config.phaseTimeoutMs ?? 1e4,
|
|
317
|
+
continueOnError: config.continueOnError ?? false,
|
|
318
|
+
validateDependencies: false,
|
|
319
|
+
enableDebugLogging: config.enableDebugLogging ?? false
|
|
320
|
+
};
|
|
321
|
+
this.allComponents = [];
|
|
322
|
+
this.componentsByLevel = [];
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Initialize component tree starting from root component
|
|
326
|
+
*
|
|
327
|
+
* @param rootComponent The root component to start initialization from
|
|
328
|
+
* @param rootName Optional name for the root component (for debugging)
|
|
329
|
+
* @returns Promise that resolves when all components are fully initialized
|
|
330
|
+
*/
|
|
331
|
+
async initializeFromRoot(rootComponent) {
|
|
332
|
+
await this.performLocalInit(rootComponent);
|
|
333
|
+
await this.injectDependencies();
|
|
334
|
+
await this.activate();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Phase 0: Discover all components via breadth-first traversal
|
|
338
|
+
*/
|
|
339
|
+
async performLocalInit(rootComponent) {
|
|
340
|
+
const visited = /* @__PURE__ */ new Set();
|
|
341
|
+
let queue = [rootComponent];
|
|
342
|
+
this.allComponents = [rootComponent];
|
|
343
|
+
if (this.componentsByLevel.length > 0) {
|
|
344
|
+
throw new Error("Already done");
|
|
345
|
+
}
|
|
346
|
+
for (let level = 0; queue.length > 0; level++) {
|
|
347
|
+
const newqueue = [];
|
|
348
|
+
const promises = new Array();
|
|
349
|
+
this.componentsByLevel.push(queue);
|
|
350
|
+
for (let i = 0; i < queue.length; i++) {
|
|
351
|
+
const component = queue[i];
|
|
352
|
+
if (visited.has(component)) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Component hierarchy integrity violation: Component '${component.constructor.name}' discovered as child of multiple parents at level ${level}. Component hierarchies must form a tree structure where each component has exactly one parent.`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
this.emitEvent(LifecycleEventTypes.LOCAL_INIT_STARTED, component, level);
|
|
358
|
+
const result = component.performLocalInit();
|
|
359
|
+
promises.push(Promise.resolve(result).then((children) => {
|
|
360
|
+
for (const ch of children) {
|
|
361
|
+
this.allComponents.push(ch);
|
|
362
|
+
newqueue.push(ch);
|
|
363
|
+
}
|
|
364
|
+
return children;
|
|
365
|
+
}));
|
|
366
|
+
await Promise.all(promises);
|
|
367
|
+
this.emitEvent(LifecycleEventTypes.LOCAL_INIT_FINISHED, component, level);
|
|
368
|
+
}
|
|
369
|
+
queue = newqueue;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Phase 1: Dependency injection.
|
|
374
|
+
* Here all our components are already discovered so we can call directly
|
|
375
|
+
*/
|
|
376
|
+
injectDependencies() {
|
|
377
|
+
for (const comp of this.allComponents) {
|
|
378
|
+
comp.setupDependencies();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Phase 2: Activate all components
|
|
383
|
+
*
|
|
384
|
+
* Here all our components are already discovered and their dependencies setup
|
|
385
|
+
* so we can call directly
|
|
386
|
+
*/
|
|
387
|
+
async activate() {
|
|
388
|
+
const visited = /* @__PURE__ */ new Set();
|
|
389
|
+
for (let level = 0; level < this.componentsByLevel.length; level++) {
|
|
390
|
+
const promises = new Array();
|
|
391
|
+
const levelComps = this.componentsByLevel[level];
|
|
392
|
+
for (const comp of levelComps) {
|
|
393
|
+
if (visited.has(comp)) {
|
|
394
|
+
throw new Error("Comp already visited");
|
|
395
|
+
}
|
|
396
|
+
visited.add(comp);
|
|
397
|
+
this.emitEvent(LifecycleEventTypes.ACTIVATION_STARTED, comp, 0);
|
|
398
|
+
const result = comp.activate();
|
|
399
|
+
promises.push(Promise.resolve(result).then(() => {
|
|
400
|
+
this.emitEvent(LifecycleEventTypes.ACTIVATION_FINISHED, comp, 0);
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
await Promise.all(promises);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Deactivate all components in reverse order. This is usually called when a page quits or a component is
|
|
408
|
+
* deactivated
|
|
409
|
+
*/
|
|
410
|
+
async deactivateAll() {
|
|
411
|
+
for (let level = this.componentsByLevel.length - 1; level >= 0; level--) {
|
|
412
|
+
const promises = new Array();
|
|
413
|
+
const levelComps = this.componentsByLevel[level];
|
|
414
|
+
for (const comp of levelComps) {
|
|
415
|
+
this.emitEvent(LifecycleEventTypes.DEACTIVATION_STARTED, comp, level);
|
|
416
|
+
const result = comp.deactivate();
|
|
417
|
+
promises.push(Promise.resolve(result).then(() => {
|
|
418
|
+
this.emitEvent(LifecycleEventTypes.DEACTIVATION_FINISHED, comp, level);
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
await Promise.all(promises);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Emit lifecycle event to registered callbacks
|
|
426
|
+
*/
|
|
427
|
+
emitEvent(event, component, level, error = null) {
|
|
428
|
+
this.eventBus.emit(
|
|
429
|
+
event,
|
|
430
|
+
{ error, success: error == null },
|
|
431
|
+
this,
|
|
432
|
+
component
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Log message if debug logging is enabled
|
|
437
|
+
*/
|
|
438
|
+
log(message, ...args) {
|
|
439
|
+
if (this.config.enableDebugLogging) {
|
|
440
|
+
console.log(`[LifecycleController] ${message}`, ...args);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Log error message
|
|
445
|
+
*/
|
|
446
|
+
logError(message, error) {
|
|
447
|
+
console.error(`[LifecycleController] ${message}`, error);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get current status of all components
|
|
451
|
+
*/
|
|
452
|
+
/*
|
|
453
|
+
public getComponentStatus(): Array<{ name: string; phase: ComponentPhase }> {
|
|
454
|
+
return Array.from(this.allComponents).map(component => ({
|
|
455
|
+
name: this.getComponentName(component),
|
|
456
|
+
phase: this.componentPhases.get(component) || 'created'
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
*/
|
|
460
|
+
};
|
|
461
|
+
LifecycleController.DefaultConfig = {
|
|
462
|
+
enableDebugLogging: true,
|
|
463
|
+
phaseTimeoutMs: 15e3,
|
|
464
|
+
// Increased timeout for component loading
|
|
465
|
+
continueOnError: false
|
|
466
|
+
// Fail fast for debugging
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/ThemeManager.ts
|
|
470
|
+
var _ThemeManager = class _ThemeManager {
|
|
471
|
+
// Computer icon
|
|
472
|
+
/**
|
|
473
|
+
* Initialize theme based on saved preference or system default
|
|
474
|
+
*/
|
|
475
|
+
static initialize() {
|
|
476
|
+
const savedTheme = localStorage.getItem("theme");
|
|
477
|
+
if (savedTheme === _ThemeManager.DARK || !savedTheme && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
478
|
+
document.documentElement.classList.add("dark");
|
|
479
|
+
} else {
|
|
480
|
+
document.documentElement.classList.remove("dark");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Set theme and save preference
|
|
485
|
+
*/
|
|
486
|
+
static setTheme(theme) {
|
|
487
|
+
if (theme === _ThemeManager.SYSTEM) {
|
|
488
|
+
localStorage.removeItem("theme");
|
|
489
|
+
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
490
|
+
document.documentElement.classList.add("dark");
|
|
491
|
+
} else {
|
|
492
|
+
document.documentElement.classList.remove("dark");
|
|
493
|
+
}
|
|
494
|
+
} else if (theme === _ThemeManager.DARK) {
|
|
495
|
+
localStorage.setItem("theme", _ThemeManager.DARK);
|
|
496
|
+
document.documentElement.classList.add("dark");
|
|
497
|
+
} else {
|
|
498
|
+
localStorage.setItem("theme", _ThemeManager.LIGHT);
|
|
499
|
+
document.documentElement.classList.remove("dark");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get current theme setting (light, dark, or system)
|
|
504
|
+
*/
|
|
505
|
+
static getCurrentThemeSetting() {
|
|
506
|
+
return localStorage.getItem("theme") || _ThemeManager.SYSTEM;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Gets the *next* theme in the cycle: Light -> Dark -> System -> Light ...
|
|
510
|
+
*/
|
|
511
|
+
static getNextTheme(currentSetting) {
|
|
512
|
+
if (currentSetting === _ThemeManager.LIGHT) {
|
|
513
|
+
return _ThemeManager.DARK;
|
|
514
|
+
} else if (currentSetting === _ThemeManager.DARK) {
|
|
515
|
+
return _ThemeManager.SYSTEM;
|
|
516
|
+
} else {
|
|
517
|
+
return _ThemeManager.LIGHT;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Gets the appropriate SVG icon string for a given theme setting.
|
|
522
|
+
*/
|
|
523
|
+
static getIconSVG(themeSetting) {
|
|
524
|
+
switch (themeSetting) {
|
|
525
|
+
case _ThemeManager.LIGHT:
|
|
526
|
+
return _ThemeManager.LIGHT_ICON_SVG;
|
|
527
|
+
case _ThemeManager.DARK:
|
|
528
|
+
return _ThemeManager.DARK_ICON_SVG;
|
|
529
|
+
case _ThemeManager.SYSTEM:
|
|
530
|
+
default:
|
|
531
|
+
return _ThemeManager.SYSTEM_ICON_SVG;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Gets a user-friendly label for the theme setting.
|
|
536
|
+
*/
|
|
537
|
+
static getThemeLabel(themeSetting) {
|
|
538
|
+
switch (themeSetting) {
|
|
539
|
+
case _ThemeManager.LIGHT:
|
|
540
|
+
return "Light Mode";
|
|
541
|
+
case _ThemeManager.DARK:
|
|
542
|
+
return "Dark Mode";
|
|
543
|
+
case _ThemeManager.SYSTEM:
|
|
544
|
+
default:
|
|
545
|
+
return "System Default";
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Initialize the ThemeManager (no instance needed for static methods)
|
|
550
|
+
*/
|
|
551
|
+
static init() {
|
|
552
|
+
_ThemeManager.initialize();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
// Theme options
|
|
556
|
+
_ThemeManager.LIGHT = "light";
|
|
557
|
+
_ThemeManager.DARK = "dark";
|
|
558
|
+
_ThemeManager.SYSTEM = "system";
|
|
559
|
+
// Icons for each theme state
|
|
560
|
+
_ThemeManager.LIGHT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /></svg>`;
|
|
561
|
+
// Sun icon
|
|
562
|
+
_ThemeManager.DARK_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /></svg>`;
|
|
563
|
+
// Moon icon
|
|
564
|
+
_ThemeManager.SYSTEM_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" /></svg>`;
|
|
565
|
+
var ThemeManager = _ThemeManager;
|
|
566
|
+
|
|
567
|
+
// src/TemplateLoader.ts
|
|
568
|
+
var TemplateLoader = class {
|
|
569
|
+
constructor(registryName = "template-registry") {
|
|
570
|
+
this.registryName = registryName;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Finds the specified template wrapper element in the registry.
|
|
574
|
+
* @param templateId The data-template-id of the wrapper element.
|
|
575
|
+
* @returns The wrapper HTMLElement or null if not found.
|
|
576
|
+
*/
|
|
577
|
+
_findTemplateWrapper(templateId) {
|
|
578
|
+
const templateRegistry = document.getElementById(this.registryName);
|
|
579
|
+
if (!templateRegistry) {
|
|
580
|
+
console.error(`Template registry '#${this.registryName}' not found!`);
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
const templateWrapper = templateRegistry.querySelector(`[data-template-id="${templateId}"]`);
|
|
584
|
+
if (!templateWrapper) {
|
|
585
|
+
console.error(`Template with ID "${templateId}" not found in registry '#${this.registryName}'.`);
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
return templateWrapper;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Loads and returns the inner HTML of a template definition.
|
|
592
|
+
* @param templateId The data-template-id of the wrapper element.
|
|
593
|
+
* @returns The innerHTML content as string if it exists otherwise null.
|
|
594
|
+
*/
|
|
595
|
+
loadHtml(templateId) {
|
|
596
|
+
const templateWrapper = this._findTemplateWrapper(templateId);
|
|
597
|
+
if (!templateWrapper) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
return templateWrapper.innerHTML;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Loads and clones the child elements of a template definition.
|
|
604
|
+
* @param templateId The data-template-id of the wrapper element.
|
|
605
|
+
* @returns An array of cloned HTMLElement children, or an empty array if not found or has no children.
|
|
606
|
+
*/
|
|
607
|
+
load(templateId) {
|
|
608
|
+
const templateWrapper = this._findTemplateWrapper(templateId);
|
|
609
|
+
if (!templateWrapper) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const templateRootElement = templateWrapper.cloneNode(true);
|
|
613
|
+
if (!templateRootElement) {
|
|
614
|
+
console.error(`Template content is empty for: ${templateId}`);
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
return Array.from(templateRootElement.children);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Loads a template's content, clears the target element, and appends the cloned content into it.
|
|
621
|
+
* @param templateId The data-template-id of the wrapper element to load.
|
|
622
|
+
* @param targetElement The HTMLElement where the cloned content should be placed.
|
|
623
|
+
* @returns True if the operation was successful (template found and content appended, even if content was empty), false otherwise.
|
|
624
|
+
*/
|
|
625
|
+
loadInto(templateId, targetElement) {
|
|
626
|
+
if (!targetElement) {
|
|
627
|
+
console.error(`Cannot load template "${templateId}": Target element is null.`);
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
const templateWrapper = this._findTemplateWrapper(templateId);
|
|
631
|
+
if (!templateWrapper) {
|
|
632
|
+
targetElement.innerHTML = `<div class="p-4 text-red-500">Error loading template '${templateId}' (Not Found)</div>`;
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
targetElement.innerHTML = "";
|
|
636
|
+
const childElements = Array.from(templateWrapper.children);
|
|
637
|
+
if (childElements.length === 0) {
|
|
638
|
+
console.warn(`Template "${templateId}" has no child elements to load.`);
|
|
639
|
+
} else {
|
|
640
|
+
childElements.forEach((child) => {
|
|
641
|
+
targetElement.appendChild(child.cloneNode(true));
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// src/Modal.ts
|
|
649
|
+
var _Modal = class _Modal {
|
|
650
|
+
// Store the Apply callback
|
|
651
|
+
/**
|
|
652
|
+
* Private constructor for singleton pattern
|
|
653
|
+
*/
|
|
654
|
+
constructor() {
|
|
655
|
+
// Current modal data - including optional callbacks
|
|
656
|
+
this.currentTemplateId = null;
|
|
657
|
+
this.currentData = null;
|
|
658
|
+
this.onSubmitCallback = null;
|
|
659
|
+
// Store the callback
|
|
660
|
+
this.onApplyCallback = null;
|
|
661
|
+
this.modalContainer = document.getElementById("modal-container");
|
|
662
|
+
this.modalBackdrop = document.getElementById("modal-backdrop");
|
|
663
|
+
this.modalPanel = document.getElementById("modal-panel");
|
|
664
|
+
this.modalContent = document.getElementById("modal-content");
|
|
665
|
+
this.closeButton = document.getElementById("modal-close");
|
|
666
|
+
this.templateLoader = new TemplateLoader();
|
|
667
|
+
this.bindEvents();
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Get the Modal instance (singleton)
|
|
671
|
+
*/
|
|
672
|
+
static getInstance() {
|
|
673
|
+
if (!_Modal.instance) {
|
|
674
|
+
_Modal.instance = new _Modal();
|
|
675
|
+
}
|
|
676
|
+
return _Modal.instance;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Bind event listeners for modal interactions
|
|
680
|
+
*/
|
|
681
|
+
bindEvents() {
|
|
682
|
+
if (this.closeButton) {
|
|
683
|
+
this.closeButton.addEventListener("click", () => this.hide());
|
|
684
|
+
}
|
|
685
|
+
if (this.modalBackdrop) {
|
|
686
|
+
this.modalBackdrop.addEventListener("click", (e) => {
|
|
687
|
+
if (e.target === this.modalBackdrop) {
|
|
688
|
+
this.hide();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
document.addEventListener("keydown", (e) => {
|
|
693
|
+
if (e.key === "Escape" && this.isVisible()) {
|
|
694
|
+
this.hide();
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
if (this.modalPanel) {
|
|
698
|
+
this.modalPanel.addEventListener("click", (e) => {
|
|
699
|
+
const target = e.target;
|
|
700
|
+
const closeButton = target.closest('button[id$="-cancel"], button[id$="-close"]');
|
|
701
|
+
if (closeButton) {
|
|
702
|
+
this.hide();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const actionButton = target.closest("button[data-modal-action]");
|
|
706
|
+
if (actionButton) {
|
|
707
|
+
const action = actionButton.getAttribute("data-modal-action");
|
|
708
|
+
if (action === "submit" && this.onSubmitCallback) {
|
|
709
|
+
this.onSubmitCallback(this.currentData);
|
|
710
|
+
} else if (action === "apply" && this.onApplyCallback) {
|
|
711
|
+
this.onApplyCallback(this.currentData);
|
|
712
|
+
this.hide();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Check if the modal is currently visible
|
|
720
|
+
*/
|
|
721
|
+
isVisible() {
|
|
722
|
+
return this.modalContainer ? !this.modalContainer.classList.contains("hidden") : false;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Show a modal with content from the specified template ID.
|
|
726
|
+
* Uses TemplateLoader to get the content element.
|
|
727
|
+
* @param templateId ID used in `data-template-id` attribute in TemplateRegistry.html
|
|
728
|
+
* @param data Optional data to pass to the modal. Can include callbacks like `onSubmit`.
|
|
729
|
+
* @returns The root HTMLElement of the loaded content, or null if failed.
|
|
730
|
+
*/
|
|
731
|
+
show(templateId, data = null) {
|
|
732
|
+
if (!this.modalContainer || !this.modalContent) {
|
|
733
|
+
console.error("Modal container or content area not found.");
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const success = this.templateLoader.loadInto(templateId, this.modalContent);
|
|
737
|
+
if (!success) {
|
|
738
|
+
this.modalContainer.classList.remove("hidden");
|
|
739
|
+
setTimeout(() => this.modalContainer.classList.add("modal-active"), 10);
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
this.currentTemplateId = templateId;
|
|
743
|
+
this.currentData = data || {};
|
|
744
|
+
this.onSubmitCallback = data?.onSubmit || null;
|
|
745
|
+
this.onApplyCallback = data?.onApply || null;
|
|
746
|
+
if (data) {
|
|
747
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
748
|
+
if (key !== "onSubmit" && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) {
|
|
749
|
+
if (this.modalContent) this.modalContent.dataset[key] = String(value);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
this.modalContainer.classList.remove("hidden");
|
|
754
|
+
setTimeout(() => {
|
|
755
|
+
this.modalContainer.classList.add("modal-active");
|
|
756
|
+
}, 10);
|
|
757
|
+
const firstElement = this.modalContent;
|
|
758
|
+
return firstElement;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Hide the modal
|
|
762
|
+
*/
|
|
763
|
+
hide() {
|
|
764
|
+
return new Promise((resolve) => {
|
|
765
|
+
if (!this.modalContainer) return;
|
|
766
|
+
this.modalContainer.classList.remove("modal-active");
|
|
767
|
+
setTimeout(() => {
|
|
768
|
+
this.modalContainer.classList.add("hidden");
|
|
769
|
+
this.currentTemplateId = null;
|
|
770
|
+
this.currentData = null;
|
|
771
|
+
this.onSubmitCallback = null;
|
|
772
|
+
this.onApplyCallback = null;
|
|
773
|
+
if (this.modalContent) this.modalContent.innerHTML = "";
|
|
774
|
+
resolve();
|
|
775
|
+
}, 200);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Get the current modal content element
|
|
780
|
+
*/
|
|
781
|
+
getContentElement() {
|
|
782
|
+
return this.modalContent;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Get the current template ID
|
|
786
|
+
*/
|
|
787
|
+
getCurrentTemplate() {
|
|
788
|
+
return this.currentTemplateId;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Get the current modal data
|
|
792
|
+
*/
|
|
793
|
+
getCurrentData() {
|
|
794
|
+
return this.currentData;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Update modal data (excluding callbacks for now)
|
|
798
|
+
*/
|
|
799
|
+
updateData(newData) {
|
|
800
|
+
this.currentData = { ...this.currentData, ...newData };
|
|
801
|
+
if (this.modalContent && newData) {
|
|
802
|
+
Object.entries(newData).forEach(([key, value]) => {
|
|
803
|
+
if (key !== "onSubmit" && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) {
|
|
804
|
+
if (this.modalContent) this.modalContent.dataset[key] = String(value);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Initialize the modal component
|
|
811
|
+
*/
|
|
812
|
+
static init() {
|
|
813
|
+
return _Modal.getInstance();
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
_Modal.instance = null;
|
|
817
|
+
var Modal = _Modal;
|
|
818
|
+
|
|
819
|
+
// src/ToastManager.ts
|
|
820
|
+
var _ToastManager = class _ToastManager {
|
|
821
|
+
/**
|
|
822
|
+
* Private constructor for singleton pattern
|
|
823
|
+
*/
|
|
824
|
+
constructor() {
|
|
825
|
+
this.toasts = /* @__PURE__ */ new Map();
|
|
826
|
+
this.counter = 0;
|
|
827
|
+
this.container = document.getElementById("toast-container");
|
|
828
|
+
this.template = document.getElementById("toast-template");
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Get the ToastManager instance (singleton)
|
|
832
|
+
*/
|
|
833
|
+
static getInstance() {
|
|
834
|
+
if (!_ToastManager.instance) {
|
|
835
|
+
_ToastManager.instance = new _ToastManager();
|
|
836
|
+
}
|
|
837
|
+
return _ToastManager.instance;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Show a toast notification
|
|
841
|
+
* @param title Toast title
|
|
842
|
+
* @param message Toast message
|
|
843
|
+
* @param type Toast type for styling
|
|
844
|
+
* @param duration Duration in ms (default: 4000)
|
|
845
|
+
*/
|
|
846
|
+
showToast(title, message, type = "info", duration = 4e3) {
|
|
847
|
+
if (!this.container || !this.template) return "";
|
|
848
|
+
const id = `toast-${Date.now()}-${this.counter++}`;
|
|
849
|
+
const toast = this.template.cloneNode(true);
|
|
850
|
+
toast.id = id;
|
|
851
|
+
toast.classList.remove("hidden");
|
|
852
|
+
const titleElement = toast.querySelector(".toast-title");
|
|
853
|
+
const messageElement = toast.querySelector(".toast-message");
|
|
854
|
+
if (titleElement) titleElement.textContent = title;
|
|
855
|
+
if (messageElement) messageElement.textContent = message;
|
|
856
|
+
const iconContainer = toast.querySelector(".flex-shrink-0");
|
|
857
|
+
if (iconContainer) {
|
|
858
|
+
iconContainer.innerHTML = "";
|
|
859
|
+
let icon;
|
|
860
|
+
let borderColor;
|
|
861
|
+
switch (type) {
|
|
862
|
+
case "success":
|
|
863
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>';
|
|
864
|
+
borderColor = "border-green-500";
|
|
865
|
+
break;
|
|
866
|
+
case "error":
|
|
867
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /></svg>';
|
|
868
|
+
borderColor = "border-red-500";
|
|
869
|
+
break;
|
|
870
|
+
case "warning":
|
|
871
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-yellow-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
|
|
872
|
+
borderColor = "border-yellow-500";
|
|
873
|
+
break;
|
|
874
|
+
case "info":
|
|
875
|
+
default:
|
|
876
|
+
icon = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg>';
|
|
877
|
+
borderColor = "border-blue-500";
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
iconContainer.innerHTML = icon;
|
|
881
|
+
const borderElement = toast.querySelector(".border-l-4");
|
|
882
|
+
if (borderElement) {
|
|
883
|
+
borderElement.className = borderElement.className.replace(/border-[a-z]+-500/g, borderColor);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const closeButton = toast.querySelector(".toast-close");
|
|
887
|
+
if (closeButton) {
|
|
888
|
+
closeButton.addEventListener("click", () => {
|
|
889
|
+
this.hideToast(id);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
this.container.appendChild(toast);
|
|
893
|
+
this.toasts.set(id, toast);
|
|
894
|
+
setTimeout(() => {
|
|
895
|
+
toast.classList.remove("scale-95", "opacity-0");
|
|
896
|
+
toast.classList.add("scale-100", "opacity-100");
|
|
897
|
+
}, 10);
|
|
898
|
+
if (duration > 0) {
|
|
899
|
+
setTimeout(() => {
|
|
900
|
+
this.hideToast(id);
|
|
901
|
+
}, duration);
|
|
902
|
+
}
|
|
903
|
+
return id;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Hide a toast notification
|
|
907
|
+
* @param id Toast ID
|
|
908
|
+
*/
|
|
909
|
+
hideToast(id) {
|
|
910
|
+
const toast = this.toasts.get(id);
|
|
911
|
+
if (!toast) return;
|
|
912
|
+
toast.classList.remove("scale-100", "opacity-100");
|
|
913
|
+
toast.classList.add("scale-95", "opacity-0");
|
|
914
|
+
setTimeout(() => {
|
|
915
|
+
toast.remove();
|
|
916
|
+
this.toasts.delete(id);
|
|
917
|
+
}, 300);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Hide all toast notifications
|
|
921
|
+
*/
|
|
922
|
+
hideAllToasts() {
|
|
923
|
+
this.toasts.forEach((_, id) => {
|
|
924
|
+
this.hideToast(id);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Initialize the component
|
|
929
|
+
*/
|
|
930
|
+
static init() {
|
|
931
|
+
return _ToastManager.getInstance();
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
_ToastManager.instance = null;
|
|
935
|
+
var ToastManager = _ToastManager;
|
|
936
|
+
|
|
937
|
+
// src/SplashScreen.ts
|
|
938
|
+
var SplashScreen = class {
|
|
939
|
+
/**
|
|
940
|
+
* Dismiss the splash screen with a fade-out animation
|
|
941
|
+
* Safe to call multiple times - only dismisses once
|
|
942
|
+
*/
|
|
943
|
+
static dismiss() {
|
|
944
|
+
if (this.dismissed) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const splash = document.getElementById(this.SPLASH_ID);
|
|
948
|
+
if (!splash) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
this.dismissed = true;
|
|
952
|
+
splash.style.opacity = "0";
|
|
953
|
+
setTimeout(() => {
|
|
954
|
+
splash.remove();
|
|
955
|
+
}, 300);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Update the splash screen message (if it hasn't been dismissed yet)
|
|
959
|
+
*/
|
|
960
|
+
static updateMessage(title, message) {
|
|
961
|
+
if (this.dismissed) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const splash = document.getElementById(this.SPLASH_ID);
|
|
965
|
+
if (!splash) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (title) {
|
|
969
|
+
const titleEl = splash.querySelector("[data-splash-title]");
|
|
970
|
+
if (titleEl) {
|
|
971
|
+
titleEl.textContent = title;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (message) {
|
|
975
|
+
const messageEl = splash.querySelector("[data-splash-message]");
|
|
976
|
+
if (messageEl) {
|
|
977
|
+
messageEl.textContent = message;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Update the splash screen progress bar
|
|
983
|
+
* @param percent - Progress percentage (0-100)
|
|
984
|
+
*/
|
|
985
|
+
static updateProgress(percent) {
|
|
986
|
+
if (this.dismissed) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const splash = document.getElementById(this.SPLASH_ID);
|
|
990
|
+
if (!splash) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
994
|
+
const progressBar = splash.querySelector("[data-splash-progress-bar]");
|
|
995
|
+
if (progressBar) {
|
|
996
|
+
progressBar.style.width = `${clampedPercent}%`;
|
|
997
|
+
}
|
|
998
|
+
const progressText = splash.querySelector("[data-splash-progress-text]");
|
|
999
|
+
if (progressText) {
|
|
1000
|
+
progressText.textContent = `${Math.round(clampedPercent)}%`;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Update both message and progress at once
|
|
1005
|
+
*/
|
|
1006
|
+
static update(options) {
|
|
1007
|
+
if (options.title !== void 0 || options.message !== void 0) {
|
|
1008
|
+
this.updateMessage(options.title, options.message);
|
|
1009
|
+
}
|
|
1010
|
+
if (options.progress !== void 0) {
|
|
1011
|
+
this.updateProgress(options.progress);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Check if splash screen is still visible
|
|
1016
|
+
*/
|
|
1017
|
+
static isVisible() {
|
|
1018
|
+
return !this.dismissed && !!document.getElementById(this.SPLASH_ID);
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
SplashScreen.SPLASH_ID = "splash-screen";
|
|
1022
|
+
SplashScreen.dismissed = false;
|
|
1023
|
+
|
|
1024
|
+
// src/BasePage.ts
|
|
1025
|
+
var BasePage = class extends BaseComponent {
|
|
1026
|
+
// Constructor now just uses document as the rootElement
|
|
1027
|
+
constructor(componentId, eventBus = null, debugMode = false) {
|
|
1028
|
+
super(componentId, document.body, eventBus, debugMode);
|
|
1029
|
+
this.componentId = componentId;
|
|
1030
|
+
this.debugMode = debugMode;
|
|
1031
|
+
this.initializeBaseComponents();
|
|
1032
|
+
this.bindBaseEvents();
|
|
1033
|
+
}
|
|
1034
|
+
// LCMComponent Phase 1: Initialize page structure and discover child components
|
|
1035
|
+
performLocalInit() {
|
|
1036
|
+
this.log("BasePage: Starting local initialization");
|
|
1037
|
+
return this.initializeSpecificComponents();
|
|
1038
|
+
}
|
|
1039
|
+
// LCMComponent Phase 3: Activate the page (bind events after all components are ready)
|
|
1040
|
+
activate() {
|
|
1041
|
+
this.log("BasePage: Activating page");
|
|
1042
|
+
this.bindSpecificEvents();
|
|
1043
|
+
this.log("BasePage: Page activation complete");
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Initialize common UI components that all pages need
|
|
1047
|
+
*/
|
|
1048
|
+
initializeBaseComponents() {
|
|
1049
|
+
ThemeManager.init();
|
|
1050
|
+
this.modal = Modal.init();
|
|
1051
|
+
this.toastManager = ToastManager.init();
|
|
1052
|
+
this.themeToggleButton = document.getElementById("theme-toggle-button");
|
|
1053
|
+
this.themeToggleIcon = document.getElementById("theme-toggle-icon");
|
|
1054
|
+
if (!this.themeToggleButton || !this.themeToggleIcon) {
|
|
1055
|
+
console.warn("Theme toggle button or icon element not found in Header.");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Bind common event handlers that all pages need
|
|
1060
|
+
*/
|
|
1061
|
+
bindBaseEvents() {
|
|
1062
|
+
if (this.themeToggleButton) {
|
|
1063
|
+
this.themeToggleButton.addEventListener("click", this.handleThemeToggleClick.bind(this));
|
|
1064
|
+
}
|
|
1065
|
+
this.updateThemeButtonState();
|
|
1066
|
+
this.initializeHeaderActionsDropdown();
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Handle theme toggle button clicks
|
|
1070
|
+
*/
|
|
1071
|
+
handleThemeToggleClick() {
|
|
1072
|
+
const currentSetting = ThemeManager.getCurrentThemeSetting();
|
|
1073
|
+
const nextSetting = ThemeManager.getNextTheme(currentSetting);
|
|
1074
|
+
ThemeManager.setTheme(nextSetting);
|
|
1075
|
+
this.updateThemeButtonState(nextSetting);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Update the theme toggle button state and appearance
|
|
1079
|
+
*/
|
|
1080
|
+
updateThemeButtonState(currentTheme) {
|
|
1081
|
+
if (!this.themeToggleButton || !this.themeToggleIcon) return;
|
|
1082
|
+
const themeToDisplay = currentTheme || ThemeManager.getCurrentThemeSetting();
|
|
1083
|
+
const iconSVG = ThemeManager.getIconSVG(themeToDisplay);
|
|
1084
|
+
const label = `Toggle theme (currently: ${ThemeManager.getThemeLabel(themeToDisplay)})`;
|
|
1085
|
+
this.themeToggleIcon.innerHTML = iconSVG;
|
|
1086
|
+
this.themeToggleButton.setAttribute("aria-label", label);
|
|
1087
|
+
this.themeToggleButton.setAttribute("title", label);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Show a toast notification
|
|
1091
|
+
*/
|
|
1092
|
+
showToast(title, message, type = "info", duration) {
|
|
1093
|
+
this.toastManager?.showToast(title, message, type, duration);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Show a modal dialog
|
|
1097
|
+
*/
|
|
1098
|
+
showModal(templateId, data) {
|
|
1099
|
+
this.modal?.show(templateId, data);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Hide the modal dialog
|
|
1103
|
+
*/
|
|
1104
|
+
hideModal() {
|
|
1105
|
+
this.modal?.hide();
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Get the current theme setting
|
|
1109
|
+
*/
|
|
1110
|
+
getCurrentTheme() {
|
|
1111
|
+
return ThemeManager.getCurrentThemeSetting();
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Check if the current theme is dark mode
|
|
1115
|
+
*/
|
|
1116
|
+
isDarkMode() {
|
|
1117
|
+
return document.documentElement.classList.contains("dark");
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Initialize responsive header actions drawer
|
|
1121
|
+
* On desktop: drawer is always visible, positioned inline with header
|
|
1122
|
+
* On mobile: drawer slides down from top when menu button is clicked
|
|
1123
|
+
*/
|
|
1124
|
+
initializeHeaderActionsDropdown() {
|
|
1125
|
+
const menuBtn = document.getElementById("header-actions-menu-btn");
|
|
1126
|
+
const drawer = document.getElementById("header-actions-drawer");
|
|
1127
|
+
if (!menuBtn || !drawer) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const drawerBackdrop = drawer.querySelector(".header-drawer-backdrop");
|
|
1131
|
+
const openDrawer = () => {
|
|
1132
|
+
drawer.classList.remove("hidden");
|
|
1133
|
+
void drawer.offsetHeight;
|
|
1134
|
+
requestAnimationFrame(() => {
|
|
1135
|
+
drawer.classList.add("open");
|
|
1136
|
+
});
|
|
1137
|
+
};
|
|
1138
|
+
const closeDrawer = () => {
|
|
1139
|
+
drawer.classList.remove("open");
|
|
1140
|
+
setTimeout(() => {
|
|
1141
|
+
drawer.classList.add("hidden");
|
|
1142
|
+
}, 300);
|
|
1143
|
+
};
|
|
1144
|
+
menuBtn.addEventListener("click", (e) => {
|
|
1145
|
+
e.stopPropagation();
|
|
1146
|
+
if (drawer.classList.contains("open")) {
|
|
1147
|
+
closeDrawer();
|
|
1148
|
+
} else {
|
|
1149
|
+
openDrawer();
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
if (drawerBackdrop) {
|
|
1153
|
+
drawerBackdrop.addEventListener("click", closeDrawer);
|
|
1154
|
+
}
|
|
1155
|
+
document.addEventListener("keydown", (e) => {
|
|
1156
|
+
if (e.key === "Escape" && drawer.classList.contains("open")) {
|
|
1157
|
+
closeDrawer();
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Abstract method that subclasses must implement to initialize their specific components
|
|
1163
|
+
* Should return any child components that need lifecycle management
|
|
1164
|
+
*/
|
|
1165
|
+
initializeSpecificComponents() {
|
|
1166
|
+
return [];
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Abstract method that subclasses must implement to bind their specific events
|
|
1170
|
+
*/
|
|
1171
|
+
bindSpecificEvents() {
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Component-specific cleanup logic (required by BaseComponent)
|
|
1175
|
+
*/
|
|
1176
|
+
destroyComponent() {
|
|
1177
|
+
this.log("BasePage: Cleaning up base page components");
|
|
1178
|
+
this.modal = null;
|
|
1179
|
+
this.toastManager = null;
|
|
1180
|
+
this.themeToggleButton = null;
|
|
1181
|
+
this.themeToggleIcon = null;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Ensure an element exists, create if missing
|
|
1185
|
+
* This is acceptable for page-level orchestration to find component root elements
|
|
1186
|
+
*/
|
|
1187
|
+
ensureElement(selector, fallbackId) {
|
|
1188
|
+
let element = document.querySelector(selector);
|
|
1189
|
+
if (!element) {
|
|
1190
|
+
console.warn(`Element not found: ${selector}, creating fallback`);
|
|
1191
|
+
element = document.createElement("div");
|
|
1192
|
+
element.id = fallbackId;
|
|
1193
|
+
element.className = "w-full h-full";
|
|
1194
|
+
const mainContainer = document.querySelector("main") || document.body;
|
|
1195
|
+
mainContainer.appendChild(element);
|
|
1196
|
+
}
|
|
1197
|
+
if (!element.id) {
|
|
1198
|
+
element.id = fallbackId;
|
|
1199
|
+
}
|
|
1200
|
+
return element;
|
|
1201
|
+
}
|
|
1202
|
+
dismissSplashScreen() {
|
|
1203
|
+
SplashScreen.dismiss();
|
|
1204
|
+
}
|
|
1205
|
+
static loadAfterPageLoaded(pageName, PageClass, PageClassName) {
|
|
1206
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
1207
|
+
const page = new PageClass(PageClassName);
|
|
1208
|
+
window[pageName] = page;
|
|
1209
|
+
const lifecycleController = new LifecycleController(page.eventBus, LifecycleController.DefaultConfig);
|
|
1210
|
+
await lifecycleController.initializeFromRoot(page);
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// src/MobileBottomDrawer.ts
|
|
1216
|
+
var MobileBottomDrawer = class extends BaseComponent {
|
|
1217
|
+
/**
|
|
1218
|
+
* Create a MobileBottomDrawer
|
|
1219
|
+
* @param rootElement - The root container element for the drawer
|
|
1220
|
+
* @param eventBus - Event bus for component communication
|
|
1221
|
+
* @param debugMode - Enable debug logging
|
|
1222
|
+
*/
|
|
1223
|
+
constructor(rootElement, eventBus, debugMode = false) {
|
|
1224
|
+
super("mobile-bottom-drawer", rootElement, eventBus, debugMode);
|
|
1225
|
+
this.isOpen = false;
|
|
1226
|
+
// Swipe gesture tracking
|
|
1227
|
+
this.touchStartY = 0;
|
|
1228
|
+
this.touchCurrentY = 0;
|
|
1229
|
+
this.isDragging = false;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Phase 1: Initialize DOM and discover child components
|
|
1233
|
+
*/
|
|
1234
|
+
async performLocalInit() {
|
|
1235
|
+
this.backdropElement = this.rootElement.querySelector(".drawer-backdrop");
|
|
1236
|
+
this.drawerElement = this.rootElement.querySelector(".drawer-container");
|
|
1237
|
+
this.contentElement = this.rootElement.querySelector(".drawer-content");
|
|
1238
|
+
this.closeButton = this.rootElement.querySelector(".drawer-close-btn");
|
|
1239
|
+
if (!this.backdropElement || !this.drawerElement || !this.contentElement) {
|
|
1240
|
+
throw new Error("MobileBottomDrawer: Required drawer elements not found");
|
|
1241
|
+
}
|
|
1242
|
+
this.bindEvents();
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Bind event listeners for drawer interactions
|
|
1247
|
+
*/
|
|
1248
|
+
bindEvents() {
|
|
1249
|
+
this.backdropElement.addEventListener("click", (e) => {
|
|
1250
|
+
if (e.target === this.backdropElement) {
|
|
1251
|
+
this.close();
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
if (this.closeButton) {
|
|
1255
|
+
this.closeButton.addEventListener("click", () => {
|
|
1256
|
+
this.close();
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
document.addEventListener("keydown", (e) => {
|
|
1260
|
+
if (e.key === "Escape" && this.isOpen) {
|
|
1261
|
+
this.close();
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
this.drawerElement.addEventListener("touchstart", (e) => this.handleTouchStart(e), { passive: true });
|
|
1265
|
+
this.drawerElement.addEventListener("touchmove", (e) => this.handleTouchMove(e), { passive: false });
|
|
1266
|
+
this.drawerElement.addEventListener("touchend", (e) => this.handleTouchEnd(e), { passive: true });
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Handle touch start for swipe gesture
|
|
1270
|
+
*/
|
|
1271
|
+
handleTouchStart(e) {
|
|
1272
|
+
const contentScrollTop = this.contentElement.scrollTop;
|
|
1273
|
+
if (contentScrollTop > 0) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
this.touchStartY = e.touches[0].clientY;
|
|
1277
|
+
this.touchCurrentY = this.touchStartY;
|
|
1278
|
+
this.isDragging = true;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Handle touch move for swipe gesture
|
|
1282
|
+
*/
|
|
1283
|
+
handleTouchMove(e) {
|
|
1284
|
+
if (!this.isDragging) return;
|
|
1285
|
+
this.touchCurrentY = e.touches[0].clientY;
|
|
1286
|
+
const deltaY = this.touchCurrentY - this.touchStartY;
|
|
1287
|
+
if (deltaY > 0) {
|
|
1288
|
+
e.preventDefault();
|
|
1289
|
+
const dragAmount = Math.min(deltaY, 300);
|
|
1290
|
+
this.drawerElement.style.transform = `translateY(${dragAmount}px)`;
|
|
1291
|
+
const opacity = Math.max(0, 0.5 - dragAmount / 600);
|
|
1292
|
+
this.backdropElement.style.background = `rgba(0, 0, 0, ${opacity})`;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Handle touch end for swipe gesture
|
|
1297
|
+
*/
|
|
1298
|
+
handleTouchEnd(e) {
|
|
1299
|
+
if (!this.isDragging) return;
|
|
1300
|
+
const deltaY = this.touchCurrentY - this.touchStartY;
|
|
1301
|
+
const velocity = deltaY / 300;
|
|
1302
|
+
if (deltaY > 100 || velocity > 0.3) {
|
|
1303
|
+
this.close();
|
|
1304
|
+
} else {
|
|
1305
|
+
this.drawerElement.style.transform = "";
|
|
1306
|
+
this.backdropElement.style.background = "";
|
|
1307
|
+
}
|
|
1308
|
+
this.isDragging = false;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Open the drawer with slide-up animation
|
|
1312
|
+
*/
|
|
1313
|
+
open() {
|
|
1314
|
+
if (this.isOpen) return;
|
|
1315
|
+
this.isOpen = true;
|
|
1316
|
+
this.drawerElement.style.transform = "";
|
|
1317
|
+
this.backdropElement.style.background = "";
|
|
1318
|
+
this.rootElement.classList.add("open");
|
|
1319
|
+
this.eventBus.emit("drawer-opened", { drawerId: this.componentId }, null, this);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Close the drawer with slide-down animation
|
|
1323
|
+
*/
|
|
1324
|
+
close() {
|
|
1325
|
+
if (!this.isOpen) return;
|
|
1326
|
+
this.isOpen = false;
|
|
1327
|
+
this.drawerElement.style.transform = "";
|
|
1328
|
+
this.backdropElement.style.background = "";
|
|
1329
|
+
this.rootElement.classList.remove("open");
|
|
1330
|
+
this.eventBus.emit("drawer-closed", { drawerId: this.componentId }, null, this);
|
|
1331
|
+
if (this.onCloseCallback) {
|
|
1332
|
+
this.onCloseCallback();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Toggle drawer open/closed
|
|
1337
|
+
*/
|
|
1338
|
+
toggle() {
|
|
1339
|
+
if (this.isOpen) {
|
|
1340
|
+
this.close();
|
|
1341
|
+
} else {
|
|
1342
|
+
this.open();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Check if drawer is currently open
|
|
1347
|
+
*/
|
|
1348
|
+
getIsOpen() {
|
|
1349
|
+
return this.isOpen;
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Set the content element for the drawer
|
|
1353
|
+
* @param element - The element to insert into the drawer content area
|
|
1354
|
+
*/
|
|
1355
|
+
setContent(element) {
|
|
1356
|
+
this.contentElement.innerHTML = "";
|
|
1357
|
+
this.contentElement.appendChild(element);
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Set a callback to be called when drawer closes
|
|
1361
|
+
* @param callback - Function to call on close
|
|
1362
|
+
*/
|
|
1363
|
+
setOnClose(callback) {
|
|
1364
|
+
this.onCloseCallback = callback;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Get the content container element
|
|
1368
|
+
*/
|
|
1369
|
+
getContentContainer() {
|
|
1370
|
+
return this.contentElement;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
// src/DOMUtils.ts
|
|
1375
|
+
function isInInputContext(element) {
|
|
1376
|
+
if (!element) return false;
|
|
1377
|
+
const tagName = element.tagName.toLowerCase();
|
|
1378
|
+
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
if (element.contentEditable === "true" || element.isContentEditable) {
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
if (element.closest("input") !== null || element.closest("textarea") !== null || element.closest("select") !== null || element.closest('[contenteditable="true"]') !== null || element.closest('[contenteditable=""]') !== null) {
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
if (element.closest(".modal") !== null) {
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
const inputFieldIds = ["map-title-input"];
|
|
1391
|
+
if (inputFieldIds.includes(element.id) || inputFieldIds.some((id) => element.closest(`#${id}`) !== null)) {
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
function hasModifierKeys(event) {
|
|
1397
|
+
return event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
1398
|
+
}
|
|
1399
|
+
function shouldIgnoreShortcut(event) {
|
|
1400
|
+
const target = event.target;
|
|
1401
|
+
return hasModifierKeys(event) || isInInputContext(target);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/KeyboardShortcutManager.ts
|
|
1405
|
+
var KeyboardState = /* @__PURE__ */ ((KeyboardState2) => {
|
|
1406
|
+
KeyboardState2["NORMAL"] = "normal";
|
|
1407
|
+
KeyboardState2["AWAITING_ARGS"] = "awaiting_args";
|
|
1408
|
+
return KeyboardState2;
|
|
1409
|
+
})(KeyboardState || {});
|
|
1410
|
+
var KeyboardShortcutManager = class {
|
|
1411
|
+
constructor(config) {
|
|
1412
|
+
this.shortcuts = /* @__PURE__ */ new Map();
|
|
1413
|
+
this.state = "normal" /* NORMAL */;
|
|
1414
|
+
this.currentCommand = "";
|
|
1415
|
+
this.currentArgs = "";
|
|
1416
|
+
this.helpContainer = null;
|
|
1417
|
+
this.timeout = 3e3;
|
|
1418
|
+
// Default 3 second timeout
|
|
1419
|
+
this.timeoutId = null;
|
|
1420
|
+
this.helpOverlay = null;
|
|
1421
|
+
this.immediateExecution = false;
|
|
1422
|
+
// Enable immediate execution mode
|
|
1423
|
+
this.previewDelay = 300;
|
|
1424
|
+
// Default 300ms preview delay
|
|
1425
|
+
this.previewTimeoutId = null;
|
|
1426
|
+
// Separate timeout for preview
|
|
1427
|
+
this.isPreviewActive = false;
|
|
1428
|
+
// Track if preview is currently active
|
|
1429
|
+
this.onStateChange = null;
|
|
1430
|
+
this.helpContainer = config.helpContainer || null;
|
|
1431
|
+
this.timeout = config.timeout || 3e3;
|
|
1432
|
+
this.immediateExecution = config.immediateExecution || false;
|
|
1433
|
+
this.previewDelay = config.previewDelay || 300;
|
|
1434
|
+
this.onStateChange = config.onStateChange || null;
|
|
1435
|
+
config.shortcuts.forEach((shortcut) => {
|
|
1436
|
+
this.shortcuts.set(shortcut.key, shortcut);
|
|
1437
|
+
});
|
|
1438
|
+
this.initialize();
|
|
1439
|
+
}
|
|
1440
|
+
initialize() {
|
|
1441
|
+
document.addEventListener("keydown", this.handleKeydown.bind(this));
|
|
1442
|
+
this.updateStateIndicator();
|
|
1443
|
+
}
|
|
1444
|
+
handleKeydown(event) {
|
|
1445
|
+
const target = event.target;
|
|
1446
|
+
if (isInInputContext(target)) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (event.key === "?" && this.state === "normal" /* NORMAL */) {
|
|
1450
|
+
event.preventDefault();
|
|
1451
|
+
this.showHelp();
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (event.key === "Escape") {
|
|
1455
|
+
event.preventDefault();
|
|
1456
|
+
if (this.immediateExecution && this.isPreviewActive) {
|
|
1457
|
+
this.cancelCurrentPreview();
|
|
1458
|
+
}
|
|
1459
|
+
this.resetState();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (this.state === "normal" /* NORMAL */) {
|
|
1463
|
+
this.handleNormalState(event);
|
|
1464
|
+
} else if (this.state === "awaiting_args" /* AWAITING_ARGS */) {
|
|
1465
|
+
this.handleAwaitingArgsState(event);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
handleNormalState(event) {
|
|
1469
|
+
const key = event.key.toLowerCase();
|
|
1470
|
+
const shortcut = this.shortcuts.get(key);
|
|
1471
|
+
if (shortcut) {
|
|
1472
|
+
event.preventDefault();
|
|
1473
|
+
if (shortcut.contextFilter && !shortcut.contextFilter(event)) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (shortcut.requiresArgs) {
|
|
1477
|
+
this.state = "awaiting_args" /* AWAITING_ARGS */;
|
|
1478
|
+
this.currentCommand = key;
|
|
1479
|
+
this.currentArgs = "";
|
|
1480
|
+
this.updateStateIndicator();
|
|
1481
|
+
this.startTimeout();
|
|
1482
|
+
if (this.onStateChange) {
|
|
1483
|
+
this.onStateChange(this.state, key);
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
this.executeShortcut(shortcut);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
handleAwaitingArgsState(event) {
|
|
1491
|
+
const key = event.key;
|
|
1492
|
+
if (key >= "0" && key <= "9") {
|
|
1493
|
+
event.preventDefault();
|
|
1494
|
+
this.currentArgs += key;
|
|
1495
|
+
this.updateStateIndicator();
|
|
1496
|
+
this.resetTimeout();
|
|
1497
|
+
if (this.immediateExecution) {
|
|
1498
|
+
this.schedulePreviewExecution();
|
|
1499
|
+
}
|
|
1500
|
+
} else if (key === "Enter" || key === " ") {
|
|
1501
|
+
event.preventDefault();
|
|
1502
|
+
this.executeCurrentCommand();
|
|
1503
|
+
} else if (key === "Backspace") {
|
|
1504
|
+
event.preventDefault();
|
|
1505
|
+
this.currentArgs = this.currentArgs.slice(0, -1);
|
|
1506
|
+
this.updateStateIndicator();
|
|
1507
|
+
this.resetTimeout();
|
|
1508
|
+
if (this.immediateExecution) {
|
|
1509
|
+
this.cancelPreviewExecution();
|
|
1510
|
+
if (this.currentArgs.length > 0) {
|
|
1511
|
+
this.schedulePreviewExecution();
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
executeCurrentCommand() {
|
|
1517
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
1518
|
+
if (shortcut && this.currentArgs) {
|
|
1519
|
+
this.executeShortcut(shortcut, this.currentArgs);
|
|
1520
|
+
}
|
|
1521
|
+
this.resetState();
|
|
1522
|
+
}
|
|
1523
|
+
executeShortcut(shortcut, args) {
|
|
1524
|
+
try {
|
|
1525
|
+
shortcut.handler(args);
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
console.error("Error executing shortcut:", error);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
schedulePreviewExecution() {
|
|
1531
|
+
this.cancelPreviewExecution();
|
|
1532
|
+
this.previewTimeoutId = window.setTimeout(() => {
|
|
1533
|
+
this.executePreview();
|
|
1534
|
+
}, this.previewDelay);
|
|
1535
|
+
}
|
|
1536
|
+
cancelPreviewExecution() {
|
|
1537
|
+
if (this.previewTimeoutId) {
|
|
1538
|
+
window.clearTimeout(this.previewTimeoutId);
|
|
1539
|
+
this.previewTimeoutId = null;
|
|
1540
|
+
}
|
|
1541
|
+
if (this.isPreviewActive) {
|
|
1542
|
+
this.cancelCurrentPreview();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
executePreview() {
|
|
1546
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
1547
|
+
if (shortcut && this.currentArgs) {
|
|
1548
|
+
try {
|
|
1549
|
+
if (shortcut.previewHandler) {
|
|
1550
|
+
shortcut.previewHandler(this.currentArgs);
|
|
1551
|
+
this.isPreviewActive = true;
|
|
1552
|
+
} else {
|
|
1553
|
+
shortcut.handler(this.currentArgs);
|
|
1554
|
+
this.isPreviewActive = true;
|
|
1555
|
+
}
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
console.error("Error executing preview:", error);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
cancelCurrentPreview() {
|
|
1562
|
+
if (this.isPreviewActive) {
|
|
1563
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
1564
|
+
if (shortcut && shortcut.cancelHandler) {
|
|
1565
|
+
try {
|
|
1566
|
+
shortcut.cancelHandler();
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
console.error("Error canceling preview:", error);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
this.isPreviewActive = false;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
resetState() {
|
|
1575
|
+
this.state = "normal" /* NORMAL */;
|
|
1576
|
+
this.currentCommand = "";
|
|
1577
|
+
this.currentArgs = "";
|
|
1578
|
+
this.clearTimeout();
|
|
1579
|
+
this.cancelPreviewExecution();
|
|
1580
|
+
this.updateStateIndicator();
|
|
1581
|
+
this.hideHelp();
|
|
1582
|
+
if (this.onStateChange) {
|
|
1583
|
+
this.onStateChange(this.state);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
startTimeout() {
|
|
1587
|
+
this.timeoutId = window.setTimeout(() => {
|
|
1588
|
+
this.resetState();
|
|
1589
|
+
}, this.timeout);
|
|
1590
|
+
}
|
|
1591
|
+
resetTimeout() {
|
|
1592
|
+
this.clearTimeout();
|
|
1593
|
+
this.startTimeout();
|
|
1594
|
+
}
|
|
1595
|
+
clearTimeout() {
|
|
1596
|
+
if (this.timeoutId) {
|
|
1597
|
+
window.clearTimeout(this.timeoutId);
|
|
1598
|
+
this.timeoutId = null;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
updateStateIndicator() {
|
|
1602
|
+
const existingIndicator = document.getElementById("keyboard-state-indicator");
|
|
1603
|
+
if (existingIndicator) {
|
|
1604
|
+
existingIndicator.remove();
|
|
1605
|
+
}
|
|
1606
|
+
if (this.state === "normal" /* NORMAL */) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const indicator = document.createElement("div");
|
|
1610
|
+
indicator.id = "keyboard-state-indicator";
|
|
1611
|
+
indicator.className = "fixed top-4 right-4 bg-blue-600 text-white px-3 py-2 rounded-lg shadow-lg z-50 font-mono text-sm";
|
|
1612
|
+
if (this.state === "awaiting_args" /* AWAITING_ARGS */) {
|
|
1613
|
+
const shortcut = this.shortcuts.get(this.currentCommand);
|
|
1614
|
+
const description = shortcut ? shortcut.description : "Unknown command";
|
|
1615
|
+
indicator.innerHTML = `
|
|
1616
|
+
<div class="flex items-center space-x-2">
|
|
1617
|
+
<span>${this.currentCommand.toUpperCase()}</span>
|
|
1618
|
+
<span class="text-blue-200">${this.currentArgs || "_"}</span>
|
|
1619
|
+
</div>
|
|
1620
|
+
<div class="text-xs text-blue-200 mt-1">${description}</div>
|
|
1621
|
+
`;
|
|
1622
|
+
}
|
|
1623
|
+
document.body.appendChild(indicator);
|
|
1624
|
+
}
|
|
1625
|
+
showHelp() {
|
|
1626
|
+
if (this.helpOverlay) {
|
|
1627
|
+
this.hideHelp();
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
this.helpOverlay = document.createElement("div");
|
|
1631
|
+
this.helpOverlay.id = "keyboard-help-overlay";
|
|
1632
|
+
this.helpOverlay.className = "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50";
|
|
1633
|
+
const helpContent = document.createElement("div");
|
|
1634
|
+
helpContent.className = "bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl max-h-[80vh] overflow-y-auto p-6";
|
|
1635
|
+
helpContent.innerHTML = this.generateHelpContent();
|
|
1636
|
+
this.helpOverlay.appendChild(helpContent);
|
|
1637
|
+
document.body.appendChild(this.helpOverlay);
|
|
1638
|
+
this.helpOverlay.addEventListener("click", (e) => {
|
|
1639
|
+
if (e.target === this.helpOverlay) {
|
|
1640
|
+
this.hideHelp();
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
hideHelp() {
|
|
1645
|
+
if (this.helpOverlay) {
|
|
1646
|
+
this.helpOverlay.remove();
|
|
1647
|
+
this.helpOverlay = null;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
generateHelpContent() {
|
|
1651
|
+
const categories = /* @__PURE__ */ new Map();
|
|
1652
|
+
this.shortcuts.forEach((shortcut) => {
|
|
1653
|
+
const category = shortcut.category || "General";
|
|
1654
|
+
if (!categories.has(category)) {
|
|
1655
|
+
categories.set(category, []);
|
|
1656
|
+
}
|
|
1657
|
+
categories.get(category).push(shortcut);
|
|
1658
|
+
});
|
|
1659
|
+
let html = `
|
|
1660
|
+
<div class="flex items-center justify-between mb-4">
|
|
1661
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Keyboard Shortcuts</h2>
|
|
1662
|
+
<button class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" onclick="this.closest('#keyboard-help-overlay').remove()">
|
|
1663
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1664
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
1665
|
+
</svg>
|
|
1666
|
+
</button>
|
|
1667
|
+
</div>
|
|
1668
|
+
`;
|
|
1669
|
+
categories.forEach((shortcuts, category) => {
|
|
1670
|
+
html += `
|
|
1671
|
+
<div class="mb-6">
|
|
1672
|
+
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">${category}</h3>
|
|
1673
|
+
<div class="space-y-2">
|
|
1674
|
+
`;
|
|
1675
|
+
shortcuts.forEach((shortcut) => {
|
|
1676
|
+
const keyDisplay = shortcut.requiresArgs ? `${shortcut.key.toUpperCase()}<span class="text-blue-600 dark:text-blue-400"><number></span>` : shortcut.key.toUpperCase();
|
|
1677
|
+
html += `
|
|
1678
|
+
<div class="flex items-center justify-between py-2 px-3 bg-gray-50 dark:bg-gray-700 rounded">
|
|
1679
|
+
<span class="text-sm text-gray-700 dark:text-gray-300">${shortcut.description}</span>
|
|
1680
|
+
<kbd class="px-2 py-1 text-xs font-mono bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded">${keyDisplay}</kbd>
|
|
1681
|
+
</div>
|
|
1682
|
+
`;
|
|
1683
|
+
});
|
|
1684
|
+
html += `
|
|
1685
|
+
</div>
|
|
1686
|
+
</div>
|
|
1687
|
+
`;
|
|
1688
|
+
});
|
|
1689
|
+
html += `
|
|
1690
|
+
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-600">
|
|
1691
|
+
<p class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
|
1692
|
+
Press <kbd class="px-2 py-1 text-xs font-mono bg-gray-200 dark:bg-gray-600 rounded">ESC</kbd> to cancel any command or
|
|
1693
|
+
<kbd class="px-2 py-1 text-xs font-mono bg-gray-200 dark:bg-gray-600 rounded">?</kbd> to close this help
|
|
1694
|
+
</p>
|
|
1695
|
+
</div>
|
|
1696
|
+
`;
|
|
1697
|
+
return html;
|
|
1698
|
+
}
|
|
1699
|
+
destroy() {
|
|
1700
|
+
document.removeEventListener("keydown", this.handleKeydown.bind(this));
|
|
1701
|
+
this.cancelPreviewExecution();
|
|
1702
|
+
this.resetState();
|
|
1703
|
+
this.hideHelp();
|
|
1704
|
+
}
|
|
1705
|
+
getState() {
|
|
1706
|
+
return this.state;
|
|
1707
|
+
}
|
|
1708
|
+
getCurrentCommand() {
|
|
1709
|
+
return this.currentCommand;
|
|
1710
|
+
}
|
|
1711
|
+
getCurrentArgs() {
|
|
1712
|
+
return this.currentArgs;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
export { BaseComponent, BasePage, ComponentEventTypes, EventBus, KeyboardShortcutManager, KeyboardState, LifecycleController, LifecycleEventTypes, MobileBottomDrawer, Modal, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, shouldIgnoreShortcut };
|
|
1717
|
+
//# sourceMappingURL=index.mjs.map
|
|
1718
|
+
//# sourceMappingURL=index.mjs.map
|