snice 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,8 @@ Snice provides a clear separation of concerns through decorators:
37
37
  - **`@query`** - Queries a single element from shadow DOM
38
38
  - **`@queryAll`** - Queries multiple elements from shadow DOM
39
39
  - **`@watch`** - Watches property changes and calls a method when they occur
40
+ - **`@ready`** - Runs a method after the element's shadow DOM is ready
41
+ - **`@dispose`** - Runs a method when the element is removed from the DOM
40
42
 
41
43
  ### Event Decorators
42
44
  - **`@on`** - Listens for events on elements
@@ -559,8 +561,9 @@ class UserProfile extends HTMLElement {
559
561
  const user = await (yield { userId: 123 });
560
562
  return user;
561
563
  }
562
-
563
- async connectedCallback() {
564
+
565
+ @ready()
566
+ async load() {
564
567
  const userData = await this.getUser();
565
568
  this.displayUser(userData);
566
569
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snice",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "A TypeScript library",
6
6
  "main": "src/index.ts",
package/src/controller.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { setupEventHandlers, cleanupEventHandlers } from './events';
2
2
  import { setupChannelHandlers, cleanupChannelHandlers } from './channel';
3
- import { IS_CONTROLLER_CLASS, CONTROLLER_KEY, CONTROLLER_NAME_KEY, CONTROLLER_ID, CONTROLLER_OPERATIONS, NATIVE_CONTROLLER, IS_ELEMENT_CLASS, ROUTER_CONTEXT } from './symbols';
3
+ import { IS_CONTROLLER_CLASS, IS_CONTROLLER_INSTANCE, CONTROLLER_KEY, CONTROLLER_NAME_KEY, CONTROLLER_ID, CONTROLLER_OPERATIONS, NATIVE_CONTROLLER, IS_ELEMENT_CLASS, ROUTER_CONTEXT } from './symbols';
4
4
  import { snice } from './global';
5
5
 
6
6
  type Maybe<T> = T | null | undefined;
@@ -107,6 +107,8 @@ export async function attachController(element: HTMLElement, controllerName: str
107
107
  const controllerId = snice.controllerIdCounter;
108
108
  const scope = new ControllerScope();
109
109
 
110
+ // Mark this as a controller instance
111
+ (controllerInstance as any)[IS_CONTROLLER_INSTANCE] = true;
110
112
  (controllerInstance as any)[CONTROLLER_ID] = controllerId;
111
113
  controllerInstance.element = element;
112
114
 
package/src/element.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { attachController, detachController } from './controller';
2
2
  import { setupEventHandlers, cleanupEventHandlers } from './events';
3
- import { IS_ELEMENT_CLASS, READY_PROMISE, READY_RESOLVE, CONTROLLER, PROPERTIES, PROPERTY_VALUES, PROPERTIES_INITIALIZED, PROPERTY_WATCHERS, EXPLICITLY_SET_PROPERTIES, ROUTER_CONTEXT } from './symbols';
3
+ import { IS_ELEMENT_CLASS, IS_CONTROLLER_INSTANCE, READY_PROMISE, READY_RESOLVE, CONTROLLER, PROPERTIES, PROPERTY_VALUES, PROPERTIES_INITIALIZED, PROPERTY_WATCHERS, EXPLICITLY_SET_PROPERTIES, ROUTER_CONTEXT, READY_HANDLERS, DISPOSE_HANDLERS } from './symbols';
4
4
 
5
5
  /**
6
6
  * Applies core element functionality to a constructor
@@ -106,7 +106,7 @@ export function applyElementFunctionality(constructor: any) {
106
106
  this[EXPLICITLY_SET_PROPERTIES].add(propName);
107
107
 
108
108
  if (propOptions.type === Boolean) {
109
- this[propName] = attrValue !== null;
109
+ this[propName] = attrValue !== null && attrValue !== 'false';
110
110
  } else if (propOptions.type === Number) {
111
111
  this[propName] = Number(attrValue);
112
112
  } else {
@@ -193,6 +193,18 @@ export function applyElementFunctionality(constructor: any) {
193
193
  }
194
194
  // Setup @on event handlers - use element for host events, shadow root for delegated events
195
195
  setupEventHandlers(this, this);
196
+
197
+ // Call @ready handlers after everything is set up
198
+ const readyHandlers = constructor[READY_HANDLERS];
199
+ if (readyHandlers) {
200
+ for (const handler of readyHandlers) {
201
+ try {
202
+ await handler.method.call(this);
203
+ } catch (error) {
204
+ console.error(`Error in @ready handler ${handler.methodName}:`, error);
205
+ }
206
+ }
207
+ }
196
208
  } finally {
197
209
  // Always mark element as ready, even if there were errors
198
210
  if (this[READY_RESOLVE]) {
@@ -202,8 +214,20 @@ export function applyElementFunctionality(constructor: any) {
202
214
  }
203
215
  };
204
216
 
205
- constructor.prototype.disconnectedCallback = function() {
206
- // Call original user-defined disconnectedCallback first
217
+ constructor.prototype.disconnectedCallback = async function() {
218
+ // Call @dispose handlers
219
+ const disposeHandlers = constructor[DISPOSE_HANDLERS];
220
+ if (disposeHandlers) {
221
+ for (const handler of disposeHandlers) {
222
+ try {
223
+ await handler.method.call(this);
224
+ } catch (error) {
225
+ console.error(`Error in @dispose handler ${handler.methodName}:`, error);
226
+ }
227
+ }
228
+ }
229
+
230
+ // Call original user-defined disconnectedCallback
207
231
  if (originalDisconnectedCallback) {
208
232
  originalDisconnectedCallback.call(this);
209
233
  }
@@ -234,7 +258,7 @@ export function applyElementFunctionality(constructor: any) {
234
258
  // Parse the new value based on type
235
259
  let parsedValue: any;
236
260
  if (propOptions.type === Boolean) {
237
- parsedValue = newValue !== null;
261
+ parsedValue = newValue !== null && newValue !== 'false';
238
262
  } else if (propOptions.type === Number) {
239
263
  parsedValue = Number(newValue);
240
264
  } else {
@@ -370,17 +394,34 @@ export function property(options?: PropertyOptions) {
370
394
  };
371
395
  }
372
396
 
373
- export function query(selector: string) {
397
+ export interface QueryOptions {
398
+ light?: boolean;
399
+ shadow?: boolean;
400
+ }
401
+
402
+ export function query(selector: string, options: QueryOptions = {}) {
374
403
  return function (target: any, propertyKey: string) {
404
+ // Default to shadow DOM only
405
+ const { light = false, shadow = true } = options;
406
+
375
407
  Object.defineProperty(target, propertyKey, {
376
408
  get() {
377
- // For elements with shadow DOM, query within shadow root
378
- // For controllers, check the element's shadow root first
379
- const root = this.element || this;
380
- if (root.shadowRoot) {
381
- return root.shadowRoot.querySelector(selector);
409
+ // Check if this is a controller using the symbol
410
+ const isController = this[IS_CONTROLLER_INSTANCE] === true;
411
+ const root = isController && this.element ? this.element : this;
412
+
413
+ // Query in specified contexts
414
+ let result = null;
415
+
416
+ if (shadow && root.shadowRoot) {
417
+ result = root.shadowRoot.querySelector(selector);
418
+ }
419
+
420
+ if (!result && light) {
421
+ result = root.querySelector(selector);
382
422
  }
383
- return root.querySelector(selector);
423
+
424
+ return result || null;
384
425
  },
385
426
  enumerable: true,
386
427
  configurable: true,
@@ -388,17 +429,32 @@ export function query(selector: string) {
388
429
  };
389
430
  }
390
431
 
391
- export function queryAll(selector: string) {
432
+ export function queryAll(selector: string, options: QueryOptions = {}) {
392
433
  return function (target: any, propertyKey: string) {
434
+ // Default to shadow DOM only
435
+ const { light = false, shadow = true } = options;
436
+
393
437
  Object.defineProperty(target, propertyKey, {
394
438
  get() {
395
- // For elements with shadow DOM, query within shadow root
396
- // For controllers, check the element's shadow root first
397
- const root = this.element || this;
398
- if (root.shadowRoot) {
399
- return root.shadowRoot.querySelectorAll(selector);
439
+ // Check if this is a controller using the symbol
440
+ const isController = this[IS_CONTROLLER_INSTANCE] === true;
441
+ const root = isController && this.element ? this.element : this;
442
+
443
+ // Query in specified contexts and combine results
444
+ const results: Element[] = [];
445
+
446
+ if (shadow && root.shadowRoot) {
447
+ const shadowResults = root.shadowRoot.querySelectorAll(selector);
448
+ results.push(...shadowResults);
449
+ }
450
+
451
+ if (light) {
452
+ const lightResults = root.querySelectorAll(selector);
453
+ results.push(...lightResults);
400
454
  }
401
- return root.querySelectorAll(selector);
455
+
456
+ // Return a static NodeList-like object
457
+ return results as any as NodeListOf<Element>;
402
458
  },
403
459
  enumerable: true,
404
460
  configurable: true,
@@ -495,4 +551,47 @@ export function context() {
495
551
  configurable: true
496
552
  });
497
553
  };
554
+ }
555
+
556
+ /**
557
+ * Decorator for methods that should run when element is ready
558
+ * Runs after shadow DOM, controller attachment, and event setup
559
+ * Supports async methods
560
+ */
561
+ export function ready() {
562
+ return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
563
+ const constructor = target.constructor;
564
+
565
+ if (!constructor[READY_HANDLERS]) {
566
+ constructor[READY_HANDLERS] = [];
567
+ }
568
+
569
+ constructor[READY_HANDLERS].push({
570
+ methodName,
571
+ method: descriptor.value
572
+ });
573
+
574
+ return descriptor;
575
+ };
576
+ }
577
+
578
+ /**
579
+ * Decorator for methods that should run when element is being disposed
580
+ * Used for cleanup tasks when element is removed from DOM
581
+ */
582
+ export function dispose() {
583
+ return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
584
+ const constructor = target.constructor;
585
+
586
+ if (!constructor[DISPOSE_HANDLERS]) {
587
+ constructor[DISPOSE_HANDLERS] = [];
588
+ }
589
+
590
+ constructor[DISPOSE_HANDLERS].push({
591
+ methodName,
592
+ method: descriptor.value
593
+ });
594
+
595
+ return descriptor;
596
+ };
498
597
  }
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
- export { element, customElement, property, query, queryAll, watch, context, applyElementFunctionality } from './element';
1
+ export { element, customElement, property, query, queryAll, watch, context, applyElementFunctionality, ready, dispose } from './element';
2
2
  export { Router } from './router';
3
3
  export { controller, attachController, detachController, getController, useNativeElementControllers, cleanupNativeElementControllers } from './controller';
4
4
  export { on, dispatch } from './events';
5
5
  export { channel } from './channel';
6
- export type { PropertyOptions, PropertyConverter } from './element';
7
- export type { RouterOptions, PageOptions, PageTransition, Guard, RouteParams, RouterInstance } from './router';
6
+ export { IS_CONTROLLER_INSTANCE } from './symbols';
7
+ export type { Transition } from './transitions';
8
+ export type { PropertyOptions, PropertyConverter, QueryOptions } from './element';
9
+ export type { RouterOptions, PageOptions, Guard, RouteParams, RouterInstance } from './router';
8
10
  export type { IController, ControllerClass } from './controller';
9
11
  export type { DispatchOptions } from './events';
10
12
  export type { ChannelOptions } from './channel';
package/src/router.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import Route from 'route-parser';
2
2
  import { applyElementFunctionality } from './element';
3
- import { ROUTER_CONTEXT, CONTEXT_REQUEST_HANDLER } from './symbols';
3
+ import { ROUTER_CONTEXT, CONTEXT_REQUEST_HANDLER, PAGE_TRANSITION } from './symbols';
4
+ import { Transition, performTransition as performTransitionUtil } from './transitions';
4
5
 
5
6
  /**
6
7
  * Route parameters extracted from the URL
@@ -39,7 +40,7 @@ export interface RouterOptions {
39
40
  /**
40
41
  * Global transition configuration for all pages
41
42
  */
42
- transition?: PageTransition;
43
+ transition?: Transition;
43
44
 
44
45
  /**
45
46
  * Optional context object passed to guard functions
@@ -47,38 +48,6 @@ export interface RouterOptions {
47
48
  context?: any;
48
49
  }
49
50
 
50
- export interface PageTransition {
51
- /**
52
- * Name of the transition (for CSS class naming)
53
- */
54
- name?: string;
55
-
56
- /**
57
- * Duration of the out transition in ms
58
- */
59
- outDuration?: number;
60
-
61
- /**
62
- * Duration of the in transition in ms
63
- */
64
- inDuration?: number;
65
-
66
- /**
67
- * CSS classes or styles for the out transition
68
- */
69
- out?: string;
70
-
71
- /**
72
- * CSS classes or styles for the in transition
73
- */
74
- in?: string;
75
-
76
- /**
77
- * Mode: 'sequential' (out then in) or 'simultaneous' (both at once)
78
- */
79
- mode?: 'sequential' | 'simultaneous';
80
- }
81
-
82
51
  export interface PageOptions {
83
52
  /**
84
53
  * The tag name of the custom element.
@@ -96,7 +65,7 @@ export interface PageOptions {
96
65
  /**
97
66
  * Optional per-page transition override
98
67
  */
99
- transition?: PageTransition;
68
+ transition?: Transition;
100
69
 
101
70
  /**
102
71
  * Guard functions that must pass for navigation to proceed.
@@ -112,7 +81,7 @@ export interface RouterInstance {
112
81
  page: (pageOptions: PageOptions) => <C extends { new(...args: any[]): HTMLElement }>(constructor: C) => void;
113
82
  initialize: () => void;
114
83
  navigate: (path: string) => Promise<void>;
115
- register: (route: string, tag: string, transition?: PageTransition, guards?: Guard<any> | Guard<any>[]) => void;
84
+ register: (route: string, tag: string, transition?: Transition, guards?: Guard<any> | Guard<any>[]) => void;
116
85
  context: any;
117
86
  }
118
87
 
@@ -122,7 +91,7 @@ export interface RouterInstance {
122
91
  * @returns An object containing the router's API methods.
123
92
  */
124
93
  export function Router(options: RouterOptions): RouterInstance {
125
- const routes: { route: Route, tag: string, transition?: PageTransition, guards?: Guard<any> | Guard<any>[] }[] = [];
94
+ const routes: { route: Route, tag: string, transition?: Transition, guards?: Guard<any> | Guard<any>[] }[] = [];
126
95
  let is_sorted = false;
127
96
 
128
97
  let _404: string; // the 404 page
@@ -142,7 +111,7 @@ export function Router(options: RouterOptions): RouterInstance {
142
111
  applyElementFunctionality(constructor);
143
112
 
144
113
  // Store transition config on constructor for later use
145
- (constructor as any).__transition = pageOptions.transition;
114
+ (constructor as any)[PAGE_TRANSITION] = pageOptions.transition;
146
115
 
147
116
  // Extend the connectedCallback to add router-specific functionality
148
117
  const elementConnectedCallback = constructor.prototype.connectedCallback;
@@ -196,7 +165,7 @@ export function Router(options: RouterOptions): RouterInstance {
196
165
  * @example
197
166
  * register('/custom-route', 'custom-element');
198
167
  */
199
- function register(route: string, tag: string, transition?: PageTransition, guards?: Guard<any> | Guard<any>[]): void {
168
+ function register(route: string, tag: string, transition?: Transition, guards?: Guard<any> | Guard<any>[]): void {
200
169
  routes.push({ route: new Route(route), tag, transition, guards });
201
170
  is_sorted = false;
202
171
 
@@ -277,7 +246,7 @@ export function Router(options: RouterOptions): RouterInstance {
277
246
  }
278
247
 
279
248
  let newPageElement: HTMLElement | null = null;
280
- let transition: PageTransition | undefined;
249
+ let transition: Transition | undefined;
281
250
  let guards: Guard<any> | Guard<any>[] | undefined;
282
251
 
283
252
  // Home
@@ -318,7 +287,7 @@ export function Router(options: RouterOptions): RouterInstance {
318
287
  // Store context on the page element
319
288
  (newPageElement as any)[ROUTER_CONTEXT] = context;
320
289
  const constructor = customElements.get(home);
321
- transition = (constructor as any)?.__transition;
290
+ transition = (constructor as any)?.[PAGE_TRANSITION];
322
291
  } else {
323
292
 
324
293
  // Get the current route
@@ -372,7 +341,7 @@ export function Router(options: RouterOptions): RouterInstance {
372
341
  // Store context on 404 page too
373
342
  (newPageElement as any)[ROUTER_CONTEXT] = context;
374
343
  const constructor = customElements.get(_404);
375
- transition = (constructor as any)?.__transition;
344
+ transition = (constructor as any)?.[PAGE_TRANSITION];
376
345
  } else {
377
346
  // Provide a default 404 page
378
347
  const div = document.createElement('div');
@@ -403,83 +372,9 @@ export function Router(options: RouterOptions): RouterInstance {
403
372
  container: Element,
404
373
  oldElement: HTMLElement,
405
374
  newElement: HTMLElement,
406
- transition: PageTransition
375
+ transition: Transition
407
376
  ): Promise<void> {
408
- const outDuration = transition.outDuration || 300;
409
- const inDuration = transition.inDuration || 300;
410
- const mode = transition.mode || 'sequential';
411
-
412
- // Parse CSS properties from transition config
413
- const parseStyles = (styleString: string): Record<string, string> => {
414
- const styles: Record<string, string> = {};
415
- styleString.split(';').forEach(rule => {
416
- const [prop, value] = rule.split(':').map(s => s.trim());
417
- if (prop && value) {
418
- styles[prop] = value;
419
- }
420
- });
421
- return styles;
422
- };
423
-
424
- // Default transitions
425
- const outStyles = transition.out ? parseStyles(transition.out) : { opacity: '0' };
426
- const inStartStyles = { opacity: '0' }; // Always start invisible
427
- const inEndStyles = transition.in ? parseStyles(transition.in) : { opacity: '1' };
428
-
429
- // Set container to relative positioning to allow absolute positioning of pages
430
- const containerStyle = (container as HTMLElement).style;
431
- const originalPosition = containerStyle.position;
432
- containerStyle.position = 'relative';
433
-
434
- // Style old element for transition
435
- oldElement.style.position = 'absolute';
436
- oldElement.style.top = '0';
437
- oldElement.style.left = '0';
438
- oldElement.style.width = '100%';
439
- oldElement.style.transition = `all ${outDuration}ms ease-in-out`;
440
-
441
- // Style new element with initial state
442
- newElement.style.position = 'absolute';
443
- newElement.style.top = '0';
444
- newElement.style.left = '0';
445
- newElement.style.width = '100%';
446
- Object.assign(newElement.style, inStartStyles);
447
- newElement.style.transition = `all ${inDuration}ms ease-in-out`;
448
-
449
- // Add new element to container
450
- container.appendChild(newElement);
451
-
452
- // Force browser to calculate styles
453
- void newElement.offsetHeight;
454
-
455
- if (mode === 'simultaneous') {
456
- // Start both transitions at once
457
- Object.assign(oldElement.style, outStyles);
458
- Object.assign(newElement.style, inEndStyles);
459
-
460
- // Wait for both transitions to complete
461
- await new Promise(resolve => setTimeout(resolve, Math.max(outDuration, inDuration)));
462
- } else {
463
- // Sequential: transition out old, then transition in new
464
- Object.assign(oldElement.style, outStyles);
465
- await new Promise(resolve => setTimeout(resolve, outDuration));
466
-
467
- Object.assign(newElement.style, inEndStyles);
468
- await new Promise(resolve => setTimeout(resolve, inDuration));
469
- }
470
-
471
- // Cleanup
472
- oldElement.remove();
473
- newElement.style.position = '';
474
- newElement.style.top = '';
475
- newElement.style.left = '';
476
- newElement.style.width = '';
477
- newElement.style.transition = '';
478
- // Reset any transition styles
479
- Object.keys({...inStartStyles, ...inEndStyles}).forEach(prop => {
480
- newElement.style[prop as any] = '';
481
- });
482
- containerStyle.position = originalPosition;
377
+ return performTransitionUtil(container, oldElement, newElement, transition);
483
378
  }
484
379
 
485
380
  return {
package/src/symbols.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import { getSymbol } from './global';
5
5
 
6
6
  export const IS_CONTROLLER_CLASS = getSymbol('is-controller-class');
7
+ export const IS_CONTROLLER_INSTANCE = getSymbol('is-controller-instance');
7
8
  export const IS_ELEMENT_CLASS = getSymbol('is-element-class');
8
9
  export const CHANNEL_HANDLERS = getSymbol('channel-handlers');
9
10
 
@@ -34,4 +35,9 @@ export const EXPLICITLY_SET_PROPERTIES = getSymbol('explicitly-set-properties');
34
35
 
35
36
  // Router context symbol
36
37
  export const ROUTER_CONTEXT = getSymbol('router-context');
37
- export const CONTEXT_REQUEST_HANDLER = getSymbol('context-request-handler');
38
+ export const CONTEXT_REQUEST_HANDLER = getSymbol('context-request-handler');
39
+ export const PAGE_TRANSITION = getSymbol('page-transition');
40
+
41
+ // Lifecycle symbols
42
+ export const READY_HANDLERS = getSymbol('ready-handlers');
43
+ export const DISPOSE_HANDLERS = getSymbol('dispose-handlers');
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Generic transition system for animating between elements
3
+ */
4
+
5
+ export interface Transition {
6
+ /**
7
+ * Name of the transition (for identification)
8
+ */
9
+ name?: string;
10
+
11
+ /**
12
+ * Duration of the out transition in ms
13
+ */
14
+ outDuration?: number;
15
+
16
+ /**
17
+ * Duration of the in transition in ms
18
+ */
19
+ inDuration?: number;
20
+
21
+ /**
22
+ * CSS properties for the out transition (as string)
23
+ * Example: 'opacity: 0; transform: scale(0.9)'
24
+ */
25
+ out?: string;
26
+
27
+ /**
28
+ * CSS properties for the in transition (as string)
29
+ * Example: 'opacity: 1; transform: scale(1)'
30
+ */
31
+ in?: string;
32
+
33
+ /**
34
+ * Transition mode:
35
+ * - 'sequential': out transition completes before in transition starts
36
+ * - 'simultaneous': both transitions happen at the same time
37
+ */
38
+ mode?: 'sequential' | 'simultaneous';
39
+ }
40
+
41
+ /**
42
+ * Parse CSS property string into an object
43
+ */
44
+ function parseStyles(styleString: string): Record<string, string> {
45
+ const styles: Record<string, string> = {};
46
+ styleString.split(';').forEach(rule => {
47
+ const [prop, value] = rule.split(':').map(s => s.trim());
48
+ if (prop && value) {
49
+ styles[prop] = value;
50
+ }
51
+ });
52
+ return styles;
53
+ }
54
+
55
+ /**
56
+ * Perform a transition between two elements
57
+ */
58
+ export async function performTransition(
59
+ container: Element,
60
+ oldElement: HTMLElement,
61
+ newElement: HTMLElement,
62
+ transition: Transition = {}
63
+ ): Promise<void> {
64
+ const outDuration = transition.outDuration || 300;
65
+ const inDuration = transition.inDuration || 300;
66
+ const mode = transition.mode || 'sequential';
67
+
68
+ // Default transitions
69
+ const outStyles = transition.out ? parseStyles(transition.out) : { opacity: '0' };
70
+ const inStartStyles = { opacity: '0' }; // Always start invisible
71
+ const inEndStyles = transition.in ? parseStyles(transition.in) : { opacity: '1' };
72
+
73
+ // Set container to relative positioning to allow absolute positioning
74
+ const containerStyle = (container as HTMLElement).style;
75
+ const originalPosition = containerStyle.position;
76
+ containerStyle.position = 'relative';
77
+
78
+ // Style old element for transition
79
+ oldElement.style.position = 'absolute';
80
+ oldElement.style.top = '0';
81
+ oldElement.style.left = '0';
82
+ oldElement.style.width = '100%';
83
+ oldElement.style.transition = `all ${outDuration}ms ease-in-out`;
84
+
85
+ // Style new element with initial state
86
+ newElement.style.position = 'absolute';
87
+ newElement.style.top = '0';
88
+ newElement.style.left = '0';
89
+ newElement.style.width = '100%';
90
+ Object.assign(newElement.style, inStartStyles);
91
+ newElement.style.transition = `all ${inDuration}ms ease-in-out`;
92
+
93
+ // Add new element to container
94
+ container.appendChild(newElement);
95
+
96
+ // Force browser to calculate styles
97
+ void newElement.offsetHeight;
98
+
99
+ if (mode === 'simultaneous') {
100
+ // Start both transitions at once
101
+ Object.assign(oldElement.style, outStyles);
102
+ Object.assign(newElement.style, inEndStyles);
103
+
104
+ // Wait for both transitions to complete
105
+ await new Promise(resolve => setTimeout(resolve, Math.max(outDuration, inDuration)));
106
+ } else {
107
+ // Sequential: transition out old, then transition in new
108
+ Object.assign(oldElement.style, outStyles);
109
+ await new Promise(resolve => setTimeout(resolve, outDuration));
110
+
111
+ Object.assign(newElement.style, inEndStyles);
112
+ await new Promise(resolve => setTimeout(resolve, inDuration));
113
+ }
114
+
115
+ // Cleanup
116
+ oldElement.remove();
117
+ newElement.style.position = '';
118
+ newElement.style.top = '';
119
+ newElement.style.left = '';
120
+ newElement.style.width = '';
121
+ newElement.style.transition = '';
122
+ // Reset any transition styles
123
+ Object.keys({...inStartStyles, ...inEndStyles}).forEach(prop => {
124
+ newElement.style[prop as any] = '';
125
+ });
126
+ containerStyle.position = originalPosition;
127
+ }
128
+
129
+ /**
130
+ * Predefined transitions
131
+ */
132
+
133
+ // Fade transition
134
+ export const fadeTransition: Transition = {
135
+ name: 'fade',
136
+ outDuration: 200,
137
+ inDuration: 200,
138
+ out: 'opacity: 0',
139
+ in: 'opacity: 1',
140
+ mode: 'simultaneous'
141
+ };
142
+
143
+ // Slide transition (from left)
144
+ export const slideTransition: Transition = {
145
+ name: 'slide',
146
+ outDuration: 300,
147
+ inDuration: 300,
148
+ out: 'transform: translateX(-100%)',
149
+ in: 'transform: translateX(0)',
150
+ mode: 'sequential'
151
+ };
152
+
153
+ // Slide from right
154
+ export const slideRightTransition: Transition = {
155
+ name: 'slide-right',
156
+ outDuration: 300,
157
+ inDuration: 300,
158
+ out: 'transform: translateX(100%)',
159
+ in: 'transform: translateX(0)',
160
+ mode: 'sequential'
161
+ };
162
+
163
+ // Slide from top
164
+ export const slideUpTransition: Transition = {
165
+ name: 'slide-up',
166
+ outDuration: 300,
167
+ inDuration: 300,
168
+ out: 'transform: translateY(-100%)',
169
+ in: 'transform: translateY(0)',
170
+ mode: 'sequential'
171
+ };
172
+
173
+ // Slide from bottom
174
+ export const slideDownTransition: Transition = {
175
+ name: 'slide-down',
176
+ outDuration: 300,
177
+ inDuration: 300,
178
+ out: 'transform: translateY(100%)',
179
+ in: 'transform: translateY(0)',
180
+ mode: 'sequential'
181
+ };
182
+
183
+ // Scale transition
184
+ export const scaleTransition: Transition = {
185
+ name: 'scale',
186
+ outDuration: 250,
187
+ inDuration: 250,
188
+ out: 'transform: scale(0.9); opacity: 0',
189
+ in: 'transform: scale(1); opacity: 1',
190
+ mode: 'simultaneous'
191
+ };
192
+
193
+ // Rotate transition
194
+ export const rotateTransition: Transition = {
195
+ name: 'rotate',
196
+ outDuration: 400,
197
+ inDuration: 400,
198
+ out: 'transform: rotate(180deg) scale(0.5); opacity: 0',
199
+ in: 'transform: rotate(0) scale(1); opacity: 1',
200
+ mode: 'simultaneous'
201
+ };
202
+
203
+ // Flip transition
204
+ export const flipTransition: Transition = {
205
+ name: 'flip',
206
+ outDuration: 400,
207
+ inDuration: 400,
208
+ out: 'transform: rotateY(180deg); opacity: 0',
209
+ in: 'transform: rotateY(0); opacity: 1',
210
+ mode: 'sequential'
211
+ };
212
+
213
+ // Zoom transition
214
+ export const zoomTransition: Transition = {
215
+ name: 'zoom',
216
+ outDuration: 300,
217
+ inDuration: 300,
218
+ out: 'transform: scale(2); opacity: 0',
219
+ in: 'transform: scale(1); opacity: 1',
220
+ mode: 'simultaneous'
221
+ };
222
+
223
+ // None transition (instant swap)
224
+ export const noneTransition: Transition = {
225
+ name: 'none',
226
+ outDuration: 0,
227
+ inDuration: 0,
228
+ out: '',
229
+ in: '',
230
+ mode: 'simultaneous'
231
+ };