@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/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">&lt;number&gt;</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