@excom/spa-route 0.1.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.
Files changed (38) hide show
  1. package/.rush/temp/chunked-rush-logs/spa-route.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/spa-route.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/spa-route.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +5 -0
  15. package/index.css +5 -0
  16. package/index.ts +29 -0
  17. package/package.json +48 -0
  18. package/rush-logs/spa-route.apply-exports.cache.log +1 -0
  19. package/rush-logs/spa-route.apply-exports.log +1 -0
  20. package/rush-logs/spa-route.build_docs.cache.log +1 -0
  21. package/rush-logs/spa-route.build_docs.log +1 -0
  22. package/rush-logs/spa-route.build_package-metas.cache.log +1 -0
  23. package/rush-logs/spa-route.build_package-metas.log +1 -0
  24. package/spa-a.ts +124 -0
  25. package/spa-manager.ts +605 -0
  26. package/spa-route.ts +322 -0
  27. package/src/spa-route.css +24 -0
  28. package/src/utils.ts +31 -0
  29. package/support/custom-elements.json +953 -0
  30. package/support/dist-docs/spa-a.md +40 -0
  31. package/support/dist-docs/spa-manager.md +66 -0
  32. package/support/dist-docs/spa-route.md +408 -0
  33. package/support/docs/README.md +314 -0
  34. package/support/package-meta.json +789 -0
  35. package/support/tests/spa-navigation.test.ts +1076 -0
  36. package/support/tests/spa-route.test.ts +766 -0
  37. package/support/tests/spa-title.test.ts +225 -0
  38. package/tsconfig.json +5 -0
