@capgo/capacitor-transitions 8.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.
@@ -0,0 +1,2210 @@
1
+ // src/core/animations.ts
2
+ var IOS_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
3
+ var ANDROID_EASING = "cubic-bezier(0.36, 0.66, 0.04, 1)";
4
+ var ANDROID_BACK_EASING = "cubic-bezier(0.47, 0, 0.745, 0.715)";
5
+ var IOS_DURATION = 540;
6
+ var ANDROID_DURATION = 280;
7
+ var ANDROID_BACK_DURATION = 200;
8
+ var IOS_OFF_OPACITY = 0.8;
9
+ var IOS_CENTER = "0%";
10
+ var IOS_OFF_RIGHT = "99.5%";
11
+ var IOS_OFF_LEFT = "-33%";
12
+ var IOS_OFF_RIGHT_RTL = "-99.5%";
13
+ var IOS_OFF_LEFT_RTL = "33%";
14
+ var MD_OFF_BOTTOM = "40px";
15
+ var MD_CENTER = "0px";
16
+ function resolveEasing(easing) {
17
+ switch (easing) {
18
+ case "ios":
19
+ return IOS_EASING;
20
+ case "android":
21
+ return ANDROID_EASING;
22
+ case "linear":
23
+ return "linear";
24
+ case "ease":
25
+ return "ease";
26
+ case "ease-in":
27
+ return "ease-in";
28
+ case "ease-out":
29
+ return "ease-out";
30
+ case "ease-in-out":
31
+ return "ease-in-out";
32
+ default:
33
+ return easing;
34
+ }
35
+ }
36
+ function detectPlatform() {
37
+ if (typeof navigator === "undefined") return "ios";
38
+ const ua = navigator.userAgent.toLowerCase();
39
+ if (/iphone|ipad|ipod/.test(ua)) return "ios";
40
+ if (/android/.test(ua)) return "android";
41
+ return "ios";
42
+ }
43
+ function getDefaultDuration(platform, direction = "forward") {
44
+ const resolved = platform === "auto" ? detectPlatform() : platform;
45
+ if (resolved === "ios") {
46
+ return IOS_DURATION;
47
+ }
48
+ return direction === "back" ? ANDROID_BACK_DURATION : ANDROID_DURATION;
49
+ }
50
+ function getDefaultEasing(platform, direction = "forward") {
51
+ const resolved = platform === "auto" ? detectPlatform() : platform;
52
+ if (resolved === "ios") {
53
+ return IOS_EASING;
54
+ }
55
+ return direction === "back" ? ANDROID_BACK_EASING : ANDROID_EASING;
56
+ }
57
+ function getDocumentDirection(element) {
58
+ const doc = element.ownerDocument;
59
+ return doc.dir === "rtl" || doc.documentElement.dir === "rtl" ? "rtl" : "ltr";
60
+ }
61
+ function preparePageLayer(element, zIndex) {
62
+ element.classList.add("cap-transition-active");
63
+ element.style.display = "";
64
+ element.style.visibility = "visible";
65
+ element.style.position = "absolute";
66
+ element.style.top = "0";
67
+ element.style.left = "0";
68
+ element.style.width = "100%";
69
+ element.style.height = "100%";
70
+ element.style.zIndex = zIndex;
71
+ element.style.pointerEvents = "none";
72
+ element.style.willChange = "transform, opacity";
73
+ element.style.backfaceVisibility = "hidden";
74
+ element.style.transformStyle = "preserve-3d";
75
+ }
76
+ function createAnimation(element, keyframes, duration, easing) {
77
+ return element.animate(keyframes, {
78
+ duration,
79
+ easing,
80
+ fill: "both"
81
+ });
82
+ }
83
+ function resolvePageContents(element) {
84
+ return Array.from(
85
+ element.querySelectorAll(
86
+ ':scope > [data-cap-content], :scope > .cap-content, :scope > cap-content, :scope > [slot="content"]'
87
+ )
88
+ );
89
+ }
90
+ function resolvePageChrome(element) {
91
+ return Array.from(
92
+ element.querySelectorAll(
93
+ ':scope > [data-cap-header], :scope > .cap-header, :scope > cap-header, :scope > [slot="header"], :scope > [data-cap-footer], :scope > .cap-footer, :scope > cap-footer, :scope > [slot="footer"]'
94
+ )
95
+ );
96
+ }
97
+ function resolvePageHeaders(element) {
98
+ return Array.from(
99
+ element.querySelectorAll(
100
+ ':scope > [data-cap-header], :scope > .cap-header, :scope > cap-header, :scope > [slot="header"]'
101
+ )
102
+ );
103
+ }
104
+ function invertTranslateOffset(offset) {
105
+ if (offset === IOS_CENTER) {
106
+ return IOS_CENTER;
107
+ }
108
+ return offset.startsWith("-") ? offset.slice(1) : `-${offset}`;
109
+ }
110
+ function createPinnedChromeAnimations(elements, fromTransform, toTransform, duration, easing) {
111
+ return elements.map((element) => {
112
+ element.style.willChange = "transform";
113
+ element.style.backfaceVisibility = "hidden";
114
+ return createAnimation(element, [{ transform: fromTransform }, { transform: toTransform }], duration, easing);
115
+ });
116
+ }
117
+ function uniqueElements(elements) {
118
+ return Array.from(new Set(elements));
119
+ }
120
+ function resolveToolbarItems(header) {
121
+ const toolbarItems = Array.from(
122
+ header.querySelectorAll(
123
+ ':scope > [data-cap-toolbar] > *, :scope > .toolbar > *, :scope > [role="toolbar"] > *, :scope > ion-toolbar > *'
124
+ )
125
+ );
126
+ const directItems = Array.from(
127
+ header.querySelectorAll(
128
+ ":scope > [data-cap-toolbar-item], :scope > .cap-toolbar-item, :scope > h1, :scope > h2, :scope > h3, :scope > button, :scope > a"
129
+ )
130
+ );
131
+ return uniqueElements(
132
+ [...toolbarItems, ...directItems].filter(
133
+ (element) => !element.matches('[data-cap-toolbar-background], .toolbar-background, [aria-hidden="true"]') && element.tagName !== "STYLE" && element.tagName !== "SCRIPT"
134
+ )
135
+ );
136
+ }
137
+ function createOpacityAnimations(elements, fromOpacity, toOpacity, duration, easing) {
138
+ return elements.map((element) => {
139
+ element.style.willChange = "opacity";
140
+ element.style.backfaceVisibility = "hidden";
141
+ return createAnimation(element, [{ opacity: fromOpacity }, { opacity: toOpacity }], duration, easing);
142
+ });
143
+ }
144
+ function createToolbarItemAnimations(headers, fromTransform, toTransform, fromOpacity, toOpacity, duration, easing) {
145
+ return headers.flatMap(
146
+ (header) => resolveToolbarItems(header).map((element) => {
147
+ element.style.willChange = "transform, opacity";
148
+ element.style.backfaceVisibility = "hidden";
149
+ return createAnimation(
150
+ element,
151
+ [
152
+ { transform: fromTransform, opacity: fromOpacity },
153
+ { transform: toTransform, opacity: toOpacity }
154
+ ],
155
+ duration,
156
+ easing
157
+ );
158
+ })
159
+ );
160
+ }
161
+ function createIOSTransition(options) {
162
+ const { enteringEl, leavingEl, direction, duration, easing } = options;
163
+ const animations = [];
164
+ const isBack = direction === "back";
165
+ const isRoot = direction === "root";
166
+ const isRTL = getDocumentDirection(enteringEl) === "rtl";
167
+ const offRight = isRTL ? IOS_OFF_RIGHT_RTL : IOS_OFF_RIGHT;
168
+ const offLeft = isRTL ? IOS_OFF_LEFT_RTL : IOS_OFF_LEFT;
169
+ const chromeOffRight = invertTranslateOffset(offRight);
170
+ const chromeOffLeft = invertTranslateOffset(offLeft);
171
+ const leadingEdgeShadow = isRTL ? "8px 0 24px rgba(0, 0, 0, 0.18)" : "-8px 0 24px rgba(0, 0, 0, 0.18)";
172
+ const enteringContent = resolvePageContents(enteringEl);
173
+ const enteringHeaders = resolvePageHeaders(enteringEl);
174
+ const leavingContent = leavingEl ? resolvePageContents(leavingEl) : [];
175
+ const leavingHeaders = leavingEl ? resolvePageHeaders(leavingEl) : [];
176
+ preparePageLayer(enteringEl, isBack ? "99" : "101");
177
+ if (leavingEl) {
178
+ preparePageLayer(leavingEl, "100");
179
+ }
180
+ if (isRoot) {
181
+ animations.push(
182
+ createAnimation(
183
+ enteringEl,
184
+ [
185
+ { opacity: 0.01, transform: "translate3d(0, 0, 0)" },
186
+ { opacity: 1, transform: "translate3d(0, 0, 0)" }
187
+ ],
188
+ duration,
189
+ easing
190
+ )
191
+ );
192
+ if (leavingEl) {
193
+ animations.push(
194
+ createAnimation(
195
+ leavingEl,
196
+ [
197
+ { opacity: 1, transform: "translate3d(0, 0, 0)" },
198
+ { opacity: 0, transform: "translate3d(0, 0, 0)" }
199
+ ],
200
+ Math.min(duration, 240),
201
+ easing
202
+ )
203
+ );
204
+ }
205
+ } else if (isBack) {
206
+ animations.push(
207
+ ...createPinnedChromeAnimations(
208
+ resolvePageChrome(enteringEl),
209
+ `translate3d(${chromeOffLeft}, 0, 0)`,
210
+ `translate3d(${IOS_CENTER}, 0, 0)`,
211
+ duration,
212
+ easing
213
+ )
214
+ );
215
+ animations.push(
216
+ createAnimation(
217
+ enteringEl,
218
+ [{ transform: `translate3d(${offLeft}, 0, 0)` }, { transform: `translate3d(${IOS_CENTER}, 0, 0)` }],
219
+ duration,
220
+ easing
221
+ )
222
+ );
223
+ animations.push(...createOpacityAnimations(enteringContent, IOS_OFF_OPACITY, 1, duration, easing));
224
+ animations.push(
225
+ ...createToolbarItemAnimations(
226
+ enteringHeaders,
227
+ `translate3d(${offLeft}, 0, 0)`,
228
+ `translate3d(${IOS_CENTER}, 0, 0)`,
229
+ 0.01,
230
+ 1,
231
+ duration,
232
+ easing
233
+ )
234
+ );
235
+ if (leavingEl) {
236
+ leavingEl.style.boxShadow = leadingEdgeShadow;
237
+ animations.push(
238
+ ...createPinnedChromeAnimations(
239
+ resolvePageChrome(leavingEl),
240
+ `translate3d(${IOS_CENTER}, 0, 0)`,
241
+ `translate3d(${chromeOffRight}, 0, 0)`,
242
+ duration,
243
+ easing
244
+ )
245
+ );
246
+ animations.push(
247
+ createAnimation(
248
+ leavingEl,
249
+ [{ transform: `translate3d(${IOS_CENTER}, 0, 0)` }, { transform: `translate3d(${offRight}, 0, 0)` }],
250
+ duration,
251
+ easing
252
+ )
253
+ );
254
+ animations.push(
255
+ ...createToolbarItemAnimations(
256
+ leavingHeaders,
257
+ `translate3d(${IOS_CENTER}, 0, 0)`,
258
+ `translate3d(${offRight}, 0, 0)`,
259
+ 0.99,
260
+ 0,
261
+ duration,
262
+ easing
263
+ )
264
+ );
265
+ }
266
+ } else {
267
+ enteringEl.style.boxShadow = leadingEdgeShadow;
268
+ animations.push(
269
+ ...createPinnedChromeAnimations(
270
+ resolvePageChrome(enteringEl),
271
+ `translate3d(${chromeOffRight}, 0, 0)`,
272
+ `translate3d(${IOS_CENTER}, 0, 0)`,
273
+ duration,
274
+ easing
275
+ )
276
+ );
277
+ animations.push(
278
+ createAnimation(
279
+ enteringEl,
280
+ [{ transform: `translate3d(${offRight}, 0, 0)` }, { transform: `translate3d(${IOS_CENTER}, 0, 0)` }],
281
+ duration,
282
+ easing
283
+ )
284
+ );
285
+ animations.push(
286
+ ...createToolbarItemAnimations(
287
+ enteringHeaders,
288
+ `translate3d(${offRight}, 0, 0)`,
289
+ `translate3d(${IOS_CENTER}, 0, 0)`,
290
+ 0.01,
291
+ 1,
292
+ duration,
293
+ easing
294
+ )
295
+ );
296
+ if (leavingEl) {
297
+ animations.push(
298
+ ...createPinnedChromeAnimations(
299
+ resolvePageChrome(leavingEl),
300
+ `translate3d(${IOS_CENTER}, 0, 0)`,
301
+ `translate3d(${chromeOffLeft}, 0, 0)`,
302
+ duration,
303
+ easing
304
+ )
305
+ );
306
+ animations.push(
307
+ createAnimation(
308
+ leavingEl,
309
+ [{ transform: `translate3d(${IOS_CENTER}, 0, 0)` }, { transform: `translate3d(${offLeft}, 0, 0)` }],
310
+ duration,
311
+ easing
312
+ )
313
+ );
314
+ animations.push(...createOpacityAnimations(leavingContent, 1, IOS_OFF_OPACITY, duration, easing));
315
+ animations.push(
316
+ ...createToolbarItemAnimations(
317
+ leavingHeaders,
318
+ `translate3d(${IOS_CENTER}, 0, 0)`,
319
+ `translate3d(${offLeft}, 0, 0)`,
320
+ 0.99,
321
+ 0,
322
+ duration,
323
+ easing
324
+ )
325
+ );
326
+ }
327
+ }
328
+ return animations;
329
+ }
330
+ function createAndroidTransition(options) {
331
+ const { enteringEl, leavingEl, direction, duration, easing } = options;
332
+ const animations = [];
333
+ const isBack = direction === "back";
334
+ const isRoot = direction === "root";
335
+ preparePageLayer(enteringEl, isBack ? "99" : "101");
336
+ if (leavingEl) {
337
+ preparePageLayer(leavingEl, "100");
338
+ }
339
+ if (isRoot) {
340
+ animations.push(
341
+ createAnimation(
342
+ enteringEl,
343
+ [
344
+ { opacity: 0.01, transform: `translate3d(0, ${MD_OFF_BOTTOM}, 0)` },
345
+ { opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` }
346
+ ],
347
+ duration,
348
+ easing
349
+ )
350
+ );
351
+ if (leavingEl) {
352
+ animations.push(
353
+ createAnimation(
354
+ leavingEl,
355
+ [
356
+ { opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` },
357
+ { opacity: 0, transform: `translate3d(0, ${MD_CENTER}, 0)` }
358
+ ],
359
+ Math.min(duration, ANDROID_BACK_DURATION),
360
+ ANDROID_BACK_EASING
361
+ )
362
+ );
363
+ }
364
+ } else if (isBack) {
365
+ enteringEl.style.opacity = "1";
366
+ enteringEl.style.transform = `translate3d(0, ${MD_CENTER}, 0)`;
367
+ if (leavingEl) {
368
+ animations.push(
369
+ createAnimation(
370
+ leavingEl,
371
+ [
372
+ { opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` },
373
+ { opacity: 0, transform: `translate3d(0, ${MD_OFF_BOTTOM}, 0)` }
374
+ ],
375
+ duration,
376
+ easing
377
+ )
378
+ );
379
+ }
380
+ } else {
381
+ animations.push(
382
+ createAnimation(
383
+ enteringEl,
384
+ [
385
+ { opacity: 0.01, transform: `translate3d(0, ${MD_OFF_BOTTOM}, 0)` },
386
+ { opacity: 1, transform: `translate3d(0, ${MD_CENTER}, 0)` }
387
+ ],
388
+ duration,
389
+ easing
390
+ )
391
+ );
392
+ if (leavingEl) {
393
+ leavingEl.style.opacity = "1";
394
+ leavingEl.style.transform = `translate3d(0, ${MD_CENTER}, 0)`;
395
+ }
396
+ }
397
+ return animations;
398
+ }
399
+ function createNoneTransition(options) {
400
+ const { enteringEl, leavingEl } = options;
401
+ enteringEl.style.opacity = "1";
402
+ enteringEl.style.transform = "none";
403
+ if (leavingEl) {
404
+ leavingEl.style.opacity = "0";
405
+ leavingEl.style.transform = "none";
406
+ }
407
+ return [];
408
+ }
409
+ function createTransition(options, platform = "auto") {
410
+ if (options.direction === "none") {
411
+ return createNoneTransition(options);
412
+ }
413
+ const resolved = platform === "auto" ? detectPlatform() : platform;
414
+ if (resolved === "android") {
415
+ return createAndroidTransition(options);
416
+ }
417
+ return createIOSTransition(options);
418
+ }
419
+ async function waitForAnimations(animations) {
420
+ if (animations.length === 0) return;
421
+ await Promise.all(animations.map((anim) => anim.finished.catch(() => void 0)));
422
+ }
423
+ function cancelAnimations(animations) {
424
+ animations.forEach((anim) => anim.cancel());
425
+ }
426
+
427
+ // src/core/view-transitions.ts
428
+ function supportsViewTransitions() {
429
+ return typeof document !== "undefined" && "startViewTransition" in document;
430
+ }
431
+ async function runViewTransition(options) {
432
+ const { update, direction = "forward", skipAnimation = false } = options;
433
+ if (skipAnimation || !supportsViewTransitions()) {
434
+ await update();
435
+ return;
436
+ }
437
+ const root = document.documentElement;
438
+ root.dataset.transitionDirection = direction;
439
+ try {
440
+ const transition = document.startViewTransition(async () => {
441
+ await update();
442
+ });
443
+ await transition.finished;
444
+ } finally {
445
+ delete root.dataset.transitionDirection;
446
+ }
447
+ }
448
+ function setViewTransitionName(element, name) {
449
+ element.style.viewTransitionName = name;
450
+ }
451
+ function clearViewTransitionName(element) {
452
+ element.style.viewTransitionName = "";
453
+ }
454
+ var VIEW_TRANSITIONS_CSS = `
455
+ /* View Transitions API base styles */
456
+ ::view-transition-old(root),
457
+ ::view-transition-new(root) {
458
+ animation-duration: 0.4s;
459
+ animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
460
+ }
461
+
462
+ /* iOS-style forward navigation */
463
+ [data-transition-direction="forward"]::view-transition-old(root) {
464
+ animation-name: cap-slide-out-left;
465
+ }
466
+
467
+ [data-transition-direction="forward"]::view-transition-new(root) {
468
+ animation-name: cap-slide-in-right;
469
+ }
470
+
471
+ /* iOS-style back navigation */
472
+ [data-transition-direction="back"]::view-transition-old(root) {
473
+ animation-name: cap-slide-out-right;
474
+ }
475
+
476
+ [data-transition-direction="back"]::view-transition-new(root) {
477
+ animation-name: cap-slide-in-left;
478
+ }
479
+
480
+ /* Root/replace navigation - fade */
481
+ [data-transition-direction="root"]::view-transition-old(root) {
482
+ animation-name: cap-fade-out;
483
+ }
484
+
485
+ [data-transition-direction="root"]::view-transition-new(root) {
486
+ animation-name: cap-fade-in;
487
+ }
488
+
489
+ /* Header transitions */
490
+ ::view-transition-old(cap-header),
491
+ ::view-transition-new(cap-header) {
492
+ animation-duration: 0.3s;
493
+ }
494
+
495
+ [data-transition-direction="forward"]::view-transition-old(cap-header) {
496
+ animation-name: cap-header-out-left;
497
+ }
498
+
499
+ [data-transition-direction="forward"]::view-transition-new(cap-header) {
500
+ animation-name: cap-header-in-right;
501
+ }
502
+
503
+ [data-transition-direction="back"]::view-transition-old(cap-header) {
504
+ animation-name: cap-header-out-right;
505
+ }
506
+
507
+ [data-transition-direction="back"]::view-transition-new(cap-header) {
508
+ animation-name: cap-header-in-left;
509
+ }
510
+
511
+ /* Content transitions */
512
+ ::view-transition-old(cap-content),
513
+ ::view-transition-new(cap-content) {
514
+ animation-duration: 0.4s;
515
+ animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
516
+ }
517
+
518
+ [data-transition-direction="forward"]::view-transition-old(cap-content) {
519
+ animation-name: cap-slide-out-left;
520
+ }
521
+
522
+ [data-transition-direction="forward"]::view-transition-new(cap-content) {
523
+ animation-name: cap-slide-in-right;
524
+ }
525
+
526
+ [data-transition-direction="back"]::view-transition-old(cap-content) {
527
+ animation-name: cap-slide-out-right;
528
+ }
529
+
530
+ [data-transition-direction="back"]::view-transition-new(cap-content) {
531
+ animation-name: cap-slide-in-left;
532
+ }
533
+
534
+ /* Animation keyframes */
535
+ @keyframes cap-slide-in-right {
536
+ from {
537
+ transform: translateX(100%);
538
+ opacity: 1;
539
+ }
540
+ to {
541
+ transform: translateX(0);
542
+ opacity: 1;
543
+ }
544
+ }
545
+
546
+ @keyframes cap-slide-out-left {
547
+ from {
548
+ transform: translateX(0);
549
+ opacity: 1;
550
+ }
551
+ to {
552
+ transform: translateX(-30%);
553
+ opacity: 0.8;
554
+ }
555
+ }
556
+
557
+ @keyframes cap-slide-in-left {
558
+ from {
559
+ transform: translateX(-30%);
560
+ opacity: 0.8;
561
+ }
562
+ to {
563
+ transform: translateX(0);
564
+ opacity: 1;
565
+ }
566
+ }
567
+
568
+ @keyframes cap-slide-out-right {
569
+ from {
570
+ transform: translateX(0);
571
+ opacity: 1;
572
+ }
573
+ to {
574
+ transform: translateX(100%);
575
+ opacity: 1;
576
+ }
577
+ }
578
+
579
+ @keyframes cap-fade-in {
580
+ from { opacity: 0; }
581
+ to { opacity: 1; }
582
+ }
583
+
584
+ @keyframes cap-fade-out {
585
+ from { opacity: 1; }
586
+ to { opacity: 0; }
587
+ }
588
+
589
+ @keyframes cap-header-in-right {
590
+ from {
591
+ transform: translateX(20px);
592
+ opacity: 0;
593
+ }
594
+ to {
595
+ transform: translateX(0);
596
+ opacity: 1;
597
+ }
598
+ }
599
+
600
+ @keyframes cap-header-out-left {
601
+ from {
602
+ transform: translateX(0);
603
+ opacity: 1;
604
+ }
605
+ to {
606
+ transform: translateX(-20px);
607
+ opacity: 0;
608
+ }
609
+ }
610
+
611
+ @keyframes cap-header-in-left {
612
+ from {
613
+ transform: translateX(-20px);
614
+ opacity: 0;
615
+ }
616
+ to {
617
+ transform: translateX(0);
618
+ opacity: 1;
619
+ }
620
+ }
621
+
622
+ @keyframes cap-header-out-right {
623
+ from {
624
+ transform: translateX(0);
625
+ opacity: 1;
626
+ }
627
+ to {
628
+ transform: translateX(20px);
629
+ opacity: 0;
630
+ }
631
+ }
632
+
633
+ /* Reduced motion support */
634
+ @media (prefers-reduced-motion: reduce) {
635
+ ::view-transition-old(root),
636
+ ::view-transition-new(root),
637
+ ::view-transition-old(cap-header),
638
+ ::view-transition-new(cap-header),
639
+ ::view-transition-old(cap-content),
640
+ ::view-transition-new(cap-content) {
641
+ animation-duration: 0.01ms !important;
642
+ }
643
+ }
644
+ `;
645
+ function injectViewTransitionsCSS() {
646
+ if (typeof document === "undefined") return;
647
+ const styleId = "cap-view-transitions-css";
648
+ if (document.getElementById(styleId)) return;
649
+ const style = document.createElement("style");
650
+ style.id = styleId;
651
+ style.textContent = VIEW_TRANSITIONS_CSS;
652
+ document.head.appendChild(style);
653
+ }
654
+
655
+ // src/core/transition-controller.ts
656
+ var DEFAULT_CONFIG = {
657
+ platform: "auto",
658
+ duration: 0,
659
+ // Will use platform default
660
+ easing: "",
661
+ // Will use platform default
662
+ useViewTransitions: false,
663
+ detectPlatform
664
+ };
665
+ var TransitionController = class {
666
+ config;
667
+ pageStack = [];
668
+ currentAnimations = [];
669
+ isAnimating = false;
670
+ interactiveBackTransition = null;
671
+ lifecycleCallbacks = /* @__PURE__ */ new Map();
672
+ constructor(config = {}) {
673
+ this.config = { ...DEFAULT_CONFIG, ...config };
674
+ if (this.config.useViewTransitions && supportsViewTransitions()) {
675
+ injectViewTransitionsCSS();
676
+ }
677
+ }
678
+ /**
679
+ * Get the resolved platform
680
+ */
681
+ get platform() {
682
+ if (this.config.platform === "ios") {
683
+ return "ios";
684
+ }
685
+ if (this.config.platform === "android") {
686
+ return "android";
687
+ }
688
+ return this.config.detectPlatform();
689
+ }
690
+ /**
691
+ * Get the current page state
692
+ */
693
+ get currentPage() {
694
+ return this.pageStack[this.pageStack.length - 1];
695
+ }
696
+ /**
697
+ * Get the page stack
698
+ */
699
+ get stack() {
700
+ return this.pageStack;
701
+ }
702
+ /**
703
+ * Check if an animation is in progress
704
+ */
705
+ get animating() {
706
+ return this.isAnimating;
707
+ }
708
+ /**
709
+ * Update global configuration
710
+ */
711
+ configure(config) {
712
+ this.config = { ...this.config, ...config };
713
+ if (this.config.useViewTransitions && supportsViewTransitions()) {
714
+ injectViewTransitionsCSS();
715
+ }
716
+ }
717
+ /**
718
+ * Register lifecycle callbacks for a page
719
+ */
720
+ registerLifecycle(pageId, lifecycle) {
721
+ this.lifecycleCallbacks.set(pageId, lifecycle);
722
+ }
723
+ /**
724
+ * Unregister lifecycle callbacks for a page
725
+ */
726
+ unregisterLifecycle(pageId) {
727
+ this.lifecycleCallbacks.delete(pageId);
728
+ }
729
+ /**
730
+ * Create a page state from an element
731
+ */
732
+ createPageState(element, options = {}) {
733
+ const id = options.id || `page-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
734
+ const header = element.querySelector(
735
+ '[data-cap-header], .cap-header, cap-header, [slot="header"]'
736
+ );
737
+ const content = element.querySelector(
738
+ '[data-cap-content], .cap-content, cap-content, [slot="content"]'
739
+ );
740
+ const footer = element.querySelector(
741
+ '[data-cap-footer], .cap-footer, cap-footer, [slot="footer"]'
742
+ );
743
+ return {
744
+ id,
745
+ element,
746
+ header: header || void 0,
747
+ content: content || void 0,
748
+ footer: footer || void 0,
749
+ isActive: false,
750
+ data: options.data
751
+ };
752
+ }
753
+ /**
754
+ * Navigate to a new page (push)
755
+ */
756
+ async push(enteringEl, config = {}) {
757
+ return this.navigate(enteringEl, { ...config, direction: "forward" });
758
+ }
759
+ /**
760
+ * Navigate back (pop)
761
+ */
762
+ async pop(config = {}) {
763
+ if (this.pageStack.length <= 1) {
764
+ return { success: false, duration: 0, error: new Error("Cannot pop: no page to go back to") };
765
+ }
766
+ const leavingState = this.pageStack[this.pageStack.length - 1];
767
+ const enteringState = this.pageStack[this.pageStack.length - 2];
768
+ return this.navigateWithStates(enteringState, leavingState, { ...config, direction: "back" }, () => {
769
+ this.pageStack.pop();
770
+ });
771
+ }
772
+ /**
773
+ * Start an interactive iOS-style back transition using the cached previous page.
774
+ */
775
+ beginInteractiveBack(config = {}) {
776
+ if (this.pageStack.length <= 1 || this.isAnimating || this.interactiveBackTransition) {
777
+ return false;
778
+ }
779
+ const leavingState = this.pageStack[this.pageStack.length - 1];
780
+ const enteringState = this.pageStack[this.pageStack.length - 2];
781
+ const direction = "back";
782
+ const duration = config.duration ?? (this.config.duration || getDefaultDuration(this.platform, direction));
783
+ this.isAnimating = true;
784
+ enteringState.element.style.display = "";
785
+ enteringState.element.style.visibility = "visible";
786
+ const animOptions = {
787
+ enteringEl: enteringState.element,
788
+ leavingEl: leavingState.element,
789
+ direction,
790
+ duration,
791
+ easing: "linear",
792
+ isBack: true
793
+ };
794
+ const animations = createTransition(animOptions, this.platform);
795
+ for (const animation of animations) {
796
+ animation.pause();
797
+ animation.currentTime = 0;
798
+ }
799
+ this.currentAnimations = animations;
800
+ this.interactiveBackTransition = {
801
+ enteringState,
802
+ leavingState,
803
+ animations,
804
+ duration
805
+ };
806
+ return true;
807
+ }
808
+ /**
809
+ * Move the current interactive back transition to a progress step from 0 to 1.
810
+ */
811
+ stepInteractiveBack(step) {
812
+ const transition = this.interactiveBackTransition;
813
+ if (!transition) {
814
+ return;
815
+ }
816
+ const progress = Math.max(0, Math.min(step, 0.9999));
817
+ for (const animation of transition.animations) {
818
+ const duration = this.getAnimationDuration(animation, transition.duration);
819
+ animation.pause();
820
+ animation.currentTime = duration * progress;
821
+ }
822
+ }
823
+ /**
824
+ * Complete or cancel the current interactive back transition.
825
+ */
826
+ async endInteractiveBack(shouldComplete, releaseDuration, commitStack) {
827
+ const transition = this.interactiveBackTransition;
828
+ if (!transition) {
829
+ return;
830
+ }
831
+ try {
832
+ await this.playInteractiveAnimationsTo(shouldComplete ? 1 : 0, releaseDuration);
833
+ if (shouldComplete && commitStack) {
834
+ this.pageStack.pop();
835
+ this.updatePageVisibility(transition.enteringState, transition.leavingState);
836
+ transition.enteringState.isActive = true;
837
+ transition.leavingState.isActive = false;
838
+ cancelAnimations(transition.animations);
839
+ } else if (!shouldComplete) {
840
+ this.updatePageVisibility(transition.leavingState, transition.enteringState);
841
+ transition.leavingState.isActive = true;
842
+ transition.enteringState.isActive = false;
843
+ cancelAnimations(transition.animations);
844
+ }
845
+ } finally {
846
+ this.interactiveBackTransition = null;
847
+ this.currentAnimations = [];
848
+ this.isAnimating = false;
849
+ }
850
+ }
851
+ /**
852
+ * Cancel the current interactive back transition immediately.
853
+ */
854
+ cancelInteractiveBack() {
855
+ const transition = this.interactiveBackTransition;
856
+ if (!transition) {
857
+ return;
858
+ }
859
+ cancelAnimations(transition.animations);
860
+ this.updatePageVisibility(transition.leavingState, transition.enteringState);
861
+ this.interactiveBackTransition = null;
862
+ this.currentAnimations = [];
863
+ this.isAnimating = false;
864
+ }
865
+ /**
866
+ * Replace all pages with a new root
867
+ */
868
+ async setRoot(enteringEl, config = {}) {
869
+ return this.navigate(enteringEl, { ...config, direction: "root" });
870
+ }
871
+ /**
872
+ * Main navigation method
873
+ */
874
+ async navigate(enteringEl, config = {}) {
875
+ const direction = config.direction || "forward";
876
+ const enteringState = this.createPageState(enteringEl);
877
+ const leavingState = this.currentPage;
878
+ return this.navigateWithStates(enteringState, leavingState, config, () => {
879
+ if (direction === "root") {
880
+ this.pageStack = [enteringState];
881
+ } else if (direction === "back" && this.pageStack.length > 0) {
882
+ this.pageStack.pop();
883
+ const staleEnteringState = this.pageStack.pop();
884
+ if (staleEnteringState && staleEnteringState.element !== enteringState.element) {
885
+ staleEnteringState.element.remove();
886
+ this.lifecycleCallbacks.delete(staleEnteringState.id);
887
+ }
888
+ this.pageStack.push(enteringState);
889
+ } else {
890
+ this.pageStack.push(enteringState);
891
+ }
892
+ });
893
+ }
894
+ /**
895
+ * Navigate between two known page states
896
+ */
897
+ async navigateWithStates(enteringState, leavingState, config, updateStack) {
898
+ const startTime = performance.now();
899
+ const direction = config.direction || "forward";
900
+ if (this.isAnimating) {
901
+ cancelAnimations(this.currentAnimations);
902
+ }
903
+ this.isAnimating = true;
904
+ const event = {
905
+ direction,
906
+ from: leavingState,
907
+ to: enteringState
908
+ };
909
+ try {
910
+ if (leavingState) {
911
+ const lifecycle = this.lifecycleCallbacks.get(leavingState.id);
912
+ await lifecycle?.onWillLeave?.(event);
913
+ config.onStart?.();
914
+ }
915
+ const enteringLifecycle = this.lifecycleCallbacks.get(enteringState.id);
916
+ await enteringLifecycle?.onWillEnter?.(event);
917
+ const duration = config.duration ?? (this.config.duration || getDefaultDuration(this.platform, direction));
918
+ const easing = this.resolveTransitionEasing(config.easing || this.config.easing, direction);
919
+ const useViewTransitions = config.useViewTransitions !== false && this.config.useViewTransitions && supportsViewTransitions();
920
+ if (useViewTransitions) {
921
+ this.prepareViewTransitionElements(enteringState, leavingState);
922
+ await runViewTransition({
923
+ direction,
924
+ update: () => {
925
+ updateStack();
926
+ this.updatePageVisibility(enteringState, leavingState);
927
+ this.applyViewTransitionNames(enteringState);
928
+ this.clearViewTransitionNames(leavingState);
929
+ }
930
+ });
931
+ this.clearViewTransitionNames(enteringState, leavingState);
932
+ } else {
933
+ updateStack();
934
+ const animOptions = {
935
+ enteringEl: enteringState.element,
936
+ leavingEl: leavingState?.element,
937
+ direction,
938
+ duration,
939
+ easing,
940
+ isBack: direction === "back"
941
+ };
942
+ this.currentAnimations = createTransition(animOptions, this.platform);
943
+ await waitForAnimations(this.currentAnimations);
944
+ this.updatePageVisibility(enteringState, leavingState);
945
+ cancelAnimations(this.currentAnimations);
946
+ }
947
+ enteringState.isActive = true;
948
+ await enteringLifecycle?.onDidEnter?.(event);
949
+ if (leavingState) {
950
+ leavingState.isActive = false;
951
+ const lifecycle = this.lifecycleCallbacks.get(leavingState.id);
952
+ await lifecycle?.onDidLeave?.(event);
953
+ }
954
+ config.onComplete?.();
955
+ const totalDuration = performance.now() - startTime;
956
+ return { success: true, duration: totalDuration };
957
+ } catch (error) {
958
+ return {
959
+ success: false,
960
+ duration: performance.now() - startTime,
961
+ error: error instanceof Error ? error : new Error(String(error))
962
+ };
963
+ } finally {
964
+ this.isAnimating = false;
965
+ this.currentAnimations = [];
966
+ }
967
+ }
968
+ /**
969
+ * Update page visibility after animation
970
+ */
971
+ updatePageVisibility(enteringState, leavingState) {
972
+ enteringState.element.style.display = "";
973
+ enteringState.element.style.visibility = "visible";
974
+ enteringState.element.style.opacity = "1";
975
+ enteringState.element.style.transform = "none";
976
+ enteringState.element.style.position = "relative";
977
+ this.clearTransitionOnlyStyles(enteringState.element);
978
+ this.clearPagePartTransitionStyles(enteringState);
979
+ if (leavingState) {
980
+ leavingState.element.style.display = "none";
981
+ leavingState.element.style.visibility = "hidden";
982
+ leavingState.element.style.opacity = "1";
983
+ leavingState.element.style.transform = "none";
984
+ this.clearTransitionOnlyStyles(leavingState.element);
985
+ this.clearPagePartTransitionStyles(leavingState);
986
+ }
987
+ }
988
+ getAnimationDuration(animation, fallback) {
989
+ const duration = animation.effect?.getTiming().duration;
990
+ return typeof duration === "number" && Number.isFinite(duration) ? duration : fallback;
991
+ }
992
+ async playInteractiveAnimationsTo(targetProgress, releaseDuration) {
993
+ const transition = this.interactiveBackTransition;
994
+ if (!transition) {
995
+ return;
996
+ }
997
+ if (releaseDuration <= 0) {
998
+ for (const animation of transition.animations) {
999
+ const duration = this.getAnimationDuration(animation, transition.duration);
1000
+ animation.pause();
1001
+ animation.currentTime = duration * targetProgress;
1002
+ }
1003
+ return;
1004
+ }
1005
+ const finished = transition.animations.map((animation) => {
1006
+ const duration = this.getAnimationDuration(animation, transition.duration);
1007
+ const currentTime = typeof animation.currentTime === "number" ? animation.currentTime : 0;
1008
+ const targetTime = duration * targetProgress;
1009
+ const distance = Math.abs(targetTime - currentTime);
1010
+ if (distance < 1) {
1011
+ animation.currentTime = targetTime;
1012
+ return Promise.resolve();
1013
+ }
1014
+ animation.playbackRate = Math.max(distance / releaseDuration, 1e-3) * (targetTime >= currentTime ? 1 : -1);
1015
+ animation.play();
1016
+ return animation.finished.catch(() => void 0);
1017
+ });
1018
+ await Promise.all(finished);
1019
+ }
1020
+ /**
1021
+ * Resolve configured easing presets after platform/direction are known.
1022
+ */
1023
+ resolveTransitionEasing(easing, direction) {
1024
+ if (!easing) {
1025
+ return getDefaultEasing(this.platform, direction || "forward");
1026
+ }
1027
+ if (typeof easing === "string" && ["ios", "android"].includes(easing)) {
1028
+ return getDefaultEasing(easing, direction || "forward");
1029
+ }
1030
+ return resolveEasing(easing);
1031
+ }
1032
+ /**
1033
+ * Remove styles that should only exist while a page is actively transitioning.
1034
+ */
1035
+ clearTransitionOnlyStyles(element) {
1036
+ element.classList.remove("cap-transition-active");
1037
+ element.style.removeProperty("z-index");
1038
+ element.style.removeProperty("pointer-events");
1039
+ element.style.removeProperty("will-change");
1040
+ element.style.removeProperty("backface-visibility");
1041
+ element.style.removeProperty("transform-style");
1042
+ element.style.removeProperty("box-shadow");
1043
+ }
1044
+ clearPagePartTransitionStyles(pageState) {
1045
+ const { header, content, footer } = this.resolvePageParts(pageState);
1046
+ for (const element of [header, content, footer]) {
1047
+ if (!element) continue;
1048
+ this.clearTransitionOnlyStyles(element);
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Prepare entering/leaving elements for a View Transition capture.
1053
+ * Entering page must be hidden in the "old" snapshot.
1054
+ */
1055
+ prepareViewTransitionElements(enteringState, leavingState) {
1056
+ this.clearAllKnownViewTransitionNames(enteringState, leavingState);
1057
+ if (leavingState) {
1058
+ this.applyViewTransitionNames(leavingState);
1059
+ enteringState.element.style.display = "none";
1060
+ enteringState.element.style.visibility = "hidden";
1061
+ }
1062
+ }
1063
+ /**
1064
+ * Assign view transition names to a page's layout parts.
1065
+ */
1066
+ applyViewTransitionNames(pageState) {
1067
+ const { header, content, footer } = this.resolvePageParts(pageState);
1068
+ if (header) {
1069
+ setViewTransitionName(header, "cap-header");
1070
+ }
1071
+ if (content) {
1072
+ setViewTransitionName(content, "cap-content");
1073
+ }
1074
+ if (footer) {
1075
+ setViewTransitionName(footer, "cap-footer");
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Clear view transition names for one or more page states.
1080
+ */
1081
+ clearViewTransitionNames(...states) {
1082
+ for (const state of states) {
1083
+ if (!state) continue;
1084
+ const { header, content, footer } = this.resolvePageParts(state);
1085
+ if (header) {
1086
+ clearViewTransitionName(header);
1087
+ }
1088
+ if (content) {
1089
+ clearViewTransitionName(content);
1090
+ }
1091
+ if (footer) {
1092
+ clearViewTransitionName(footer);
1093
+ }
1094
+ }
1095
+ }
1096
+ /**
1097
+ * Clear view transition names from all known pages plus transient states.
1098
+ */
1099
+ clearAllKnownViewTransitionNames(...extraStates) {
1100
+ const knownStates = /* @__PURE__ */ new Set();
1101
+ for (const state of this.pageStack) {
1102
+ knownStates.add(state);
1103
+ }
1104
+ for (const state of extraStates) {
1105
+ if (state) {
1106
+ knownStates.add(state);
1107
+ }
1108
+ }
1109
+ this.clearViewTransitionNames(...knownStates);
1110
+ }
1111
+ /**
1112
+ * Resolve page parts lazily to avoid timing issues with custom-element setup.
1113
+ */
1114
+ resolvePageParts(pageState) {
1115
+ const header = pageState.header || pageState.element.querySelector(
1116
+ '[data-cap-header], .cap-header, cap-header, [slot="header"]'
1117
+ ) || void 0;
1118
+ const content = pageState.content || pageState.element.querySelector(
1119
+ '[data-cap-content], .cap-content, cap-content, [slot="content"]'
1120
+ ) || void 0;
1121
+ const footer = pageState.footer || pageState.element.querySelector(
1122
+ '[data-cap-footer], .cap-footer, cap-footer, [slot="footer"]'
1123
+ ) || void 0;
1124
+ if (header) pageState.header = header;
1125
+ if (content) pageState.content = content;
1126
+ if (footer) pageState.footer = footer;
1127
+ return { header, content, footer };
1128
+ }
1129
+ /**
1130
+ * Save scroll position for a page
1131
+ */
1132
+ saveScrollPosition(pageId) {
1133
+ const page = this.pageStack.find((p) => p.id === pageId);
1134
+ if (page?.content) {
1135
+ page.scrollPosition = {
1136
+ x: page.content.scrollLeft,
1137
+ y: page.content.scrollTop
1138
+ };
1139
+ }
1140
+ }
1141
+ /**
1142
+ * Restore scroll position for a page
1143
+ */
1144
+ restoreScrollPosition(pageId) {
1145
+ const page = this.pageStack.find((p) => p.id === pageId);
1146
+ if (page?.content && page.scrollPosition) {
1147
+ page.content.scrollLeft = page.scrollPosition.x;
1148
+ page.content.scrollTop = page.scrollPosition.y;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Remove a page from the stack (used when cleaning up)
1153
+ */
1154
+ removePage(pageId) {
1155
+ const index = this.pageStack.findIndex((p) => p.id === pageId);
1156
+ if (index !== -1) {
1157
+ this.pageStack.splice(index, 1);
1158
+ this.lifecycleCallbacks.delete(pageId);
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Clear all pages
1163
+ */
1164
+ clear() {
1165
+ this.pageStack = [];
1166
+ this.lifecycleCallbacks.clear();
1167
+ cancelAnimations(this.currentAnimations);
1168
+ this.currentAnimations = [];
1169
+ this.isAnimating = false;
1170
+ }
1171
+ };
1172
+ function createTransitionController(config) {
1173
+ return new TransitionController(config);
1174
+ }
1175
+
1176
+ // src/core/native-platform.ts
1177
+ function getCapacitorRuntime() {
1178
+ return globalThis.Capacitor;
1179
+ }
1180
+ function normalizePlatform(platform) {
1181
+ if (platform === "ios" || platform === "android" || platform === "web") {
1182
+ return platform;
1183
+ }
1184
+ return "unknown";
1185
+ }
1186
+ function detectNativePlatform() {
1187
+ const capacitor = getCapacitorRuntime();
1188
+ const platform = normalizePlatform(capacitor?.getPlatform?.());
1189
+ const isNative = capacitor?.isNativePlatform?.() ?? (platform === "ios" || platform === "android");
1190
+ return {
1191
+ platform,
1192
+ isNative
1193
+ };
1194
+ }
1195
+ function isNativeSwipeGesturePlatform() {
1196
+ const { platform, isNative } = detectNativePlatform();
1197
+ return isNative && platform === "ios";
1198
+ }
1199
+
1200
+ // src/components/cap-router-outlet.ts
1201
+ var CapRouterOutlet = class extends HTMLElement {
1202
+ controller;
1203
+ options;
1204
+ observer = null;
1205
+ pendingPage = null;
1206
+ ignoredNodes = /* @__PURE__ */ new WeakSet();
1207
+ swipeGesturePointer = null;
1208
+ swipeGestureListenersActive = false;
1209
+ skipNextHistoryBackTransition = false;
1210
+ swipeBackDepth = 0;
1211
+ lastNavigationHref = null;
1212
+ swipeGestureEdgeWidth = 50;
1213
+ swipeGestureThreshold = 10;
1214
+ swipeGestureMinimumVelocity = 0.2;
1215
+ static get observedAttributes() {
1216
+ return ["platform", "duration", "keep-in-dom", "max-cached", "swipe-gesture"];
1217
+ }
1218
+ constructor() {
1219
+ super();
1220
+ this.options = {
1221
+ keepInDom: true,
1222
+ maxCached: 10,
1223
+ swipeGesture: "auto"
1224
+ };
1225
+ this.controller = createTransitionController();
1226
+ }
1227
+ connectedCallback() {
1228
+ this.style.display = "block";
1229
+ this.style.position = "relative";
1230
+ this.style.width = "100%";
1231
+ this.style.height = "100%";
1232
+ this.style.overflow = "hidden";
1233
+ this.lastNavigationHref = this.getCurrentNavigationHref();
1234
+ this.observer = new MutationObserver((mutations) => {
1235
+ this.handleMutations(mutations);
1236
+ });
1237
+ this.observer.observe(this, {
1238
+ childList: true,
1239
+ subtree: false
1240
+ });
1241
+ const children = Array.from(this.children);
1242
+ if (children.length > 0) {
1243
+ this.initializeFirstPage(children[children.length - 1]);
1244
+ }
1245
+ this.updateSwipeGestureListeners();
1246
+ }
1247
+ disconnectedCallback() {
1248
+ this.observer?.disconnect();
1249
+ this.removeSwipeGestureListeners();
1250
+ this.controller.clear();
1251
+ this.swipeBackDepth = 0;
1252
+ this.lastNavigationHref = null;
1253
+ }
1254
+ attributeChangedCallback(name, _oldValue, newValue) {
1255
+ switch (name) {
1256
+ case "platform":
1257
+ this.controller.configure({ platform: newValue });
1258
+ break;
1259
+ case "duration":
1260
+ this.controller.configure({ duration: parseInt(newValue, 10) });
1261
+ break;
1262
+ case "keep-in-dom":
1263
+ this.options.keepInDom = newValue !== "false";
1264
+ break;
1265
+ case "max-cached":
1266
+ this.options.maxCached = parseInt(newValue, 10);
1267
+ break;
1268
+ case "swipe-gesture":
1269
+ this.options.swipeGesture = this.parseSwipeGestureAttribute(newValue);
1270
+ this.updateSwipeGestureListeners();
1271
+ break;
1272
+ }
1273
+ }
1274
+ /**
1275
+ * Handle DOM mutations (child additions/removals)
1276
+ */
1277
+ handleMutations(mutations) {
1278
+ const addedNodes = [];
1279
+ const removedNodes = [];
1280
+ for (const mutation of mutations) {
1281
+ for (const node of mutation.removedNodes) {
1282
+ if (node instanceof HTMLElement) {
1283
+ removedNodes.push(node);
1284
+ }
1285
+ }
1286
+ for (const node of mutation.addedNodes) {
1287
+ if (node instanceof HTMLElement) {
1288
+ addedNodes.push(node);
1289
+ }
1290
+ }
1291
+ }
1292
+ const currentEl = this.controller.currentPage?.element;
1293
+ if (currentEl && removedNodes.includes(currentEl) && addedNodes.length > 0 && !currentEl.isConnected) {
1294
+ this.stylePageForTransition(currentEl);
1295
+ currentEl.style.display = "";
1296
+ currentEl.style.visibility = "visible";
1297
+ const anchor = addedNodes[0];
1298
+ this.ignoredNodes.add(currentEl);
1299
+ if (anchor.parentElement === this) {
1300
+ this.insertBefore(currentEl, anchor);
1301
+ } else {
1302
+ this.appendChild(currentEl);
1303
+ }
1304
+ }
1305
+ for (const node of removedNodes) {
1306
+ if (node === currentEl) continue;
1307
+ const state = this.controller.stack.find((pageState) => pageState.element === node);
1308
+ if (state) {
1309
+ this.controller.removePage(state.id);
1310
+ }
1311
+ }
1312
+ for (const node of addedNodes) {
1313
+ if (node === this.pendingPage) continue;
1314
+ if (this.ignoredNodes.has(node)) {
1315
+ this.ignoredNodes.delete(node);
1316
+ continue;
1317
+ }
1318
+ this.handleNewPage(node);
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Initialize the first page without animation
1323
+ */
1324
+ initializeFirstPage(page) {
1325
+ page.style.position = "relative";
1326
+ page.style.width = "100%";
1327
+ page.style.height = "100%";
1328
+ const state = this.controller.createPageState(page);
1329
+ state.isActive = true;
1330
+ this.controller.pageStack.push(state);
1331
+ this.swipeBackDepth = 0;
1332
+ this.lastNavigationHref = this.getCurrentNavigationHref();
1333
+ }
1334
+ /**
1335
+ * Handle a new page being added
1336
+ */
1337
+ async handleNewPage(page) {
1338
+ const outletDirection = this.dataset.direction;
1339
+ const direction = page.dataset.direction || outletDirection || "forward";
1340
+ if (outletDirection) {
1341
+ delete this.dataset.direction;
1342
+ }
1343
+ const skipTransition = this.skipNextHistoryBackTransition && direction === "back";
1344
+ this.skipNextHistoryBackTransition = false;
1345
+ const hadPageBefore = this.controller.stack.length > 0;
1346
+ this.stylePageForTransition(page);
1347
+ this.pendingPage = page;
1348
+ try {
1349
+ const result = await this.controller.navigate(page, { direction, duration: skipTransition ? 0 : void 0 });
1350
+ if (result.success) {
1351
+ this.recordCompletedNavigation(direction, { hadPageBefore });
1352
+ }
1353
+ } finally {
1354
+ this.pendingPage = null;
1355
+ }
1356
+ if (!this.options.keepInDom) {
1357
+ this.cleanupOldPages();
1358
+ } else {
1359
+ this.enforceCacheLimit();
1360
+ }
1361
+ }
1362
+ /**
1363
+ * Clean up pages that are no longer needed
1364
+ */
1365
+ cleanupOldPages() {
1366
+ const stack = this.controller.stack;
1367
+ const children = Array.from(this.children);
1368
+ for (const child of children) {
1369
+ const inStack = stack.some((s) => s.element === child);
1370
+ if (!inStack && !child.dataset.keepInDom) {
1371
+ child.remove();
1372
+ }
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Enforce the cache limit
1377
+ */
1378
+ enforceCacheLimit() {
1379
+ const stack = this.controller.stack;
1380
+ const maxCached = this.options.maxCached || 10;
1381
+ if (stack.length > maxCached) {
1382
+ const toRemove = stack.slice(0, stack.length - maxCached);
1383
+ for (const page of toRemove) {
1384
+ if (!page.isActive) {
1385
+ page.element.remove();
1386
+ this.controller.removePage(page.id);
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+ /**
1392
+ * Programmatic navigation - push a new page
1393
+ */
1394
+ async push(page, config = {}) {
1395
+ this.stylePageForTransition(page);
1396
+ const hadPageBefore = this.controller.stack.length > 0;
1397
+ this.pendingPage = page;
1398
+ this.appendChild(page);
1399
+ try {
1400
+ const result = await this.controller.push(page, config);
1401
+ if (result.success) {
1402
+ this.recordCompletedNavigation("forward", { hadPageBefore, forceForward: true });
1403
+ }
1404
+ } finally {
1405
+ this.pendingPage = null;
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Programmatic navigation - pop current page
1410
+ */
1411
+ async pop(config = {}) {
1412
+ const result = await this.controller.pop(config);
1413
+ if (result.success) {
1414
+ this.recordCompletedNavigation("back", { hadPageBefore: true });
1415
+ if (!this.options.keepInDom) {
1416
+ const children = Array.from(this.children);
1417
+ const lastChild = children[children.length - 1];
1418
+ if (lastChild) {
1419
+ lastChild.remove();
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Programmatic navigation - set root page
1426
+ */
1427
+ async setRoot(page, config = {}) {
1428
+ const oldChildren = Array.from(this.children);
1429
+ this.stylePageForTransition(page);
1430
+ this.pendingPage = page;
1431
+ this.appendChild(page);
1432
+ try {
1433
+ const result = await this.controller.setRoot(page, config);
1434
+ if (result.success) {
1435
+ this.recordCompletedNavigation("root", { hadPageBefore: true });
1436
+ }
1437
+ } finally {
1438
+ this.pendingPage = null;
1439
+ }
1440
+ for (const child of oldChildren) {
1441
+ child.remove();
1442
+ }
1443
+ }
1444
+ /**
1445
+ * Get the current page stack length
1446
+ */
1447
+ get stackLength() {
1448
+ return this.controller.stack.length;
1449
+ }
1450
+ /**
1451
+ * Check if we can go back
1452
+ */
1453
+ get canGoBack() {
1454
+ return this.controller.stack.length > 1 && this.swipeBackDepth > 0;
1455
+ }
1456
+ /**
1457
+ * Get whether edge swipe-back gesture is enabled.
1458
+ */
1459
+ get swipeGesture() {
1460
+ return this.options.swipeGesture ?? "auto";
1461
+ }
1462
+ /**
1463
+ * Enable, disable, or auto-detect edge swipe-back gesture.
1464
+ */
1465
+ set swipeGesture(value) {
1466
+ this.setSwipeGesture(value);
1467
+ }
1468
+ /**
1469
+ * Enable, disable, or auto-detect edge swipe-back gesture.
1470
+ */
1471
+ setSwipeGesture(value) {
1472
+ this.options.swipeGesture = value;
1473
+ const serialized = this.serializeSwipeGesture(value);
1474
+ if (this.getAttribute("swipe-gesture") !== serialized) {
1475
+ this.setAttribute("swipe-gesture", serialized);
1476
+ } else {
1477
+ this.updateSwipeGestureListeners();
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Get the transition controller for advanced usage
1482
+ */
1483
+ getController() {
1484
+ return this.controller;
1485
+ }
1486
+ /**
1487
+ * Apply layout styles required for transition animations.
1488
+ */
1489
+ stylePageForTransition(page) {
1490
+ page.style.position = "absolute";
1491
+ page.style.top = "0";
1492
+ page.style.left = "0";
1493
+ page.style.width = "100%";
1494
+ page.style.height = "100%";
1495
+ }
1496
+ parseSwipeGestureAttribute(value) {
1497
+ if (value === null || value === "auto") {
1498
+ return "auto";
1499
+ }
1500
+ if (value === "false") {
1501
+ return false;
1502
+ }
1503
+ return true;
1504
+ }
1505
+ serializeSwipeGesture(value) {
1506
+ return typeof value === "boolean" ? String(value) : value;
1507
+ }
1508
+ updateSwipeGestureListeners() {
1509
+ if (this.options.swipeGesture === false) {
1510
+ this.removeSwipeGestureListeners();
1511
+ return;
1512
+ }
1513
+ if (this.swipeGestureListenersActive || typeof PointerEvent === "undefined") {
1514
+ return;
1515
+ }
1516
+ this.addEventListener("pointerdown", this.handleSwipeGesturePointerDown);
1517
+ this.addEventListener("pointermove", this.handleSwipeGesturePointerMove, { passive: false });
1518
+ this.addEventListener("pointerup", this.handleSwipeGesturePointerEnd);
1519
+ this.addEventListener("pointercancel", this.handleSwipeGesturePointerCancel);
1520
+ this.swipeGestureListenersActive = true;
1521
+ }
1522
+ removeSwipeGestureListeners() {
1523
+ if (!this.swipeGestureListenersActive) {
1524
+ return;
1525
+ }
1526
+ this.removeEventListener("pointerdown", this.handleSwipeGesturePointerDown);
1527
+ this.removeEventListener("pointermove", this.handleSwipeGesturePointerMove);
1528
+ this.removeEventListener("pointerup", this.handleSwipeGesturePointerEnd);
1529
+ this.removeEventListener("pointercancel", this.handleSwipeGesturePointerCancel);
1530
+ this.swipeGestureListenersActive = false;
1531
+ this.swipeGesturePointer = null;
1532
+ }
1533
+ isSwipeGestureEnabled() {
1534
+ const option = this.options.swipeGesture ?? "auto";
1535
+ if (option === true) {
1536
+ return true;
1537
+ }
1538
+ if (option === false) {
1539
+ return false;
1540
+ }
1541
+ return isNativeSwipeGesturePlatform();
1542
+ }
1543
+ getCurrentNavigationHref() {
1544
+ return this.ownerDocument.defaultView?.location.href ?? null;
1545
+ }
1546
+ recordCompletedNavigation(direction, options) {
1547
+ const currentHref = this.getCurrentNavigationHref();
1548
+ if (!options.hadPageBefore || direction === "root") {
1549
+ this.swipeBackDepth = 0;
1550
+ this.lastNavigationHref = currentHref;
1551
+ return;
1552
+ }
1553
+ if (direction === "back") {
1554
+ this.swipeBackDepth = Math.max(0, this.swipeBackDepth - 1);
1555
+ this.lastNavigationHref = currentHref;
1556
+ return;
1557
+ }
1558
+ if (direction === "forward" || direction === "none") {
1559
+ const hrefChanged = currentHref === null || this.lastNavigationHref === null || currentHref !== this.lastNavigationHref;
1560
+ if (options.forceForward || hrefChanged) {
1561
+ this.swipeBackDepth += 1;
1562
+ }
1563
+ this.lastNavigationHref = currentHref;
1564
+ return;
1565
+ }
1566
+ this.lastNavigationHref = currentHref;
1567
+ }
1568
+ canStartSwipeGesture(event) {
1569
+ if (!this.isSwipeGestureEnabled() || this.controller.animating || this.pendingPage || !this.canGoBack) {
1570
+ return false;
1571
+ }
1572
+ if (!event.isPrimary || event.pointerType === "mouse" && event.button !== 0) {
1573
+ return false;
1574
+ }
1575
+ if (this.isInteractiveSwipeTarget(event.target) || this.hasScrollableInlineAncestor(event.target)) {
1576
+ return false;
1577
+ }
1578
+ const rect = this.getBoundingClientRect();
1579
+ const startX = event.clientX - rect.left;
1580
+ if (event.clientY < rect.top || event.clientY > rect.bottom) {
1581
+ return false;
1582
+ }
1583
+ return this.isRTL() ? startX >= rect.width - this.swipeGestureEdgeWidth : startX <= this.swipeGestureEdgeWidth;
1584
+ }
1585
+ isRTL() {
1586
+ const doc = this.ownerDocument;
1587
+ return doc.dir === "rtl" || doc.documentElement.dir === "rtl" || getComputedStyle(this).direction === "rtl";
1588
+ }
1589
+ getSwipeGestureDeltaX(pointer) {
1590
+ const deltaX = pointer.currentX - pointer.startX;
1591
+ return this.isRTL() ? -deltaX : deltaX;
1592
+ }
1593
+ isInteractiveSwipeTarget(target) {
1594
+ if (!(target instanceof Element)) {
1595
+ return false;
1596
+ }
1597
+ return Boolean(
1598
+ target.closest(
1599
+ 'a, button, input, textarea, select, option, [contenteditable="true"], [data-swipe-gesture-ignore], [data-swipe-back-ignore]'
1600
+ )
1601
+ );
1602
+ }
1603
+ hasScrollableInlineAncestor(target) {
1604
+ let element = target instanceof Element ? target : null;
1605
+ while (element && element !== this) {
1606
+ if (element instanceof HTMLElement) {
1607
+ const style = getComputedStyle(element);
1608
+ const canScrollInline = /(auto|scroll)/.test(style.overflowX) && element.scrollWidth > element.clientWidth + 1;
1609
+ if (canScrollInline && element.scrollLeft > 0) {
1610
+ return true;
1611
+ }
1612
+ }
1613
+ element = element.parentElement;
1614
+ }
1615
+ return false;
1616
+ }
1617
+ handleSwipeGesturePointerDown = (event) => {
1618
+ if (!this.canStartSwipeGesture(event)) {
1619
+ return;
1620
+ }
1621
+ this.swipeGesturePointer = {
1622
+ pointerId: event.pointerId,
1623
+ startX: event.clientX,
1624
+ startY: event.clientY,
1625
+ currentX: event.clientX,
1626
+ currentY: event.clientY,
1627
+ startTime: performance.now(),
1628
+ dragging: false,
1629
+ transitionStarted: false
1630
+ };
1631
+ try {
1632
+ this.setPointerCapture(event.pointerId);
1633
+ } catch {
1634
+ }
1635
+ };
1636
+ handleSwipeGesturePointerMove = (event) => {
1637
+ const pointer = this.swipeGesturePointer;
1638
+ if (!pointer || pointer.pointerId !== event.pointerId) {
1639
+ return;
1640
+ }
1641
+ pointer.currentX = event.clientX;
1642
+ pointer.currentY = event.clientY;
1643
+ const deltaX = this.getSwipeGestureDeltaX(pointer);
1644
+ const deltaY = pointer.currentY - pointer.startY;
1645
+ const absX = Math.abs(deltaX);
1646
+ const absY = Math.abs(deltaY);
1647
+ if (!pointer.dragging && absY > 12 && absY > absX) {
1648
+ this.cancelSwipeGesturePointer(event.pointerId);
1649
+ return;
1650
+ }
1651
+ if (deltaX < -this.swipeGestureThreshold) {
1652
+ this.cancelSwipeGesture(event.pointerId);
1653
+ return;
1654
+ }
1655
+ if (!pointer.dragging && deltaX > this.swipeGestureThreshold && absX > absY) {
1656
+ pointer.dragging = true;
1657
+ pointer.transitionStarted = this.controller.beginInteractiveBack({ direction: "back" });
1658
+ if (!pointer.transitionStarted) {
1659
+ this.cancelSwipeGesturePointer(event.pointerId);
1660
+ return;
1661
+ }
1662
+ }
1663
+ if (pointer.dragging && pointer.transitionStarted) {
1664
+ if (event.cancelable) event.preventDefault();
1665
+ const width = Math.max(this.getBoundingClientRect().width, 1);
1666
+ this.controller.stepInteractiveBack(deltaX / width);
1667
+ }
1668
+ };
1669
+ handleSwipeGesturePointerEnd = (event) => {
1670
+ const pointer = this.swipeGesturePointer;
1671
+ if (!pointer || pointer.pointerId !== event.pointerId) {
1672
+ return;
1673
+ }
1674
+ pointer.currentX = event.clientX;
1675
+ pointer.currentY = event.clientY;
1676
+ const deltaX = this.getSwipeGestureDeltaX(pointer);
1677
+ const elapsed = Math.max(performance.now() - pointer.startTime, 1);
1678
+ const velocityX = deltaX / elapsed;
1679
+ const width = Math.max(this.getBoundingClientRect().width, 1);
1680
+ const step = deltaX / width;
1681
+ const shouldCommit = pointer.dragging && pointer.transitionStarted && velocityX >= 0 && (velocityX > this.swipeGestureMinimumVelocity || deltaX > width / 2);
1682
+ const missing = shouldCommit ? 1 - step : step;
1683
+ const missingDistance = Math.max(missing, 0) * width;
1684
+ const releaseDuration = missingDistance > 5 && Math.abs(velocityX) > 0 ? Math.min(missingDistance / Math.abs(velocityX), 540) : 0;
1685
+ this.releaseSwipeGesturePointer(event.pointerId);
1686
+ void this.finishSwipeGestureBack(shouldCommit, releaseDuration);
1687
+ };
1688
+ handleSwipeGesturePointerCancel = (event) => {
1689
+ this.cancelSwipeGesture(event.pointerId);
1690
+ };
1691
+ cancelSwipeGesturePointer(pointerId) {
1692
+ this.releaseSwipeGesturePointer(pointerId);
1693
+ }
1694
+ releaseSwipeGesturePointer(pointerId) {
1695
+ if (this.swipeGesturePointer?.pointerId !== pointerId) {
1696
+ return;
1697
+ }
1698
+ try {
1699
+ this.releasePointerCapture(pointerId);
1700
+ } catch {
1701
+ }
1702
+ this.swipeGesturePointer = null;
1703
+ }
1704
+ cancelSwipeGesture(pointerId) {
1705
+ const pointer = this.swipeGesturePointer;
1706
+ if (!pointer || pointer.pointerId !== pointerId) {
1707
+ return;
1708
+ }
1709
+ this.releaseSwipeGesturePointer(pointerId);
1710
+ if (pointer.transitionStarted) {
1711
+ void this.finishSwipeGestureBack(false, 0);
1712
+ }
1713
+ }
1714
+ async finishSwipeGestureBack(shouldComplete, releaseDuration) {
1715
+ const shouldUseHistory = shouldComplete && typeof window !== "undefined" && window.history.length > 1;
1716
+ await this.controller.endInteractiveBack(shouldComplete, releaseDuration, !shouldUseHistory);
1717
+ if (!shouldComplete) {
1718
+ return;
1719
+ }
1720
+ if (shouldUseHistory) {
1721
+ this.skipNextHistoryBackTransition = true;
1722
+ this.dataset.direction = "back";
1723
+ window.history.back();
1724
+ return;
1725
+ }
1726
+ if (!this.options.keepInDom) {
1727
+ this.cleanupOldPages();
1728
+ }
1729
+ }
1730
+ };
1731
+ if (typeof customElements !== "undefined" && !customElements.get("cap-router-outlet")) {
1732
+ customElements.define("cap-router-outlet", CapRouterOutlet);
1733
+ }
1734
+
1735
+ // src/components/cap-page.ts
1736
+ var CapPage = class extends HTMLElement {
1737
+ _lifecycle = {};
1738
+ _isActive = false;
1739
+ static get observedAttributes() {
1740
+ return ["key", "cache-scroll"];
1741
+ }
1742
+ constructor() {
1743
+ super();
1744
+ const shadow = this.attachShadow({ mode: "open" });
1745
+ shadow.innerHTML = `
1746
+ <style>
1747
+ :host {
1748
+ display: flex;
1749
+ flex-direction: column;
1750
+ width: 100%;
1751
+ height: 100%;
1752
+ position: relative;
1753
+ overflow: hidden;
1754
+ background: var(--cap-page-background, Canvas);
1755
+ color: var(--cap-page-color, CanvasText);
1756
+ }
1757
+
1758
+ :host(.cap-transition-active) {
1759
+ overflow: visible;
1760
+ }
1761
+
1762
+ .header-container {
1763
+ flex-shrink: 0;
1764
+ position: relative;
1765
+ z-index: 10;
1766
+ }
1767
+
1768
+ .content-container {
1769
+ flex: 1;
1770
+ position: relative;
1771
+ overflow: hidden;
1772
+ }
1773
+
1774
+ .footer-container {
1775
+ flex-shrink: 0;
1776
+ position: relative;
1777
+ z-index: 10;
1778
+ }
1779
+
1780
+ ::slotted([slot="header"]),
1781
+ ::slotted([data-cap-header]) {
1782
+ display: block;
1783
+ }
1784
+
1785
+ ::slotted([slot="content"]),
1786
+ ::slotted([data-cap-content]) {
1787
+ display: block;
1788
+ height: 100%;
1789
+ overflow: auto;
1790
+ }
1791
+
1792
+ ::slotted([slot="footer"]),
1793
+ ::slotted([data-cap-footer]) {
1794
+ display: block;
1795
+ }
1796
+ </style>
1797
+
1798
+ <div class="header-container" part="header">
1799
+ <slot name="header"></slot>
1800
+ </div>
1801
+
1802
+ <div class="content-container" part="content">
1803
+ <slot name="content"></slot>
1804
+ <slot></slot>
1805
+ </div>
1806
+
1807
+ <div class="footer-container" part="footer">
1808
+ <slot name="footer"></slot>
1809
+ </div>
1810
+ `;
1811
+ }
1812
+ connectedCallback() {
1813
+ this.markTransitionElements();
1814
+ this.dispatchEvent(
1815
+ new CustomEvent("cap-page-connected", {
1816
+ bubbles: true,
1817
+ composed: true,
1818
+ detail: { page: this }
1819
+ })
1820
+ );
1821
+ }
1822
+ disconnectedCallback() {
1823
+ this.dispatchEvent(
1824
+ new CustomEvent("cap-page-disconnected", {
1825
+ bubbles: true,
1826
+ composed: true,
1827
+ detail: { page: this }
1828
+ })
1829
+ );
1830
+ }
1831
+ /**
1832
+ * Mark child elements for transition controller
1833
+ */
1834
+ markTransitionElements() {
1835
+ const header = this.querySelector('[slot="header"]');
1836
+ if (header) {
1837
+ header.setAttribute("data-cap-header", "");
1838
+ }
1839
+ const content = this.querySelector('[slot="content"]');
1840
+ if (content) {
1841
+ content.setAttribute("data-cap-content", "");
1842
+ }
1843
+ const footer = this.querySelector('[slot="footer"]');
1844
+ if (footer) {
1845
+ footer.setAttribute("data-cap-footer", "");
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Set lifecycle callbacks
1850
+ */
1851
+ set lifecycle(callbacks) {
1852
+ this._lifecycle = callbacks;
1853
+ }
1854
+ get lifecycle() {
1855
+ return this._lifecycle;
1856
+ }
1857
+ /**
1858
+ * Check if page is active
1859
+ */
1860
+ get isActive() {
1861
+ return this._isActive;
1862
+ }
1863
+ /**
1864
+ * Called when page will enter view
1865
+ */
1866
+ async willEnter(event) {
1867
+ await this._lifecycle.onWillEnter?.(event);
1868
+ this.dispatchEvent(
1869
+ new CustomEvent("cap-will-enter", {
1870
+ bubbles: true,
1871
+ detail: event
1872
+ })
1873
+ );
1874
+ }
1875
+ /**
1876
+ * Called when page has entered view
1877
+ */
1878
+ async didEnter(event) {
1879
+ this._isActive = true;
1880
+ await this._lifecycle.onDidEnter?.(event);
1881
+ this.dispatchEvent(
1882
+ new CustomEvent("cap-did-enter", {
1883
+ bubbles: true,
1884
+ detail: event
1885
+ })
1886
+ );
1887
+ }
1888
+ /**
1889
+ * Called when page will leave view
1890
+ */
1891
+ async willLeave(event) {
1892
+ await this._lifecycle.onWillLeave?.(event);
1893
+ this.dispatchEvent(
1894
+ new CustomEvent("cap-will-leave", {
1895
+ bubbles: true,
1896
+ detail: event
1897
+ })
1898
+ );
1899
+ }
1900
+ /**
1901
+ * Called when page has left view
1902
+ */
1903
+ async didLeave(event) {
1904
+ this._isActive = false;
1905
+ await this._lifecycle.onDidLeave?.(event);
1906
+ this.dispatchEvent(
1907
+ new CustomEvent("cap-did-leave", {
1908
+ bubbles: true,
1909
+ detail: event
1910
+ })
1911
+ );
1912
+ }
1913
+ /**
1914
+ * Get the header element
1915
+ */
1916
+ get headerElement() {
1917
+ return this.querySelector('[slot="header"], [data-cap-header]');
1918
+ }
1919
+ /**
1920
+ * Get the content element
1921
+ */
1922
+ get contentElement() {
1923
+ return this.querySelector('[slot="content"], [data-cap-content]');
1924
+ }
1925
+ /**
1926
+ * Get the footer element
1927
+ */
1928
+ get footerElement() {
1929
+ return this.querySelector('[slot="footer"], [data-cap-footer]');
1930
+ }
1931
+ /**
1932
+ * Save scroll position
1933
+ */
1934
+ saveScrollPosition() {
1935
+ const content = this.contentElement;
1936
+ if (!content) return null;
1937
+ return {
1938
+ x: content.scrollLeft,
1939
+ y: content.scrollTop
1940
+ };
1941
+ }
1942
+ /**
1943
+ * Restore scroll position
1944
+ */
1945
+ restoreScrollPosition(position) {
1946
+ const content = this.contentElement;
1947
+ if (!content) return;
1948
+ content.scrollLeft = position.x;
1949
+ content.scrollTop = position.y;
1950
+ }
1951
+ };
1952
+ if (typeof customElements !== "undefined" && !customElements.get("cap-page")) {
1953
+ customElements.define("cap-page", CapPage);
1954
+ }
1955
+
1956
+ // src/components/cap-header.ts
1957
+ var CapHeader = class extends HTMLElement {
1958
+ static get observedAttributes() {
1959
+ return ["translucent", "collapse"];
1960
+ }
1961
+ constructor() {
1962
+ super();
1963
+ }
1964
+ connectedCallback() {
1965
+ this.style.display = "block";
1966
+ this.style.position = "relative";
1967
+ this.style.zIndex = "10";
1968
+ this.setAttribute("data-cap-header", "");
1969
+ if (this.parentElement?.tagName === "CAP-PAGE" && !this.hasAttribute("slot")) {
1970
+ this.setAttribute("slot", "header");
1971
+ }
1972
+ }
1973
+ attributeChangedCallback(name, _oldValue, newValue) {
1974
+ switch (name) {
1975
+ case "translucent":
1976
+ this.dataset.translucent = newValue !== null ? "true" : "false";
1977
+ break;
1978
+ case "collapse":
1979
+ this.dataset.collapse = newValue;
1980
+ break;
1981
+ }
1982
+ }
1983
+ /**
1984
+ * Get the current height of the header
1985
+ */
1986
+ get height() {
1987
+ return this.offsetHeight;
1988
+ }
1989
+ };
1990
+ if (typeof customElements !== "undefined" && !customElements.get("cap-header")) {
1991
+ customElements.define("cap-header", CapHeader);
1992
+ }
1993
+
1994
+ // src/components/cap-content.ts
1995
+ var CapContent = class extends HTMLElement {
1996
+ _scrollPosition = { x: 0, y: 0 };
1997
+ static get observedAttributes() {
1998
+ return ["fullscreen", "scroll-x", "scroll-y"];
1999
+ }
2000
+ constructor() {
2001
+ super();
2002
+ }
2003
+ connectedCallback() {
2004
+ this.style.display = "block";
2005
+ this.style.position = "relative";
2006
+ this.style.flex = "1";
2007
+ this.style.overflow = "auto";
2008
+ this.style.overscrollBehavior = "contain";
2009
+ this.style.webkitOverflowScrolling = "touch";
2010
+ this.setAttribute("data-cap-content", "");
2011
+ if (this.parentElement?.tagName === "CAP-PAGE" && !this.hasAttribute("slot")) {
2012
+ this.setAttribute("slot", "content");
2013
+ }
2014
+ this.addEventListener("scroll", this.handleScroll.bind(this), { passive: true });
2015
+ }
2016
+ disconnectedCallback() {
2017
+ this.removeEventListener("scroll", this.handleScroll.bind(this));
2018
+ }
2019
+ attributeChangedCallback(name, _oldValue, newValue) {
2020
+ switch (name) {
2021
+ case "fullscreen":
2022
+ this.dataset.fullscreen = newValue !== null ? "true" : "false";
2023
+ break;
2024
+ case "scroll-x":
2025
+ this.style.overflowX = newValue === "false" ? "hidden" : "auto";
2026
+ break;
2027
+ case "scroll-y":
2028
+ this.style.overflowY = newValue === "false" ? "hidden" : "auto";
2029
+ break;
2030
+ }
2031
+ }
2032
+ /**
2033
+ * Handle scroll events
2034
+ */
2035
+ handleScroll() {
2036
+ this._scrollPosition = {
2037
+ x: this.scrollLeft,
2038
+ y: this.scrollTop
2039
+ };
2040
+ this.dispatchEvent(
2041
+ new CustomEvent("cap-scroll", {
2042
+ bubbles: true,
2043
+ detail: this._scrollPosition
2044
+ })
2045
+ );
2046
+ }
2047
+ /**
2048
+ * Get current scroll position
2049
+ */
2050
+ get scrollPosition() {
2051
+ return { ...this._scrollPosition };
2052
+ }
2053
+ /**
2054
+ * Save current scroll position
2055
+ */
2056
+ saveScrollPosition() {
2057
+ this._scrollPosition = {
2058
+ x: this.scrollLeft,
2059
+ y: this.scrollTop
2060
+ };
2061
+ return { ...this._scrollPosition };
2062
+ }
2063
+ /**
2064
+ * Restore scroll position
2065
+ */
2066
+ restoreScrollPosition(position) {
2067
+ const pos = position || this._scrollPosition;
2068
+ this.scrollLeft = pos.x;
2069
+ this.scrollTop = pos.y;
2070
+ }
2071
+ /**
2072
+ * Scroll to top
2073
+ */
2074
+ scrollToTop(smooth = true) {
2075
+ this.scrollTo({
2076
+ top: 0,
2077
+ behavior: smooth ? "smooth" : "instant"
2078
+ });
2079
+ }
2080
+ /**
2081
+ * Scroll to bottom
2082
+ */
2083
+ scrollToBottom(smooth = true) {
2084
+ this.scrollTo({
2085
+ top: this.scrollHeight,
2086
+ behavior: smooth ? "smooth" : "instant"
2087
+ });
2088
+ }
2089
+ /**
2090
+ * Scroll to element
2091
+ */
2092
+ scrollToElement(element, smooth = true) {
2093
+ element.scrollIntoView({
2094
+ behavior: smooth ? "smooth" : "instant",
2095
+ block: "start"
2096
+ });
2097
+ }
2098
+ };
2099
+ if (typeof customElements !== "undefined" && !customElements.get("cap-content")) {
2100
+ customElements.define("cap-content", CapContent);
2101
+ }
2102
+
2103
+ // src/components/cap-footer.ts
2104
+ var CapFooter = class extends HTMLElement {
2105
+ static get observedAttributes() {
2106
+ return ["translucent"];
2107
+ }
2108
+ constructor() {
2109
+ super();
2110
+ }
2111
+ connectedCallback() {
2112
+ this.style.display = "block";
2113
+ this.style.position = "relative";
2114
+ this.style.zIndex = "10";
2115
+ this.setAttribute("data-cap-footer", "");
2116
+ if (this.parentElement?.tagName === "CAP-PAGE" && !this.hasAttribute("slot")) {
2117
+ this.setAttribute("slot", "footer");
2118
+ }
2119
+ }
2120
+ attributeChangedCallback(name, _oldValue, newValue) {
2121
+ switch (name) {
2122
+ case "translucent":
2123
+ this.dataset.translucent = newValue !== null ? "true" : "false";
2124
+ break;
2125
+ }
2126
+ }
2127
+ /**
2128
+ * Get the current height of the footer
2129
+ */
2130
+ get height() {
2131
+ return this.offsetHeight;
2132
+ }
2133
+ };
2134
+ if (typeof customElements !== "undefined" && !customElements.get("cap-footer")) {
2135
+ customElements.define("cap-footer", CapFooter);
2136
+ }
2137
+
2138
+ // src/solid/index.ts
2139
+ var globalController = null;
2140
+ var globalDirection = "forward";
2141
+ function initTransitions(config = {}) {
2142
+ globalController = createTransitionController(config);
2143
+ return globalController;
2144
+ }
2145
+ function getController() {
2146
+ if (!globalController) {
2147
+ globalController = createTransitionController();
2148
+ }
2149
+ return globalController;
2150
+ }
2151
+ function getDirection() {
2152
+ return globalDirection;
2153
+ }
2154
+ function setDirection(direction) {
2155
+ globalDirection = direction;
2156
+ if (typeof document !== "undefined") {
2157
+ for (const outlet of document.querySelectorAll("cap-router-outlet")) {
2158
+ outlet.dataset.direction = direction;
2159
+ }
2160
+ }
2161
+ }
2162
+ function setupRouterOutlet(element, options = {}) {
2163
+ const { keepInDom = true, maxCached = 10, platform = "auto", duration, swipeGesture } = options;
2164
+ element.setAttribute("platform", platform);
2165
+ if (duration) element.setAttribute("duration", String(duration));
2166
+ element.setAttribute("keep-in-dom", String(keepInDom));
2167
+ element.setAttribute("max-cached", String(maxCached));
2168
+ if (swipeGesture !== void 0) element.setAttribute("swipe-gesture", String(swipeGesture));
2169
+ }
2170
+ function setupPage(element, callbacks) {
2171
+ const handleWillEnter = (e) => {
2172
+ callbacks?.onWillEnter?.(e.detail);
2173
+ };
2174
+ const handleDidEnter = (e) => {
2175
+ callbacks?.onDidEnter?.(e.detail);
2176
+ };
2177
+ const handleWillLeave = (e) => {
2178
+ callbacks?.onWillLeave?.(e.detail);
2179
+ };
2180
+ const handleDidLeave = (e) => {
2181
+ callbacks?.onDidLeave?.(e.detail);
2182
+ };
2183
+ element.addEventListener("cap-will-enter", handleWillEnter);
2184
+ element.addEventListener("cap-did-enter", handleDidEnter);
2185
+ element.addEventListener("cap-will-leave", handleWillLeave);
2186
+ element.addEventListener("cap-did-leave", handleDidLeave);
2187
+ return () => {
2188
+ element.removeEventListener("cap-will-enter", handleWillEnter);
2189
+ element.removeEventListener("cap-did-enter", handleDidEnter);
2190
+ element.removeEventListener("cap-will-leave", handleWillLeave);
2191
+ element.removeEventListener("cap-did-leave", handleDidLeave);
2192
+ };
2193
+ }
2194
+ function createTransitionNavigate(navigate) {
2195
+ return (to, direction = "forward") => {
2196
+ setDirection(direction);
2197
+ navigate(to);
2198
+ };
2199
+ }
2200
+ export {
2201
+ TransitionController,
2202
+ createTransitionNavigate,
2203
+ getController,
2204
+ getDirection,
2205
+ initTransitions,
2206
+ setDirection,
2207
+ setupPage,
2208
+ setupRouterOutlet
2209
+ };
2210
+ //# sourceMappingURL=index.mjs.map