@hawsen-the-first/interactiv 0.0.1

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.
Files changed (48) hide show
  1. package/README.md +326 -0
  2. package/dist/animations.css +160 -0
  3. package/dist/index.d.ts +20 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +26 -0
  6. package/dist/src/animationBus.d.ts +30 -0
  7. package/dist/src/animationBus.d.ts.map +1 -0
  8. package/dist/src/animationBus.js +125 -0
  9. package/dist/src/appBuilder.d.ts +173 -0
  10. package/dist/src/appBuilder.d.ts.map +1 -0
  11. package/dist/src/appBuilder.js +957 -0
  12. package/dist/src/eventBus.d.ts +100 -0
  13. package/dist/src/eventBus.d.ts.map +1 -0
  14. package/dist/src/eventBus.js +326 -0
  15. package/dist/src/eventManager.d.ts +87 -0
  16. package/dist/src/eventManager.d.ts.map +1 -0
  17. package/dist/src/eventManager.js +455 -0
  18. package/dist/src/garbageCollector.d.ts +68 -0
  19. package/dist/src/garbageCollector.d.ts.map +1 -0
  20. package/dist/src/garbageCollector.js +169 -0
  21. package/dist/src/logger.d.ts +11 -0
  22. package/dist/src/logger.d.ts.map +1 -0
  23. package/dist/src/logger.js +15 -0
  24. package/dist/src/navigationManager.d.ts +105 -0
  25. package/dist/src/navigationManager.d.ts.map +1 -0
  26. package/dist/src/navigationManager.js +533 -0
  27. package/dist/src/screensaverManager.d.ts +66 -0
  28. package/dist/src/screensaverManager.d.ts.map +1 -0
  29. package/dist/src/screensaverManager.js +417 -0
  30. package/dist/src/settingsManager.d.ts +48 -0
  31. package/dist/src/settingsManager.d.ts.map +1 -0
  32. package/dist/src/settingsManager.js +317 -0
  33. package/dist/src/stateManager.d.ts +58 -0
  34. package/dist/src/stateManager.d.ts.map +1 -0
  35. package/dist/src/stateManager.js +278 -0
  36. package/dist/src/types.d.ts +32 -0
  37. package/dist/src/types.d.ts.map +1 -0
  38. package/dist/src/types.js +1 -0
  39. package/dist/utils/generateGuid.d.ts +2 -0
  40. package/dist/utils/generateGuid.d.ts.map +1 -0
  41. package/dist/utils/generateGuid.js +19 -0
  42. package/dist/utils/logger.d.ts +9 -0
  43. package/dist/utils/logger.d.ts.map +1 -0
  44. package/dist/utils/logger.js +42 -0
  45. package/dist/utils/template-helpers.d.ts +32 -0
  46. package/dist/utils/template-helpers.d.ts.map +1 -0
  47. package/dist/utils/template-helpers.js +24 -0
  48. package/package.json +59 -0