package/spa-manager.ts ADDED
@@ -0,0 +1,605 @@
1
+ import type {
2
+ SpaRoute as TSpaRoute,
3
+ SpaRouteProvisionEvent,
4
+ } from "./spa-route";
5
+ import { type KitRouteData, kitRouter } from "@excom/kit-router";
6
+ import { ConstructorType, Neutron, TEvent } from "@excom/neutron";
7
+ import type {
8
+ RenderableErrorEvent,
9
+ RenderableRenderEvent,
10
+ RenderableUnrenderEvent,
11
+ } from "@excom/renderable-element";
12
+ import { RoutableElement } from "@excom/routable-element";
13
+
14
+ type THTMLSpaRouteElement = typeof TSpaRoute.CustomElement;
15
+
16
+ export type SpaManagerTransitionDetail = {
17
+ transition: ViewTransition;
18
+ };
19
+
20
+ export type SpaManagerWillTransitionEvent = TEvent & {
21
+ type: "spa-manager-will-transition";
22
+ detail: void;
23
+ };
24
+
25
+ export type SpaManagerTransitionEvent = TEvent & {
26
+ type: "spa-manager-transition";
27
+ detail: SpaManagerTransitionDetail;
28
+ };
29
+
30
+ export type SpaManagerRenderedEvent = TEvent & {
31
+ type: "spa-manager-rendered";
32
+ detail: void;
33
+ };
34
+
35
+ export type SpaManagerErrorEvent = TEvent & {
36
+ type: "spa-manager-error";
37
+ detail: unknown;
38
+ };
39
+
40
+ export type SpaManagerPushEvent = TEvent & {
41
+ type: "spa-manager-push";
42
+ detail: void;
43
+ };
44
+
45
+ export type SpaManagerReplaceEvent = TEvent & {
46
+ type: "spa-manager-replace";
47
+ detail: void;
48
+ };
49
+
50
+ export type SpaManagerBackEvent = TEvent & {
51
+ type: "spa-manager-back";
52
+ detail: void;
53
+ };
54
+
55
+ export type SpaManagerForwardEvent = TEvent & {
56
+ type: "spa-manager-forward";
57
+ detail: void;
58
+ };
59
+
60
+ export type SpaManagerRouteListenEvents =
61
+ | SpaRouteProvisionEvent
62
+ | RenderableRenderEvent
63
+ | RenderableUnrenderEvent
64
+ | RenderableErrorEvent;
65
+
66
+ const clamp = (value, min, max) => {
67
+ return Math.min(Math.max(Math.round(value), min), max);
68
+ };
69
+
70
+ /**
71
+ * Coordinates sibling `<spa-route>` children: batches their render / unrender into one View Transition per navigation, re-emits nav lifecycle events, and optionally handles touch edge-swipe back / forward.
72
+ *
73
+ * @summary SPA shell — view transitions, swipe nav, route orchestration.
74
+ *
75
+ * @example
76
+ * <spa-manager overscroll-behavior-x="navigate">
77
+ * <spa-route route-href="/"><template>Home</template></spa-route>
78
+ * <spa-route route-href="/about"><template>About</template></spa-route>
79
+ * </spa-manager>
80
+ *
81
+ * @descendant spa-route - Routes this manager coordinates. Their render lifecycle is intercepted and batched into one view transition per navigation.
82
+ *
83
+ * @fires spa-manager-will-transition - Cancelable. Once per navigation, just before the View Transition starts. `preventDefault()` skips the transition for this navigation.
84
+ * @type SpaManagerWillTransitionEvent
85
+ * @fires spa-manager-transition - After `document.startViewTransition()` is called.
86
+ * @type SpaManagerTransitionEvent
87
+ * @fires spa-manager-rendered - After the transition (or synchronous fallback) finishes and routes are settled.
88
+ * @type SpaManagerRenderedEvent
89
+ * @fires spa-manager-error - Re-emitted when a child fires `spa-route-error`. `event.detail` mirrors the source error.
90
+ * @type SpaManagerErrorEvent
91
+ * @fires spa-manager-push - On `pushState` navigations.
92
+ * @type SpaManagerPushEvent
93
+ * @fires spa-manager-replace - On `replaceState` navigations.
94
+ * @type SpaManagerReplaceEvent
95
+ * @fires spa-manager-back - On `back` navigations.
96
+ * @type SpaManagerBackEvent
97
+ * @fires spa-manager-forward - On `forward` navigations.
98
+ * @type SpaManagerForwardEvent
99
+ *
100
+ * @default-action spa-manager-will-transition - Starts the batched View Transition (or synchronous update when transitions are unavailable / opted out).
101
+ *
102
+ * @listens spa-route-render - Queues the child's render callback for the next batched transition.
103
+ * @type RenderableRenderEvent
104
+ * @listens spa-route-unrender - Queues the child's unrender callback (runs before renders so the outgoing route leaves first).
105
+ * @type RenderableUnrenderEvent
106
+ * @listens spa-route-provision - Queues a same-route param update (reuse) into the next batched transition.
107
+ * @type SpaRouteProvisionEvent
108
+ * @listens spa-route-error - Falls back to a non-transitioned update and re-emits as `spa-manager-error`.
109
+ * @type RenderableErrorEvent
110
+ */
111
+ export const SpaManager = Neutron.compose([
112
+ RoutableElement,
113
+ Neutron({
114
+ tag: "spa-manager",
115
+ props: {
116
+ /**
117
+ * @option
118
+ * Animate the very first activation (cold load) with a View Transition. Off by default so the initial paint is instant.
119
+ */
120
+ transitionFirstRender: Boolean,
121
+ /**
122
+ * @option
123
+ * Delay (ms) before starting the batched View Transition. Gives late
124
+ * sibling render/unrender events time to queue, or room for last-second
125
+ * DOM work. Unset / `null` starts synchronously.
126
+ */
127
+ transitionDelay: Number,
128
+ /**
129
+ * @option
130
+ * Disable View Transitions for every child route.
131
+ */
132
+ noTransition: Boolean,
133
+ /**
134
+ * @option
135
+ * Max wait (ms) for child render promises before forcing the transition to resolve. Raise for slow remote templates.
136
+ * @default 2000
137
+ */
138
+ renderTimeout: {
139
+ type: Number,
140
+ // Safety timeout for an uncaught failure. High so slow template loads survive.
141
+ defaultValue: () => 2000,
142
+ },
143
+ /**
144
+ * @option
145
+ * Touch edge-swipe on touch devices. `none` blocks horizontal overscroll; `navigate` also calls back / forward past the threshold.
146
+ * @values none | navigate
147
+ */
148
+ overscrollBehaviorX: String,
149
+ /**
150
+ * @option
151
+ * Edge inset (px) where a touch start counts as an edge swipe.
152
+ * @default 40
153
+ */
154
+ overscrollXThreshold: {
155
+ type: Number,
156
+ defaultValue: () => 40,
157
+ },
158
+ /**
159
+ * @option
160
+ * Cap retained router history states (scroll positions, transition types, etc.).
161
+ */
162
+ maxStates: Number,
163
+ /**
164
+ * @state
165
+ * Last navigation direction (`push` / `replace` / `back` / `forward`). Pick CSS transition styles from this.
166
+ * @values push | replace | back | forward
167
+ */
168
+ lastMove: String,
169
+ /**
170
+ * @state
171
+ * URL of the currently active route.
172
+ */
173
+ activeUrl: String,
174
+ /**
175
+ * @state
176
+ * A View Transition is in flight.
177
+ */
178
+ isTransitioning: Boolean,
179
+ /**
180
+ * @state
181
+ * At least one render has committed. Gates `transition-first-render`; useful for hiding loading shells.
182
+ */
183
+ hasRendered: Boolean,
184
+ /**
185
+ * @provision
186
+ * Current route payload. Not reflected as an attribute.
187
+ * @type KitRouteData
188
+ */
189
+ provision: Object as unknown as ConstructorType<KitRouteData>,
190
+ router: Object as unknown as ConstructorType<typeof kitRouter>,
191
+ // private
192
+ executingTransition: { type: Boolean, attr: false },
193
+ // the batch right after an unbatched first paint (see pushRouteCallback)
194
+ _firstPaintPending: { type: Boolean, attr: false },
195
+ // the page's own <title>, captured before the first route title lands
196
+ _defaultTitle: { type: String, attr: false },
197
+ renderTimeoutId: {
198
+ type: Number as unknown as ConstructorType<
199
+ ReturnType<typeof setTimeout>
200
+ >,
201
+ attr: false,
202
+ },
203
+ transitionDelayId: {
204
+ type: Number as unknown as ConstructorType<
205
+ ReturnType<typeof setTimeout>
206
+ >,
207
+ attr: false,
208
+ },
209
+ _activeViewTransition: {
210
+ type: Object as unknown as ConstructorType<ViewTransition | null>,
211
+ attr: false,
212
+ },
213
+ _routeCallbacks: Object as unknown as ConstructorType<{
214
+ provision: Array<() => void>;
215
+ render: Array<() => void>;
216
+ unrender: Array<() => void>;
217
+ }>,
218
+ attemptTransitionDebounced: Function,
219
+ swipeTracker: Object as unknown as ConstructorType<{
220
+ identifier: number;
221
+ startX: number;
222
+ startY: number;
223
+ }>,
224
+ routeHref: {
225
+ // Override RoutableElement
226
+ type: RegExp,
227
+ attr: false,
228
+ },
229
+ },
230
+ }),
231
+ ])
232
+ .defineMethods({
233
+ hasCallbacks: ({ _routeCallbacks }) => ({
234
+ returns: Object.values(_routeCallbacks!).some(
235
+ (callbacks) => callbacks.length > 0
236
+ ),
237
+ }),
238
+ pushRouteCallback: (
239
+ element,
240
+ e,
241
+ type: "provision" | "render" | "unrender",
242
+ callback
243
+ ) => {
244
+ e.stopPropagation();
245
+ if (shouldTransition(element, e.target as THTMLSpaRouteElement)) {
246
+ const shouldStartTransition =
247
+ // @ts-ignore TODO defineMethods
248
+ !element.isTransitioning && !element.hasCallbacks();
249
+ const _routeCallbacks = {
250
+ provision: [...(element._routeCallbacks?.provision || [])],
251
+ render: [...(element._routeCallbacks?.render || [])],
252
+ unrender: [...(element._routeCallbacks?.unrender || [])],
253
+ };
254
+ _routeCallbacks[type]!.push(callback);
255
+ e.preventDefault();
256
+ /* Rapid back/forward (or any nav) during a transition: skip the
257
+ current View Transition so `finished` resolves and `doCleanup`
258
+ can drain the newly queued callbacks now. */
259
+ if (element.isTransitioning) {
260
+ element._activeViewTransition?.skipTransition?.();
261
+ }
262
+ return [
263
+ { _routeCallbacks },
264
+ // Start a new transition only when nothing is in flight
265
+ shouldStartTransition && {
266
+ emit: ["spa-manager-will-transition"],
267
+ },
268
+ ];
269
+ } else {
270
+ /* First paint runs unbatched; the rest of the same activation
271
+ (`spa-route-provision`) is batched next, and that batch must not
272
+ start a View Transition — a hard load used to cross-fade the page. */
273
+ return { hasRendered: true, _firstPaintPending: !element.hasRendered };
274
+ }
275
+ },
276
+ // @ts-ignore TODO defineMethods
277
+ doCleanup: ({ _routeCallbacks, hasCallbacks }) => {
278
+ // More callbacks arrived mid-transition: run another transition
279
+ return [
280
+ { _activeViewTransition: null },
281
+ ...(hasCallbacks()
282
+ ? [{ updateRoutes: [true] }]
283
+ : [
284
+ {
285
+ executingTransition: false,
286
+ },
287
+ // Routes have settled: the active set is final
288
+ {
289
+ syncDocumentTitle: [],
290
+ },
291
+ {
292
+ emit: ["spa-manager-rendered"],
293
+ },
294
+ ]),
295
+ ];
296
+ },
297
+ /**
298
+ * `document.title` = the last active descendant route carrying a
299
+ * `document-title`, else the page's own `<title>`.
300
+ */
301
+ syncDocumentTitle: (element) => {
302
+ const parentManager = element.parentElement?.closest(
303
+ "spa-manager"
304
+ ) as unknown as { syncDocumentTitle: () => void } | null;
305
+ // One document, one title: a nested manager computes nothing, it hands
306
+ // the job to the outermost one, which sees every active route.
307
+ if (parentManager) {
308
+ parentManager.syncDocumentTitle();
309
+ return;
310
+ }
311
+ // Attribute reflection is synchronous, but the prop is the source of truth
312
+ const titled = (
313
+ Array.from(
314
+ element.querySelectorAll("spa-route")
315
+ ) as THTMLSpaRouteElement[]
316
+ )
317
+ // Last in document order wins, so a nested route beats its ancestor
318
+ .findLast((route) => route.isActive && route.documentTitle);
319
+ // No route has ever claimed the title: leave the page's own alone
320
+ if (!titled && element._defaultTitle == null) return;
321
+ if (element._defaultTitle == null) {
322
+ element._defaultTitle = document.title;
323
+ }
324
+ const title = titled?.documentTitle ?? element._defaultTitle;
325
+ if (document.title !== title) {
326
+ document.title = title;
327
+ }
328
+ },
329
+ _updateRoutes: (element, doTransition: boolean) => {
330
+ // @ts-ignore TODO defineMethods
331
+ const { doCleanup, provision, fireTransition } = element;
332
+ // Callbacks that arrived mid-transition are picked up by `doCleanup`
333
+ const callbacks = [
334
+ // Order matters: unrender first
335
+ ...(element._routeCallbacks?.unrender || []),
336
+ // then render
337
+ ...(element._routeCallbacks?.render || []),
338
+ // `provision` last: otherwise Quark starts rendering before unrender/render wipes the tree.
339
+ ...(element._routeCallbacks?.provision || []),
340
+ ];
341
+ const runUpdate = () => {
342
+ const promises = callbacks
343
+ .map((cb) => cb())
344
+ .filter((p: any) => p instanceof Promise);
345
+ return new Promise((resolve) => {
346
+ if (!promises.length) {
347
+ resolve(true);
348
+ } else {
349
+ // Aborted renders reject their ready promise; settle either way
350
+ // so a skipped / canceled nav doesn't hang until `renderTimeout`.
351
+ Promise.allSettled(promises).then(() => {
352
+ resolve(true);
353
+ });
354
+ element.renderTimeoutId = setTimeout(() => {
355
+ resolve(true);
356
+ }, element.renderTimeout!);
357
+ }
358
+ });
359
+ };
360
+ const firstPaint = element._firstPaintPending;
361
+ element._firstPaintPending = false;
362
+ // Skip if the browser already has a UA visual transition
363
+ if (
364
+ doTransition &&
365
+ !firstPaint &&
366
+ !provision?.event?.hasUAVisualTransition
367
+ ) {
368
+ element.isTransitioning = true;
369
+ const viewTransition = document.startViewTransition({
370
+ update: runUpdate,
371
+ types: [provision?.move ? "route-" + provision.move : undefined]
372
+ .concat(
373
+ provision?.move === "back"
374
+ ? provision?.next?.ttypes || []
375
+ : provision?.active?.ttypes || []
376
+ )
377
+ .filter((type): type is string => type !== undefined),
378
+ });
379
+ viewTransition.finished.then(() => {
380
+ doCleanup();
381
+ });
382
+ fireTransition(viewTransition);
383
+ } else {
384
+ runUpdate().then(() => {
385
+ doCleanup();
386
+ });
387
+ }
388
+ return {
389
+ _routeCallbacks: {
390
+ provision: [],
391
+ render: [],
392
+ unrender: [],
393
+ },
394
+ };
395
+ },
396
+ resetTransitionDelayId: ({ transitionDelayId }, clear: boolean) => {
397
+ if (clear && transitionDelayId) {
398
+ clearTimeout(transitionDelayId);
399
+ }
400
+ return { transitionDelayId: null };
401
+ },
402
+ updateRoutes: (
403
+ // @ts-ignore TODO defineMethods
404
+ { _updateRoutes, transitionDelay, resetTransitionDelayId },
405
+ doTransition: boolean
406
+ ) => {
407
+ resetTransitionDelayId(true);
408
+ const cb = () => {
409
+ resetTransitionDelayId(false);
410
+ _updateRoutes(doTransition);
411
+ };
412
+ if (transitionDelay == null) {
413
+ cb();
414
+ } else {
415
+ return { transitionDelayId: setTimeout(cb, transitionDelay) };
416
+ }
417
+ },
418
+ fireTransition: (_, viewTransition: ViewTransition) => ({
419
+ _activeViewTransition: viewTransition,
420
+ emit: [
421
+ "spa-manager-transition",
422
+ { detail: { transition: viewTransition } },
423
+ ],
424
+ }),
425
+ routeChanged: ({ activeUrl }, routeData: KitRouteData) => ({
426
+ ...(activeUrl && { lastMove: routeData.move }),
427
+ activeUrl: routeData.active.url,
428
+ provision: routeData,
429
+ ...(routeData.move && { emit: ["spa-manager-" + routeData.move] }),
430
+ }),
431
+ handleTouchStart: ({ overscrollXThreshold }, e: TouchEvent) => {
432
+ if (e.touches.length === 1) {
433
+ const touch = e.touches[0];
434
+ if (
435
+ touch.clientX < overscrollXThreshold ||
436
+ touch.clientX > window.innerWidth - overscrollXThreshold
437
+ ) {
438
+ return {
439
+ swipeTracker: {
440
+ identifier: touch.identifier,
441
+ startX: clamp(touch.clientX, 0, window.innerWidth),
442
+ startY: clamp(touch.clientY, 0, window.innerHeight),
443
+ },
444
+ };
445
+ }
446
+ }
447
+ },
448
+ handleTouchMove: (
449
+ { swipeTracker, overscrollBehaviorX, overscrollXThreshold, router },
450
+ e: TouchEvent
451
+ ) => {
452
+ if (!swipeTracker) return null;
453
+ const touch = Array.from(e.touches).find(
454
+ ({ identifier }) => identifier === swipeTracker.identifier
455
+ );
456
+ if (!touch) return null;
457
+
458
+ const diffX = touch.clientX - swipeTracker.startX;
459
+ const diffY = touch.clientY - swipeTracker.startY;
460
+ const Y_TOLERANCE = 0.4;
461
+ const shouldPrevent =
462
+ Math.abs(diffX) >= 2 &&
463
+ Math.abs(diffX) >= Math.round(diffY * Y_TOLERANCE);
464
+ const shouldNavigate =
465
+ shouldPrevent && overscrollBehaviorX === "navigate";
466
+ const draggedRight =
467
+ // went right
468
+ diffX > 0 &&
469
+ // started before threshold
470
+ swipeTracker.startX < overscrollXThreshold &&
471
+ // ended after threshold
472
+ touch.clientX > overscrollXThreshold;
473
+ const draggedLeft =
474
+ // dragged left
475
+ diffX < 0 &&
476
+ // started before threshold
477
+ swipeTracker.startX > window.innerWidth - overscrollXThreshold &&
478
+ // ended after threshold
479
+ touch.clientX < window.innerWidth - overscrollXThreshold;
480
+
481
+ if (shouldPrevent) {
482
+ e.preventDefault();
483
+ }
484
+ if (shouldNavigate && (draggedRight || draggedLeft)) {
485
+ const method = draggedRight
486
+ ? "back"
487
+ : draggedLeft
488
+ ? "forward"
489
+ : undefined;
490
+ if (method) {
491
+ router![method]();
492
+ return { swipeTracker: null };
493
+ }
494
+ }
495
+ },
496
+ handleTouchEnd: () => {
497
+ return {
498
+ swipeTracker: null,
499
+ };
500
+ },
501
+ })
502
+ .onConstructed(() => ({
503
+ router: kitRouter,
504
+ _routeCallbacks: {
505
+ provision: [],
506
+ render: [],
507
+ unrender: [],
508
+ },
509
+ }))
510
+ .onConnected(
511
+ // Run RoutableElement logic
512
+ ({ wasMounted }) => !wasMounted && { routeHref: new RegExp(".*") }
513
+ )
514
+ .onPropChanged(
515
+ "overscrollBehaviorX",
516
+ ({
517
+ overscrollBehaviorX,
518
+ handleTouchStart,
519
+ handleTouchMove,
520
+ handleTouchEnd,
521
+ }) => {
522
+ // Skip on non-touch devices
523
+ if (!("ontouchstart" in window || navigator.maxTouchPoints > 0))
524
+ return null;
525
+
526
+ const shouldListen = ["none", "navigate"].includes(
527
+ overscrollBehaviorX as string
528
+ );
529
+ const opts = { target: document, passive: false };
530
+ return {
531
+ toggleListeners: [
532
+ ["touchstart", handleTouchStart, shouldListen, opts],
533
+ ["touchmove", handleTouchMove, shouldListen, opts],
534
+ ["touchend", handleTouchEnd, shouldListen, opts],
535
+ ["touchcancel", handleTouchEnd, shouldListen, opts],
536
+ ],
537
+ };
538
+ }
539
+ )
540
+ .onPropChanged("maxStates", ({ maxStates, router }) => {
541
+ router!.MAX_STATES = maxStates ?? router!.DEFAULT_MAX_STATES;
542
+ })
543
+ .onPropSet("executingTransition", () => ({
544
+ updateRoutes: [true],
545
+ }))
546
+ .onPropUnset(
547
+ "executingTransition",
548
+ ({ renderTimeoutId, resetTransitionDelayId, _activeViewTransition }) => {
549
+ if (renderTimeoutId) {
550
+ clearTimeout(renderTimeoutId);
551
+ }
552
+ // @ts-ignore TODO fix `onPropUnset` typing for boolean props
553
+ resetTransitionDelayId(true);
554
+ // @ts-ignore TODO fix `onPropUnset` typing for boolean props
555
+ _activeViewTransition?.skipTransition?.();
556
+ return {
557
+ renderTimeoutId: null,
558
+ _activeViewTransition: null,
559
+ _routeCallbacks: {
560
+ provision: [],
561
+ render: [],
562
+ unrender: [],
563
+ },
564
+ isTransitioning: false,
565
+ };
566
+ }
567
+ )
568
+ .onEvent("spa-route-unrender", (_, e) => ({
569
+ pushRouteCallback: [e, "unrender", e.detail],
570
+ }))
571
+ .onEvent("spa-route-render", (_, e) => ({
572
+ pushRouteCallback: [e, "render", e.detail],
573
+ }))
574
+ .onEvent("spa-route-provision", (_, e) => ({
575
+ pushRouteCallback: [e, "provision", e.detail],
576
+ }))
577
+ .onEventDefault("spa-manager-will-transition", () => ({
578
+ hasRendered: true,
579
+ executingTransition: true,
580
+ }))
581
+ .onEvent("spa-route-error", (_, e) => {
582
+ e.stopPropagation();
583
+ return {
584
+ updateRoutes: [false],
585
+ emit: ["spa-manager-error", { detail: e.detail }],
586
+ };
587
+ });
588
+ const prefersReducedMotion =
589
+ // @ts-ignore make ts unhappy, but this supports legacy browsers
590
+ window.matchMedia?.(`(prefers-reduced-motion: reduce)`) === true ||
591
+ window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches === true;
592
+
593
+ function shouldTransition(
594
+ // typeof SpaManager.CustomElement
595
+ manager: any,
596
+ route: THTMLSpaRouteElement
597
+ ): boolean {
598
+ return (
599
+ !!document.startViewTransition &&
600
+ !prefersReducedMotion &&
601
+ !manager.noTransition &&
602
+ !route.noTransition &&
603
+ (manager.transitionFirstRender || manager.hasRendered)
604
+ );
605
+ }