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