@@ -0,0 +1,11 @@
1
+ import { Logger } from "../utils/logger";
2
+ declare const logger: Logger;
3
+ /**
4
+ * Configure the internal package logger
5
+ * This allows consuming projects to adjust logging levels without creating their own logger
6
+ * @param debugMode Enable debug mode (overrides minLogLevel)
7
+ * @param minLogLevel Minimum log level: 'none' | 'error' | 'warn' | 'trace'
8
+ */
9
+ export declare function configureLogger(debugMode?: boolean, minLogLevel?: string): void;
10
+ export { logger };
11
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAIzC,QAAA,MAAM,MAAM,QAA6B,CAAC;AAE1C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,GAAE,OAAe,EAAE,WAAW,GAAE,MAAgB,GAAG,IAAI,CAG/F;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { Logger } from "../utils/logger";
2
+ // Centralized logger instance for internal package use
3
+ // Default configuration: error-only logging for production builds
4
+ const logger = new Logger(false, "error");
5
+ /**
6
+ * Configure the internal package logger
7
+ * This allows consuming projects to adjust logging levels without creating their own logger
8
+ * @param debugMode Enable debug mode (overrides minLogLevel)
9
+ * @param minLogLevel Minimum log level: 'none' | 'error' | 'warn' | 'trace'
10
+ */
11
+ export function configureLogger(debugMode = false, minLogLevel = "error") {
12
+ logger.debugMode = debugMode;
13
+ logger.minLoglevel = minLogLevel;
14
+ }
15
+ export { logger };
@@ -0,0 +1,105 @@
1
+ import { EventOrchestrator } from "./eventBus";
2
+ import { Page, View } from "./appBuilder";
3
+ import { type StateSubscription } from "./stateManager";
4
+ export interface TransitionConfig {
5
+ type: "slide" | "fade" | "scale" | "flip" | "snap" | "custom";
6
+ direction?: "left" | "right" | "up" | "down";
7
+ duration?: number;
8
+ easing?: string;
9
+ customCSS?: string;
10
+ }
11
+ export interface NavigationState {
12
+ currentPageId: string | null;
13
+ currentViewId: string | null;
14
+ isTransitioning: boolean;
15
+ }
16
+ /**
17
+ * NavigationManager - Singleton Service for Application Navigation
18
+ *
19
+ * **IMPORTANT: Only ONE instance of NavigationManager should exist per application.**
20
+ *
21
+ * This class manages page and view navigation using shared global state. Multiple instances
22
+ * would cause critical issues:
23
+ * - Conflicting global state (navigation.currentPageId, navigation.currentViewId, navigation.isTransitioning)
24
+ * - Event bus namespace collisions (all instances register as "navigation-manager")
25
+ * - Ambiguous page/view registration and navigation routing
26
+ *
27
+ * The NavigationManager is automatically instantiated by AppBuilder and should not be
28
+ * manually created elsewhere in the application.
29
+ *
30
+ * @example
31
+ * // ✅ Correct: Created once by AppBuilder
32
+ * class AppBuilder extends RenderableComponent {
33
+ * private navigationManager: NavigationManager;
34
+ * constructor(orchestrator: EventOrchestrator) {
35
+ * this.navigationManager = new NavigationManager(orchestrator);
36
+ * }
37
+ * }
38
+ *
39
+ * // ❌ Wrong: Creating additional instances
40
+ * const nav1 = new NavigationManager(orchestrator); // First instance - OK
41
+ * const nav2 = new NavigationManager(orchestrator); // ERROR: Will throw!
42
+ */
43
+ export declare class NavigationManager {
44
+ private static instance;
45
+ private static isDestroyed;
46
+ private eventBus;
47
+ private orchestrator;
48
+ private pages;
49
+ private views;
50
+ private stateSubscriptions;
51
+ private activeTransitionCleanups;
52
+ /**
53
+ * Get the singleton instance of NavigationManager
54
+ * Returns null if no instance has been created yet
55
+ */
56
+ static getInstance(): NavigationManager | null;
57
+ constructor(orchestrator: EventOrchestrator);
58
+ private initializeGlobalState;
59
+ private setupEventListeners;
60
+ registerPage(page: Page): void;
61
+ registerView(view: View): void;
62
+ private setupPageContainer;
63
+ private setupViewContainer;
64
+ navigateToPage(pageId: string, config?: TransitionConfig, priority?: string): Promise<void>;
65
+ navigateToView(viewId: string, config?: TransitionConfig, priority?: string): Promise<void>;
66
+ private performPageNavigationInternal;
67
+ private performViewNavigationInternal;
68
+ private performPageTransition;
69
+ private performViewTransition;
70
+ private animateOut;
71
+ private animateIn;
72
+ private normalizeTransitionConfig;
73
+ private clearAnimationClasses;
74
+ private showPage;
75
+ private hidePage;
76
+ private showView;
77
+ private hideView;
78
+ private hideAllViews;
79
+ getCurrentPageId(): string | null;
80
+ getCurrentViewId(): string | null;
81
+ isTransitioning(): boolean;
82
+ getRegisteredPages(): string[];
83
+ getRegisteredViews(): string[];
84
+ subscribeToCurrentPage(callback: (pageId: string | null) => void): StateSubscription;
85
+ subscribeToCurrentView(callback: (viewId: string | null) => void): StateSubscription;
86
+ subscribeToTransitionState(callback: (isTransitioning: boolean) => void): StateSubscription;
87
+ /**
88
+ * Clean up any orphaned transition listeners
89
+ * This is a safety net for transitions that didn't complete properly
90
+ */
91
+ cleanupOrphanedTransitions(): void;
92
+ /**
93
+ * Get the count of active transitions
94
+ */
95
+ getActiveTransitionCount(): number;
96
+ /**
97
+ * Cleanup method for proper resource management
98
+ *
99
+ * Destroys the NavigationManager instance and clears the singleton reference.
100
+ * After calling destroy(), a new NavigationManager instance can be created if needed
101
+ * (though this is typically only necessary during application teardown/restart).
102
+ */
103
+ destroy(): void;
104
+ }
105
+ //# sourceMappingURL=navigationManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigationManager.d.ts","sourceRoot":"","sources":["../../src/navigationManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAKtE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAkC;IACzD,OAAO,CAAC,MAAM,CAAC,WAAW,CAAkB;IAE5C,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,wBAAwB,CAA2C;IAE3E;;;OAGG;WACW,WAAW,IAAI,iBAAiB,GAAG,IAAI;gBAIzC,YAAY,EAAE,iBAAiB;IA4B3C,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,mBAAmB;IAsBpB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAa9B,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAQrC,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,kBAAkB;IAMb,cAAc,CACzB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,gBAAmC,EAC3C,QAAQ,GAAE,MAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAYH,cAAc,CACzB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,gBAAmC,EAC3C,QAAQ,GAAE,MAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC;YAYF,6BAA6B;YA6B7B,6BAA6B;YAgC7B,qBAAqB;YAmBrB,qBAAqB;YAqBrB,UAAU;YAuEV,SAAS;IA2FvB,OAAO,CAAC,yBAAyB;IAkBjC,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,YAAY;IAQb,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAIjC,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAIjC,eAAe,IAAI,OAAO;IAI1B,kBAAkB,IAAI,MAAM,EAAE;IAI9B,kBAAkB,IAAI,MAAM,EAAE;IAK9B,sBAAsB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,iBAAiB;IAIpF,sBAAsB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,iBAAiB;IAIpF,0BAA0B,CAAC,QAAQ,EAAE,CAAC,eAAe,EAAE,OAAO,KAAK,IAAI,GAAG,iBAAiB;IAIlG;;;OAGG;IACI,0BAA0B,IAAI,IAAI;IAezC;;OAEG;IACI,wBAAwB,IAAI,MAAM;IAIzC;;;;;;OAMG;IACI,OAAO,IAAI,IAAI;CAoBvB"}
@@ -0,0 +1,533 @@
1
+ import { stateManager } from "./stateManager";
2
+ import { logger } from "./logger";
3
+ const log = logger;
4
+ /**
5
+ * NavigationManager - Singleton Service for Application Navigation
6
+ *
7
+ * **IMPORTANT: Only ONE instance of NavigationManager should exist per application.**
8
+ *
9
+ * This class manages page and view navigation using shared global state. Multiple instances
10
+ * would cause critical issues:
11
+ * - Conflicting global state (navigation.currentPageId, navigation.currentViewId, navigation.isTransitioning)
12
+ * - Event bus namespace collisions (all instances register as "navigation-manager")
13
+ * - Ambiguous page/view registration and navigation routing
14
+ *
15
+ * The NavigationManager is automatically instantiated by AppBuilder and should not be
16
+ * manually created elsewhere in the application.
17
+ *
18
+ * @example
19
+ * // ✅ Correct: Created once by AppBuilder
20
+ * class AppBuilder extends RenderableComponent {
21
+ * private navigationManager: NavigationManager;
22
+ * constructor(orchestrator: EventOrchestrator) {
23
+ * this.navigationManager = new NavigationManager(orchestrator);
24
+ * }
25
+ * }
26
+ *
27
+ * // ❌ Wrong: Creating additional instances
28
+ * const nav1 = new NavigationManager(orchestrator); // First instance - OK
29
+ * const nav2 = new NavigationManager(orchestrator); // ERROR: Will throw!
30
+ */
31
+ export class NavigationManager {
32
+ static instance = null;
33
+ static isDestroyed = false;
34
+ eventBus;
35
+ orchestrator;
36
+ pages = new Map();
37
+ views = new Map();
38
+ stateSubscriptions = [];
39
+ activeTransitionCleanups = new Map();
40
+ /**
41
+ * Get the singleton instance of NavigationManager
42
+ * Returns null if no instance has been created yet
43
+ */
44
+ static getInstance() {
45
+ return NavigationManager.instance;
46
+ }
47
+ constructor(orchestrator) {
48
+ // Enforce singleton pattern - only one instance allowed
49
+ if (NavigationManager.instance !== null) {
50
+ const errorMsg = "NavigationManager instance already exists! Only one NavigationManager should be created per application. " +
51
+ "The NavigationManager is automatically created by AppBuilder and should not be instantiated manually.";
52
+ const error = new Error(errorMsg);
53
+ log.error(errorMsg, error);
54
+ throw error;
55
+ }
56
+ if (NavigationManager.isDestroyed) {
57
+ log.warn("Creating new NavigationManager instance after previous instance was destroyed.");
58
+ NavigationManager.isDestroyed = false;
59
+ }
60
+ // Register this as the singleton instance
61
+ NavigationManager.instance = this;
62
+ log.trace("NavigationManager singleton instance created");
63
+ this.orchestrator = orchestrator;
64
+ this.eventBus = orchestrator.registerEventBus("navigation-manager");
65
+ // Initialize global navigation state
66
+ this.initializeGlobalState();
67
+ this.setupEventListeners();
68
+ }
69
+ initializeGlobalState() {
70
+ // Initialize navigation state in global store if not already present
71
+ if (!stateManager.has("navigation.currentPageId")) {
72
+ stateManager.set("navigation.currentPageId", null);
73
+ }
74
+ if (!stateManager.has("navigation.currentViewId")) {
75
+ stateManager.set("navigation.currentViewId", null);
76
+ }
77
+ if (!stateManager.has("navigation.isTransitioning")) {
78
+ stateManager.set("navigation.isTransitioning", false);
79
+ }
80
+ }
81
+ setupEventListeners() {
82
+ this.eventBus.on("navigate-to-page", (e) => {
83
+ const { pageId, config } = e.detail;
84
+ this.performPageNavigationInternal(pageId, config);
85
+ });
86
+ this.eventBus.on("navigate-to-view", (e) => {
87
+ const { viewId, config } = e.detail;
88
+ this.performViewNavigationInternal(viewId, config);
89
+ });
90
+ this.eventBus.on("register-page", (e) => {
91
+ const { page } = e.detail;
92
+ this.registerPage(page);
93
+ });
94
+ this.eventBus.on("register-view", (e) => {
95
+ const { view } = e.detail;
96
+ this.registerView(view);
97
+ });
98
+ }
99
+ registerPage(page) {
100
+ this.pages.set(page.componentId, page);
101
+ this.setupPageContainer(page);
102
+ // If this is the first page, make it active
103
+ if (!stateManager.get("navigation.currentPageId")) {
104
+ stateManager.set("navigation.currentPageId", page.componentId);
105
+ this.showPage(page.componentId);
106
+ }
107
+ else {
108
+ this.hidePage(page.componentId);
109
+ }
110
+ }
111
+ registerView(view) {
112
+ this.views.set(view.componentId, view);
113
+ this.setupViewContainer(view);
114
+ // Hide all views by default - they will be shown when navigated to
115
+ this.hideView(view.componentId);
116
+ }
117
+ setupPageContainer(page) {
118
+ const hostElement = page.getHostElement();
119
+ hostElement.classList.add("nav-item", "nav-page");
120
+ hostElement.setAttribute("data-page-id", page.componentId);
121
+ }
122
+ setupViewContainer(view) {
123
+ const hostElement = view.getHostElement();
124
+ hostElement.classList.add("nav-item", "nav-view");
125
+ hostElement.setAttribute("data-view-id", view.componentId);
126
+ }
127
+ async navigateToPage(pageId, config = { type: "snap" }, priority = "immediate") {
128
+ // Use EventOrchestrator queue for navigation requests
129
+ if (stateManager.get("navigation.isTransitioning")) {
130
+ // Queue the navigation through the orchestrator
131
+ this.orchestrator.enqueue("navigate-to-page", "navigation-manager", priority, { pageId, config });
132
+ return;
133
+ }
134
+ // Direct navigation if not transitioning
135
+ this.orchestrator.enqueue("navigate-to-page", "navigation-manager", priority, { pageId, config });
136
+ }
137
+ async navigateToView(viewId, config = { type: "snap" }, priority = "immediate") {
138
+ // Use EventOrchestrator queue for navigation requests
139
+ if (stateManager.get("navigation.isTransitioning")) {
140
+ // Queue the navigation through the orchestrator
141
+ this.orchestrator.enqueue("navigate-to-view", "navigation-manager", priority, { viewId, config });
142
+ return;
143
+ }
144
+ // Direct navigation if not transitioning
145
+ this.orchestrator.enqueue("navigate-to-view", "navigation-manager", priority, { viewId, config });
146
+ }
147
+ async performPageNavigationInternal(pageId, config) {
148
+ if (!this.pages.has(pageId)) {
149
+ throw new Error(`Page with id "${pageId}" not found`);
150
+ }
151
+ if (stateManager.get("navigation.currentPageId") === pageId) {
152
+ return; // Already on this page
153
+ }
154
+ const previousPageId = stateManager.get("navigation.currentPageId");
155
+ stateManager.set("navigation.isTransitioning", true);
156
+ try {
157
+ await this.performPageTransition(pageId, config);
158
+ stateManager.set("navigation.currentPageId", pageId);
159
+ stateManager.set("navigation.currentViewId", null); // Reset view when changing pages
160
+ // Hide all views when navigating to a page to ensure clean state
161
+ this.hideAllViews();
162
+ this.eventBus.emit("page-changed", {
163
+ newPageId: pageId,
164
+ previousPageId,
165
+ });
166
+ }
167
+ finally {
168
+ stateManager.set("navigation.isTransitioning", false);
169
+ }
170
+ }
171
+ async performViewNavigationInternal(viewId, config) {
172
+ if (!this.views.has(viewId)) {
173
+ throw new Error(`View with id "${viewId}" not found`);
174
+ }
175
+ if (stateManager.get("navigation.currentViewId") === viewId) {
176
+ log.trace(`Already on view ${viewId}, emitting re-entry event`);
177
+ //this.eventBus.emit("view-re-entered", { viewId });
178
+ return; // Already on this view
179
+ }
180
+ const previousViewId = stateManager.get("navigation.currentViewId");
181
+ log.trace(`Starting navigation from ${previousViewId} to ${viewId}`);
182
+ stateManager.set("navigation.isTransitioning", true);
183
+ try {
184
+ await this.performViewTransition(viewId, config);
185
+ stateManager.set("navigation.currentViewId", viewId);
186
+ this.eventBus.emit("view-changed", {
187
+ newViewId: viewId,
188
+ previousViewId,
189
+ });
190
+ log.trace(`Navigation to ${viewId} completed successfully`);
191
+ }
192
+ catch (error) {
193
+ log.error(`Navigation to ${viewId} failed:`, error);
194
+ throw error;
195
+ }
196
+ finally {
197
+ stateManager.set("navigation.isTransitioning", false);
198
+ }
199
+ }
200
+ async performPageTransition(targetPageId, config) {
201
+ const currentPageId = stateManager.get("navigation.currentPageId");
202
+ const currentPage = currentPageId ? this.pages.get(currentPageId) : null;
203
+ const targetPage = this.pages.get(targetPageId);
204
+ // Validate and normalize config
205
+ const normalizedConfig = this.normalizeTransitionConfig(config);
206
+ // Show target page first (but invisible)
207
+ this.showPage(targetPageId);
208
+ if (currentPage) {
209
+ await this.animateOut(currentPage.getHostElement(), normalizedConfig);
210
+ this.hidePage(currentPage.componentId);
211
+ }
212
+ await this.animateIn(targetPage.getHostElement(), normalizedConfig);
213
+ }
214
+ async performViewTransition(targetViewId, config) {
215
+ const currentViewId = stateManager.get("navigation.currentViewId");
216
+ const currentView = currentViewId ? this.views.get(currentViewId) : null;
217
+ const targetView = this.views.get(targetViewId);
218
+ // Validate and normalize config
219
+ const normalizedConfig = this.normalizeTransitionConfig(config);
220
+ // Show target view first (before animation)
221
+ this.showView(targetViewId, normalizedConfig.type !== "snap");
222
+ // Animate if needed
223
+ if (currentView) {
224
+ await this.animateOut(currentView.getHostElement(), normalizedConfig);
225
+ // Hide current view after animation completes
226
+ this.hideView(currentViewId);
227
+ }
228
+ await this.animateIn(targetView.getHostElement(), normalizedConfig);
229
+ }
230
+ async animateOut(element, config) {
231
+ // Handle snap navigation - immediate hide with no animation
232
+ if (config.type === "snap") {
233
+ element.style.transition = "none";
234
+ element.style.opacity = "0";
235
+ element.style.visibility = "hidden";
236
+ return Promise.resolve();
237
+ }
238
+ return new Promise((resolve) => {
239
+ const duration = config.duration || 300;
240
+ element.style.transition = `all ${duration}ms ${config.easing || "ease-in-out"}`;
241
+ let transitionendFired = false;
242
+ let fallbackTimerId = null;
243
+ const cleanup = () => {
244
+ if (transitionendFired)
245
+ return; // Prevent double execution
246
+ transitionendFired = true;
247
+ // Clean up both the event listener and timeout
248
+ element.removeEventListener("transitionend", transitionendHandler);
249
+ if (fallbackTimerId !== null) {
250
+ clearTimeout(fallbackTimerId);
251
+ fallbackTimerId = null;
252
+ }
253
+ // Remove from active transitions map
254
+ this.activeTransitionCleanups.delete(element);
255
+ resolve();
256
+ };
257
+ const transitionendHandler = () => {
258
+ cleanup();
259
+ };
260
+ element.addEventListener("transitionend", transitionendHandler, { once: true });
261
+ // Store cleanup function for this element
262
+ this.activeTransitionCleanups.set(element, cleanup);
263
+ // Apply exit animation
264
+ switch (config.type) {
265
+ case "slide":
266
+ element.classList.add(`slide-out-${config.direction || "left"}`);
267
+ break;
268
+ case "fade":
269
+ element.classList.add("fade-out");
270
+ break;
271
+ case "scale":
272
+ element.classList.add("scale-out");
273
+ break;
274
+ case "flip":
275
+ element.classList.add("flip-out");
276
+ break;
277
+ case "custom":
278
+ if (config.customCSS) {
279
+ element.style.cssText += config.customCSS;
280
+ }
281
+ break;
282
+ }
283
+ // Fallback timeout
284
+ fallbackTimerId = window.setTimeout(() => {
285
+ log.trace("Transition fallback timeout triggered for animateOut");
286
+ cleanup();
287
+ }, duration + 50);
288
+ });
289
+ }
290
+ async animateIn(element, config) {
291
+ // Handle snap navigation - immediate show with no animation
292
+ if (config.type === "snap") {
293
+ element.style.transition = "none";
294
+ element.style.opacity = "1";
295
+ element.style.visibility = "visible";
296
+ element.style.transform = "none";
297
+ this.clearAnimationClasses(element);
298
+ return Promise.resolve();
299
+ }
300
+ return new Promise((resolve) => {
301
+ const duration = config.duration || 300;
302
+ element.style.transition = `all ${duration}ms ${config.easing || "ease-in-out"}`;
303
+ let transitionendFired = false;
304
+ let fallbackTimerId = null;
305
+ const cleanup = () => {
306
+ if (transitionendFired)
307
+ return; // Prevent double execution
308
+ transitionendFired = true;
309
+ // Clean up both the event listener and timeout
310
+ element.removeEventListener("transitionend", transitionendHandler);
311
+ if (fallbackTimerId !== null) {
312
+ clearTimeout(fallbackTimerId);
313
+ fallbackTimerId = null;
314
+ }
315
+ // Remove from active transitions map
316
+ this.activeTransitionCleanups.delete(element);
317
+ this.clearAnimationClasses(element);
318
+ resolve();
319
+ };
320
+ const transitionendHandler = () => {
321
+ cleanup();
322
+ };
323
+ element.addEventListener("transitionend", transitionendHandler, { once: true });
324
+ // Store cleanup function for this element
325
+ this.activeTransitionCleanups.set(element, cleanup);
326
+ // Set initial state
327
+ switch (config.type) {
328
+ case "slide":
329
+ element.classList.add(`slide-in-${config.direction || "right"}`);
330
+ break;
331
+ case "fade":
332
+ element.classList.add("fade-in");
333
+ break;
334
+ case "scale":
335
+ element.classList.add("scale-in");
336
+ break;
337
+ case "flip":
338
+ element.classList.add("flip-in");
339
+ break;
340
+ }
341
+ // Animate to active state
342
+ requestAnimationFrame(() => {
343
+ switch (config.type) {
344
+ case "slide":
345
+ element.classList.remove(`slide-in-${config.direction || "right"}`);
346
+ element.classList.add("slide-active");
347
+ break;
348
+ case "fade":
349
+ element.classList.remove("fade-in");
350
+ element.classList.add("fade-active");
351
+ break;
352
+ case "scale":
353
+ element.classList.remove("scale-in");
354
+ element.classList.add("scale-active");
355
+ break;
356
+ case "flip":
357
+ element.classList.remove("flip-in");
358
+ element.classList.add("flip-active");
359
+ break;
360
+ }
361
+ });
362
+ // Fallback timeout
363
+ fallbackTimerId = window.setTimeout(() => {
364
+ log.trace("Transition fallback timeout triggered for animateIn");
365
+ cleanup();
366
+ }, duration + 50);
367
+ });
368
+ }
369
+ normalizeTransitionConfig(config) {
370
+ // If config is missing or invalid, fallback to snap
371
+ if (!config || !config.type) {
372
+ log.warn("Invalid transition config, falling back to snap navigation");
373
+ return { type: "snap" };
374
+ }
375
+ // For animated transitions, ensure duration is set
376
+ if (config.type !== "snap" && config.type !== "custom") {
377
+ if (!config.duration || config.duration <= 0) {
378
+ log.warn(`Invalid duration for ${config.type} transition, falling back to snap navigation`);
379
+ return { type: "snap" };
380
+ }
381
+ }
382
+ return config;
383
+ }
384
+ clearAnimationClasses(element) {
385
+ const animationClasses = [
386
+ "slide-out-left",
387
+ "slide-out-right",
388
+ "slide-out-up",
389
+ "slide-out-down",
390
+ "slide-in-left",
391
+ "slide-in-right",
392
+ "slide-in-up",
393
+ "slide-in-down",
394
+ "slide-active",
395
+ "fade-out",
396
+ "fade-in",
397
+ "fade-active",
398
+ "scale-out",
399
+ "scale-in",
400
+ "scale-active",
401
+ "flip-out",
402
+ "flip-in",
403
+ "flip-active",
404
+ ];
405
+ element.classList.remove(...animationClasses);
406
+ }
407
+ showPage(pageId) {
408
+ const page = this.pages.get(pageId);
409
+ if (page) {
410
+ const element = page.getHostElement();
411
+ element.style.display = ""; // Clear the inline display:none
412
+ element.classList.remove("nav-hidden", "out");
413
+ element.classList.add("in");
414
+ }
415
+ }
416
+ hidePage(pageId) {
417
+ const page = this.pages.get(pageId);
418
+ if (page) {
419
+ const element = page.getHostElement();
420
+ element.classList.add("nav-hidden");
421
+ // Set display none after a short delay to allow animation to complete
422
+ setTimeout(() => {
423
+ if (element.classList.contains("nav-hidden")) {
424
+ element.style.display = "none";
425
+ }
426
+ }, 50);
427
+ }
428
+ }
429
+ showView(viewId, animate = true) {
430
+ const view = this.views.get(viewId);
431
+ if (view) {
432
+ const element = view.getHostElement();
433
+ element.style.display = "block";
434
+ element.classList.remove("nav-hidden");
435
+ element.style.visibility = "visible";
436
+ if (!animate) {
437
+ element.style.opacity = "1";
438
+ element.style.transform = "none";
439
+ }
440
+ // When animating, let CSS animation classes control opacity entirely
441
+ // Don't set inline opacity as it would override CSS classes
442
+ }
443
+ }
444
+ hideView(viewId) {
445
+ const view = this.views.get(viewId);
446
+ if (view) {
447
+ const element = view.getHostElement();
448
+ element.classList.add("nav-hidden");
449
+ element.style.display = "none";
450
+ element.style.visibility = "hidden";
451
+ }
452
+ }
453
+ hideAllViews() {
454
+ // Hide all registered views to ensure clean state
455
+ this.views.forEach((_, viewId) => {
456
+ this.hideView(viewId);
457
+ });
458
+ log.trace("All views hidden");
459
+ }
460
+ getCurrentPageId() {
461
+ return stateManager.get("navigation.currentPageId");
462
+ }
463
+ getCurrentViewId() {
464
+ return stateManager.get("navigation.currentViewId");
465
+ }
466
+ isTransitioning() {
467
+ return stateManager.get("navigation.isTransitioning");
468
+ }
469
+ getRegisteredPages() {
470
+ return Array.from(this.pages.keys());
471
+ }
472
+ getRegisteredViews() {
473
+ return Array.from(this.views.keys());
474
+ }
475
+ // Convenience methods for external components to subscribe to navigation state
476
+ subscribeToCurrentPage(callback) {
477
+ return stateManager.subscribe("navigation.currentPageId", callback);
478
+ }
479
+ subscribeToCurrentView(callback) {
480
+ return stateManager.subscribe("navigation.currentViewId", callback);
481
+ }
482
+ subscribeToTransitionState(callback) {
483
+ return stateManager.subscribe("navigation.isTransitioning", callback);
484
+ }
485
+ /**
486
+ * Clean up any orphaned transition listeners
487
+ * This is a safety net for transitions that didn't complete properly
488
+ */
489
+ cleanupOrphanedTransitions() {
490
+ const orphanedCount = this.activeTransitionCleanups.size;
491
+ if (orphanedCount > 0) {
492
+ log.trace(`Cleaning up ${orphanedCount} orphaned transition(s)`);
493
+ this.activeTransitionCleanups.forEach((cleanup, element) => {
494
+ try {
495
+ cleanup();
496
+ }
497
+ catch (error) {
498
+ log.error(`Error cleaning up orphaned transition for element with ID ${element.id}:`, error);
499
+ }
500
+ });
501
+ this.activeTransitionCleanups.clear();
502
+ }
503
+ }
504
+ /**
505
+ * Get the count of active transitions
506
+ */
507
+ getActiveTransitionCount() {
508
+ return this.activeTransitionCleanups.size;
509
+ }
510
+ /**
511
+ * Cleanup method for proper resource management
512
+ *
513
+ * Destroys the NavigationManager instance and clears the singleton reference.
514
+ * After calling destroy(), a new NavigationManager instance can be created if needed
515
+ * (though this is typically only necessary during application teardown/restart).
516
+ */
517
+ destroy() {
518
+ // Clean up any active transitions
519
+ this.cleanupOrphanedTransitions();
520
+ // Clean up any state subscriptions
521
+ this.stateSubscriptions.forEach((subscription) => {
522
+ subscription.unsubscribe();
523
+ });
524
+ this.stateSubscriptions.length = 0;
525
+ // Clear local maps
526
+ this.pages.clear();
527
+ this.views.clear();
528
+ // Clear the singleton instance reference to allow new instance creation
529
+ NavigationManager.instance = null;
530
+ NavigationManager.isDestroyed = true;
531
+ log.trace("NavigationManager singleton instance destroyed and cleared");
532
+ }
533
+ }