@ecopages/react-router 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +212 -0
  4. package/browser.d.ts +13 -0
  5. package/browser.js +11 -0
  6. package/browser.ts +17 -0
  7. package/package.json +42 -0
  8. package/src/adapter.d.ts +28 -0
  9. package/src/adapter.js +22 -0
  10. package/src/adapter.ts +48 -0
  11. package/src/context.d.ts +16 -0
  12. package/src/context.js +11 -0
  13. package/src/context.ts +25 -0
  14. package/src/head-morpher.d.ts +15 -0
  15. package/src/head-morpher.js +94 -0
  16. package/src/head-morpher.ts +170 -0
  17. package/src/index.d.ts +14 -0
  18. package/src/index.js +13 -0
  19. package/src/index.ts +21 -0
  20. package/src/manage-scroll.d.ts +17 -0
  21. package/src/manage-scroll.js +25 -0
  22. package/src/manage-scroll.ts +47 -0
  23. package/src/navigation.d.ts +65 -0
  24. package/src/navigation.js +120 -0
  25. package/src/navigation.ts +247 -0
  26. package/src/props-script.d.ts +11 -0
  27. package/src/props-script.js +11 -0
  28. package/src/props-script.ts +19 -0
  29. package/src/router.d.ts +73 -0
  30. package/src/router.js +225 -0
  31. package/src/router.ts +348 -0
  32. package/src/scroll-persist.d.ts +40 -0
  33. package/src/scroll-persist.js +57 -0
  34. package/src/scroll-persist.ts +96 -0
  35. package/src/styles.css +200 -0
  36. package/src/types.d.ts +49 -0
  37. package/src/types.js +12 -0
  38. package/src/types.ts +64 -0
  39. package/src/view-transition-manager.d.ts +5 -0
  40. package/src/view-transition-manager.js +16 -0
  41. package/src/view-transition-manager.ts +30 -0
  42. package/src/view-transition-utils.d.ts +13 -0
  43. package/src/view-transition-utils.js +60 -0
  44. package/src/view-transition-utils.ts +95 -0
package/src/styles.css ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Default View Transition CSS for EcoPages React Router
3
+ *
4
+ * NOTE: This file is not exported from the package.
5
+ * JSR does not support CSS exports yet: https://github.com/jsr-io/jsr/issues/293
6
+ *
7
+ * Use the ecopages npm package instead:
8
+ * @import 'ecopages/css/view-transitions.css';
9
+ */
10
+
11
+ /* Disable root animation to prevent full page flash during transitions */
12
+ ::view-transition-old(root),
13
+ ::view-transition-new(root) {
14
+ animation: none;
15
+ }
16
+
17
+ /* Shared element transitions animate position/size smoothly */
18
+ ::view-transition-group(*) {
19
+ animation-duration: 180ms;
20
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
21
+ }
22
+
23
+ /* Prevent transitioning elements from affecting layout of non-transitioning elements */
24
+ ::view-transition-image-pair(*) {
25
+ isolation: isolate;
26
+ }
27
+
28
+ /* Respect reduced motion preference */
29
+ @media (prefers-reduced-motion: reduce) {
30
+ ::view-transition-group(*),
31
+ ::view-transition-old(*),
32
+ ::view-transition-new(*) {
33
+ animation: none !important;
34
+ }
35
+ }
36
+
37
+ /* Slide transition for elements with view-transition-name: eco-slide */
38
+ ::view-transition-image-pair(eco-slide) {
39
+ isolation: isolate;
40
+ }
41
+
42
+ ::view-transition-old(eco-slide),
43
+ ::view-transition-new(eco-slide) {
44
+ animation-duration: 180ms;
45
+ animation-timing-function: ease-out;
46
+ mix-blend-mode: normal;
47
+ }
48
+
49
+ ::view-transition-old(eco-slide) {
50
+ animation-name: eco-slide-out;
51
+ }
52
+
53
+ ::view-transition-new(eco-slide) {
54
+ animation-name: eco-slide-in;
55
+ }
56
+
57
+ @keyframes eco-slide-out {
58
+ from {
59
+ opacity: 1;
60
+ transform: translateX(0);
61
+ }
62
+ to {
63
+ opacity: 0;
64
+ transform: translateX(-30px);
65
+ }
66
+ }
67
+
68
+ @keyframes eco-slide-in {
69
+ from {
70
+ opacity: 0;
71
+ transform: translateX(30px);
72
+ }
73
+ to {
74
+ opacity: 1;
75
+ transform: translateX(0);
76
+ }
77
+ }
78
+
79
+ /* Persistent elements should not animate */
80
+ [data-eco-persist] {
81
+ view-transition-name: none;
82
+ }
83
+
84
+ /* Declarative transition attributes */
85
+ [data-eco-transition='slide'] {
86
+ view-transition-name: eco-slide;
87
+ }
88
+ [data-eco-transition='fade'] {
89
+ view-transition-name: eco-fade;
90
+ }
91
+ [data-eco-transition='zoom'] {
92
+ view-transition-name: eco-zoom;
93
+ }
94
+ [data-eco-transition='slide-up'] {
95
+ view-transition-name: eco-slide-up;
96
+ }
97
+ [data-eco-transition='slide-down'] {
98
+ view-transition-name: eco-slide-down;
99
+ }
100
+
101
+ /* FADE */
102
+ ::view-transition-old(eco-fade) {
103
+ animation: eco-fade-out 180ms ease-out both;
104
+ }
105
+ ::view-transition-new(eco-fade) {
106
+ animation: eco-fade-in 180ms ease-out both;
107
+ }
108
+
109
+ /* ZOOM */
110
+ ::view-transition-image-pair(eco-zoom) {
111
+ isolation: isolate;
112
+ }
113
+ ::view-transition-old(eco-zoom) {
114
+ animation: eco-zoom-out 180ms ease-in both;
115
+ }
116
+ ::view-transition-new(eco-zoom) {
117
+ animation: eco-zoom-in 180ms ease-out both;
118
+ }
119
+ @keyframes eco-zoom-out {
120
+ from {
121
+ opacity: 1;
122
+ transform: scale(1);
123
+ }
124
+ to {
125
+ opacity: 0;
126
+ transform: scale(0.9);
127
+ }
128
+ }
129
+ @keyframes eco-zoom-in {
130
+ from {
131
+ opacity: 0;
132
+ transform: scale(0.9);
133
+ }
134
+ to {
135
+ opacity: 1;
136
+ transform: scale(1);
137
+ }
138
+ }
139
+
140
+ /* SLIDE UP */
141
+ ::view-transition-image-pair(eco-slide-up) {
142
+ isolation: isolate;
143
+ }
144
+ ::view-transition-old(eco-slide-up) {
145
+ animation: eco-slide-up-out 180ms ease-in both;
146
+ }
147
+ ::view-transition-new(eco-slide-up) {
148
+ animation: eco-slide-up-in 180ms ease-out both;
149
+ }
150
+ @keyframes eco-slide-up-out {
151
+ from {
152
+ opacity: 1;
153
+ transform: translateY(0);
154
+ }
155
+ to {
156
+ opacity: 0;
157
+ transform: translateY(-30px);
158
+ }
159
+ }
160
+ @keyframes eco-slide-up-in {
161
+ from {
162
+ opacity: 0;
163
+ transform: translateY(30px);
164
+ }
165
+ to {
166
+ opacity: 1;
167
+ transform: translateY(0);
168
+ }
169
+ }
170
+
171
+ /* SLIDE DOWN */
172
+ ::view-transition-image-pair(eco-slide-down) {
173
+ isolation: isolate;
174
+ }
175
+ ::view-transition-old(eco-slide-down) {
176
+ animation: eco-slide-down-out 180ms ease-in both;
177
+ }
178
+ ::view-transition-new(eco-slide-down) {
179
+ animation: eco-slide-down-in 180ms ease-out both;
180
+ }
181
+ @keyframes eco-slide-down-out {
182
+ from {
183
+ opacity: 1;
184
+ transform: translateY(0);
185
+ }
186
+ to {
187
+ opacity: 0;
188
+ transform: translateY(30px);
189
+ }
190
+ }
191
+ @keyframes eco-slide-down-in {
192
+ from {
193
+ opacity: 0;
194
+ transform: translateY(-30px);
195
+ }
196
+ to {
197
+ opacity: 1;
198
+ transform: translateY(0);
199
+ }
200
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Configuration options for the EcoRouter
3
+ */
4
+ export interface EcoRouterOptions {
5
+ /**
6
+ * CSS selector for links to intercept
7
+ * @default 'a[href]'
8
+ */
9
+ linkSelector?: string;
10
+ /**
11
+ * Attribute that forces a full page reload when present on a link
12
+ * @default 'data-eco-reload'
13
+ */
14
+ reloadAttribute?: string;
15
+ /**
16
+ * Whether to use the View Transitions API for page animations.
17
+ * When enabled and supported, page transitions animate using CSS view-transition pseudo-elements.
18
+ * Falls back to instant swap if not supported by the browser.
19
+ * @default true
20
+ */
21
+ viewTransitions?: boolean;
22
+ /**
23
+ * Enable debug logging to console
24
+ * @default false
25
+ */
26
+ debug?: boolean;
27
+ /**
28
+ * Scroll behavior after navigation:
29
+ * - 'top': Always scroll to top (default)
30
+ * - 'preserve': Keep current scroll position
31
+ * - 'auto': Scroll to top only when pathname changes
32
+ */
33
+ scrollBehavior?: 'top' | 'preserve' | 'auto';
34
+ /**
35
+ * Whether to use smooth scrolling during navigation.
36
+ * If true, uses 'smooth' behavior. If false, uses 'instant' behavior.
37
+ * @default false
38
+ */
39
+ smoothScroll?: boolean;
40
+ /**
41
+ * Keep layouts mounted between navigations.
42
+ * When enabled, if consecutive pages share the same layout component,
43
+ * the layout stays mounted and only the page content re-renders.
44
+ * This preserves layout state including scroll positions.
45
+ * @default true
46
+ */
47
+ persistLayouts?: boolean;
48
+ }
49
+ export declare const DEFAULT_OPTIONS: Required<EcoRouterOptions>;
package/src/types.js ADDED
@@ -0,0 +1,12 @@
1
+ const DEFAULT_OPTIONS = {
2
+ linkSelector: "a[href]",
3
+ reloadAttribute: "data-eco-reload",
4
+ viewTransitions: true,
5
+ debug: false,
6
+ scrollBehavior: "top",
7
+ smoothScroll: false,
8
+ persistLayouts: true
9
+ };
10
+ export {
11
+ DEFAULT_OPTIONS
12
+ };
package/src/types.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Configuration options for the EcoRouter
3
+ */
4
+ export interface EcoRouterOptions {
5
+ /**
6
+ * CSS selector for links to intercept
7
+ * @default 'a[href]'
8
+ */
9
+ linkSelector?: string;
10
+
11
+ /**
12
+ * Attribute that forces a full page reload when present on a link
13
+ * @default 'data-eco-reload'
14
+ */
15
+ reloadAttribute?: string;
16
+
17
+ /**
18
+ * Whether to use the View Transitions API for page animations.
19
+ * When enabled and supported, page transitions animate using CSS view-transition pseudo-elements.
20
+ * Falls back to instant swap if not supported by the browser.
21
+ * @default true
22
+ */
23
+ viewTransitions?: boolean;
24
+
25
+ /**
26
+ * Enable debug logging to console
27
+ * @default false
28
+ */
29
+ debug?: boolean;
30
+
31
+ /**
32
+ * Scroll behavior after navigation:
33
+ * - 'top': Always scroll to top (default)
34
+ * - 'preserve': Keep current scroll position
35
+ * - 'auto': Scroll to top only when pathname changes
36
+ */
37
+ scrollBehavior?: 'top' | 'preserve' | 'auto';
38
+
39
+ /**
40
+ * Whether to use smooth scrolling during navigation.
41
+ * If true, uses 'smooth' behavior. If false, uses 'instant' behavior.
42
+ * @default false
43
+ */
44
+ smoothScroll?: boolean;
45
+
46
+ /**
47
+ * Keep layouts mounted between navigations.
48
+ * When enabled, if consecutive pages share the same layout component,
49
+ * the layout stays mounted and only the page content re-renders.
50
+ * This preserves layout state including scroll positions.
51
+ * @default true
52
+ */
53
+ persistLayouts?: boolean;
54
+ }
55
+
56
+ export const DEFAULT_OPTIONS: Required<EcoRouterOptions> = {
57
+ linkSelector: 'a[href]',
58
+ reloadAttribute: 'data-eco-reload',
59
+ viewTransitions: true,
60
+ debug: false,
61
+ scrollBehavior: 'top',
62
+ smoothScroll: false,
63
+ persistLayouts: true,
64
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Manages View Transition API integration
3
+ * @module
4
+ */
5
+ export declare function withViewTransition(callback: () => void): Promise<void>;
@@ -0,0 +1,16 @@
1
+ function isViewTransitionSupported() {
2
+ return "startViewTransition" in document;
3
+ }
4
+ async function withViewTransition(callback) {
5
+ if (!isViewTransitionSupported()) {
6
+ callback();
7
+ return;
8
+ }
9
+ const transition = document.startViewTransition(() => {
10
+ callback();
11
+ });
12
+ await transition.finished;
13
+ }
14
+ export {
15
+ withViewTransition
16
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Manages View Transition API integration
3
+ * @module
4
+ */
5
+
6
+ type ViewTransitionDocument = Document & {
7
+ startViewTransition: (callback: () => void | Promise<void>) => {
8
+ finished: Promise<void>;
9
+ ready: Promise<void>;
10
+ updateCallbackDone: Promise<void>;
11
+ skipTransition(): void;
12
+ };
13
+ };
14
+
15
+ function isViewTransitionSupported(): boolean {
16
+ return 'startViewTransition' in document;
17
+ }
18
+
19
+ export async function withViewTransition(callback: () => void): Promise<void> {
20
+ if (!isViewTransitionSupported()) {
21
+ callback();
22
+ return;
23
+ }
24
+
25
+ const transition = (document as ViewTransitionDocument).startViewTransition(() => {
26
+ callback();
27
+ });
28
+
29
+ await transition.finished;
30
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * View transition utilities for applying transition names from data attributes.
3
+ * @module
4
+ */
5
+ /**
6
+ * Applies view-transition-name CSS property to elements with data-view-transition attribute.
7
+ * Also handles data-view-transition-animate="morph" (default) logic and custom duration.
8
+ */
9
+ export declare function applyViewTransitionNames(): void;
10
+ /**
11
+ * Clears view-transition-name CSS property from all elements.
12
+ */
13
+ export declare function clearViewTransitionNames(): void;
@@ -0,0 +1,60 @@
1
+ const VIEW_TRANSITION_ATTR = "data-view-transition";
2
+ const VIEW_TRANSITION_ANIMATE_ATTR = "data-view-transition-animate";
3
+ const VIEW_TRANSITION_DURATION_ATTR = "data-view-transition-duration";
4
+ function applyViewTransitionNames() {
5
+ const elements = document.querySelectorAll(`[${VIEW_TRANSITION_ATTR}]`);
6
+ const morphNames = [];
7
+ const customDurations = [];
8
+ elements.forEach((el) => {
9
+ const name = el.getAttribute(VIEW_TRANSITION_ATTR);
10
+ if (name) {
11
+ el.style.viewTransitionName = name;
12
+ const animate = el.getAttribute(VIEW_TRANSITION_ANIMATE_ATTR);
13
+ if (animate !== "fade") {
14
+ morphNames.push(name);
15
+ }
16
+ const duration = el.getAttribute(VIEW_TRANSITION_DURATION_ATTR);
17
+ if (duration) {
18
+ customDurations.push({ name, duration });
19
+ }
20
+ }
21
+ });
22
+ if (morphNames.length > 0 || customDurations.length > 0) {
23
+ injectDynamicStyles(morphNames, customDurations);
24
+ }
25
+ }
26
+ function injectDynamicStyles(morphNames, customDurations) {
27
+ let styleEl = document.getElementById("eco-vt-dynamic-styles");
28
+ if (!styleEl) {
29
+ styleEl = document.createElement("style");
30
+ styleEl.id = "eco-vt-dynamic-styles";
31
+ styleEl.setAttribute("data-eco-persist", "");
32
+ document.head.appendChild(styleEl);
33
+ }
34
+ const morphCss = morphNames.map(
35
+ (name) => `
36
+ ::view-transition-old(${name}) { display: none !important; }
37
+ ::view-transition-new(${name}) { animation: none !important; opacity: 1 !important; }
38
+ `
39
+ ).join("\n");
40
+ const durationCss = customDurations.map(
41
+ ({ name, duration }) => `
42
+ ::view-transition-group(${name}) { animation-duration: ${duration} !important; }
43
+ `
44
+ ).join("\n");
45
+ styleEl.textContent = morphCss + "\n" + durationCss;
46
+ }
47
+ function clearViewTransitionNames() {
48
+ const elements = document.querySelectorAll(`[${VIEW_TRANSITION_ATTR}]`);
49
+ elements.forEach((el) => {
50
+ el.style.viewTransitionName = "";
51
+ });
52
+ const styleEl = document.getElementById("eco-vt-dynamic-styles");
53
+ if (styleEl) {
54
+ styleEl.textContent = "";
55
+ }
56
+ }
57
+ export {
58
+ applyViewTransitionNames,
59
+ clearViewTransitionNames
60
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * View transition utilities for applying transition names from data attributes.
3
+ * @module
4
+ */
5
+
6
+ const VIEW_TRANSITION_ATTR = 'data-view-transition';
7
+ const VIEW_TRANSITION_ANIMATE_ATTR = 'data-view-transition-animate';
8
+ const VIEW_TRANSITION_DURATION_ATTR = 'data-view-transition-duration';
9
+
10
+ /**
11
+ * Applies view-transition-name CSS property to elements with data-view-transition attribute.
12
+ * Also handles data-view-transition-animate="morph" (default) logic and custom duration.
13
+ */
14
+ export function applyViewTransitionNames(): void {
15
+ const elements = document.querySelectorAll(`[${VIEW_TRANSITION_ATTR}]`);
16
+ const morphNames: string[] = [];
17
+ const customDurations: { name: string; duration: string }[] = [];
18
+
19
+ elements.forEach((el) => {
20
+ const name = el.getAttribute(VIEW_TRANSITION_ATTR);
21
+ if (name) {
22
+ (el as HTMLElement).style.viewTransitionName = name;
23
+ /**
24
+ * By default, we apply a clean geometric morph (no cross-fade/ghosting).
25
+ * The 'fade' value is reserved for opting out of this behavior.
26
+ */
27
+ const animate = el.getAttribute(VIEW_TRANSITION_ANIMATE_ATTR);
28
+ if (animate !== 'fade') {
29
+ morphNames.push(name);
30
+ }
31
+
32
+ const duration = el.getAttribute(VIEW_TRANSITION_DURATION_ATTR);
33
+ if (duration) {
34
+ customDurations.push({ name, duration });
35
+ }
36
+ }
37
+ });
38
+
39
+ if (morphNames.length > 0 || customDurations.length > 0) {
40
+ injectDynamicStyles(morphNames, customDurations);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Injects dynamic CSS to hide old snapshots and apply custom durations.
46
+ */
47
+ function injectDynamicStyles(morphNames: string[], customDurations: { name: string; duration: string }[]) {
48
+ let styleEl = document.getElementById('eco-vt-dynamic-styles');
49
+ if (!styleEl) {
50
+ styleEl = document.createElement('style');
51
+ styleEl.id = 'eco-vt-dynamic-styles';
52
+ /**
53
+ * Persistence is required to prevent the head-morpher from removing this style tag during navigation.
54
+ */
55
+ styleEl.setAttribute('data-eco-persist', '');
56
+ document.head.appendChild(styleEl);
57
+ }
58
+
59
+ const morphCss = morphNames
60
+ .map(
61
+ (name) => `
62
+ ::view-transition-old(${name}) { display: none !important; }
63
+ ::view-transition-new(${name}) { animation: none !important; opacity: 1 !important; }
64
+ `,
65
+ )
66
+ .join('\n');
67
+
68
+ const durationCss = customDurations
69
+ .map(
70
+ ({ name, duration }) => `
71
+ ::view-transition-group(${name}) { animation-duration: ${duration} !important; }
72
+ `,
73
+ )
74
+ .join('\n');
75
+
76
+ styleEl.textContent = morphCss + '\n' + durationCss;
77
+ }
78
+
79
+ /**
80
+ * Clears view-transition-name CSS property from all elements.
81
+ */
82
+ export function clearViewTransitionNames(): void {
83
+ const elements = document.querySelectorAll(`[${VIEW_TRANSITION_ATTR}]`);
84
+ elements.forEach((el) => {
85
+ (el as HTMLElement).style.viewTransitionName = '';
86
+ });
87
+
88
+ /**
89
+ * Cleanup dynamic styles to ensure a clean slate for the next transition.
90
+ */
91
+ const styleEl = document.getElementById('eco-vt-dynamic-styles');
92
+ if (styleEl) {
93
+ styleEl.textContent = '';
94
+ }
95
+ }