@nexus_js/runtime 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/cache.d.ts +78 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +132 -0
- package/dist/cache.js.map +1 -0
- package/dist/dev-mode.d.ts +60 -0
- package/dist/dev-mode.d.ts.map +1 -0
- package/dist/dev-mode.js +89 -0
- package/dist/dev-mode.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/island.d.ts +29 -0
- package/dist/island.d.ts.map +1 -0
- package/dist/island.js +161 -0
- package/dist/island.js.map +1 -0
- package/dist/navigation.d.ts +97 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +390 -0
- package/dist/navigation.js.map +1 -0
- package/dist/optimistic.d.ts +67 -0
- package/dist/optimistic.d.ts.map +1 -0
- package/dist/optimistic.js +104 -0
- package/dist/optimistic.js.map +1 -0
- package/dist/prefetch-ai.d.ts +88 -0
- package/dist/prefetch-ai.d.ts.map +1 -0
- package/dist/prefetch-ai.js +277 -0
- package/dist/prefetch-ai.js.map +1 -0
- package/dist/runes.d.ts +61 -0
- package/dist/runes.d.ts.map +1 -0
- package/dist/runes.js +155 -0
- package/dist/runes.js.map +1 -0
- package/dist/store.d.ts +123 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +267 -0
- package/dist/store.js.map +1 -0
- package/dist/sync.d.ts +49 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +156 -0
- package/dist/sync.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus SPA Navigation — Server-Driven DOM Morphing.
|
|
3
|
+
*
|
|
4
|
+
* The core question: "How do you handle state rehydration when the user
|
|
5
|
+
* navigates between routes without a page refresh?"
|
|
6
|
+
*
|
|
7
|
+
* ─── THE STRATEGY: Server-Driven Morphing ─────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* We DON'T do client-side routing with a VDOM (React Router style).
|
|
10
|
+
* We DON'T do full page reloads (MPA style).
|
|
11
|
+
* We DO: fetch the new page HTML from the server, morph the current DOM
|
|
12
|
+
* into the new DOM surgically, and preserve island state across the transition.
|
|
13
|
+
*
|
|
14
|
+
* Why morphing instead of innerHTML replacement?
|
|
15
|
+
* innerHTML = destroy all islands + re-create = flicker + lost state
|
|
16
|
+
* morphing = surgical diff + update only what changed = smooth + preserved
|
|
17
|
+
*
|
|
18
|
+
* ─── THE ALGORITHM ────────────────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* 1. User clicks <a href="/new-route"> (or calls navigate('/new-route'))
|
|
21
|
+
* 2. Intercept: preventDefault(), push to history
|
|
22
|
+
* 3. Fetch: GET /_nexus/navigate?path=/new-route
|
|
23
|
+
* Server returns: { html, head, islands, props }
|
|
24
|
+
* 4. Diff <head>: update title, meta, canonical (via @nexus_js/head)
|
|
25
|
+
* 5. Morph <body>: walk the DOM tree
|
|
26
|
+
* a. Same node type + same [data-nx-key] → update attributes + children
|
|
27
|
+
* b. Island node ([data-nexus-island]) with same component path:
|
|
28
|
+
* → PRESERVE the island (skip re-hydration, keep its $state)
|
|
29
|
+
* c. New island in new page → mount fresh
|
|
30
|
+
* d. Removed island → destroy() cleanly
|
|
31
|
+
* 6. Update URL, fire 'nexus:navigate' event, restore scroll
|
|
32
|
+
*
|
|
33
|
+
* ─── STATE PRESERVATION RULES ─────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* Island state is preserved when ALL of these match:
|
|
36
|
+
* - Same [data-nexus-component] path (same component file)
|
|
37
|
+
* - Same [data-nx-key] if provided (explicit identity)
|
|
38
|
+
* - OR same position in the layout tree (implicit identity)
|
|
39
|
+
*
|
|
40
|
+
* State is reset when:
|
|
41
|
+
* - The component file changes
|
|
42
|
+
* - The user explicitly passes key={Math.random()} (force reset)
|
|
43
|
+
* - The island is in a part of the layout that changed
|
|
44
|
+
*
|
|
45
|
+
* ─── LAYOUT PERSISTENCE ───────────────────────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* Shared layouts (+layout.nx) are identified by [data-nx-layout="path"].
|
|
48
|
+
* The morphing algorithm skips islands inside unchanged layouts,
|
|
49
|
+
* achieving the SvelteKit-style "layout persistence" where the
|
|
50
|
+
* sidebar counter doesn't reset when navigating between pages.
|
|
51
|
+
*
|
|
52
|
+
* ─── PREFETCHING ──────────────────────────────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* Links get automatic prefetching based on:
|
|
55
|
+
* - data-nx-prefetch="hover" → prefetch on mouseenter (default)
|
|
56
|
+
* - data-nx-prefetch="load" → prefetch on page load
|
|
57
|
+
* - data-nx-prefetch="visible" → prefetch when link enters viewport
|
|
58
|
+
* - data-nx-prefetch="false" → disable prefetch for this link
|
|
59
|
+
*/
|
|
60
|
+
export interface NavigateOptions {
|
|
61
|
+
/** Replace current history entry instead of pushing */
|
|
62
|
+
replace?: boolean;
|
|
63
|
+
/** Scroll to top after navigation (default: true) */
|
|
64
|
+
scroll?: boolean;
|
|
65
|
+
/** Override prefetch cache */
|
|
66
|
+
noCache?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface NavigationState {
|
|
69
|
+
url: string;
|
|
70
|
+
pending: boolean;
|
|
71
|
+
error: string | null;
|
|
72
|
+
}
|
|
73
|
+
/** Reactive navigation state — use in islands to show loading indicators */
|
|
74
|
+
export declare const navigation: {
|
|
75
|
+
url: {
|
|
76
|
+
value: string;
|
|
77
|
+
};
|
|
78
|
+
pending: {
|
|
79
|
+
value: boolean;
|
|
80
|
+
};
|
|
81
|
+
error: {
|
|
82
|
+
value: string | null;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Programmatic SPA navigation.
|
|
87
|
+
* Equivalent to `<a href="/path">` but callable from island code.
|
|
88
|
+
*/
|
|
89
|
+
export declare function navigate(path: string, opts?: NavigateOptions): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Prefetches a route in the background.
|
|
92
|
+
* Call this in `$effect` or on mouse hover for instant navigation.
|
|
93
|
+
*/
|
|
94
|
+
export declare function prefetch(path: string): void;
|
|
95
|
+
/** Bootstrap: call once when the page loads */
|
|
96
|
+
export declare function initNavigation(): void;
|
|
97
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAQH,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,4EAA4E;AAC5E,eAAO,MAAM,UAAU;;;;;;;;;;CAItB,CAAC;AAEF;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAgB3C;AAID,+CAA+C;AAC/C,wBAAgB,cAAc,IAAI,IAAI,CAcrC"}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus SPA Navigation — Server-Driven DOM Morphing.
|
|
3
|
+
*
|
|
4
|
+
* The core question: "How do you handle state rehydration when the user
|
|
5
|
+
* navigates between routes without a page refresh?"
|
|
6
|
+
*
|
|
7
|
+
* ─── THE STRATEGY: Server-Driven Morphing ─────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* We DON'T do client-side routing with a VDOM (React Router style).
|
|
10
|
+
* We DON'T do full page reloads (MPA style).
|
|
11
|
+
* We DO: fetch the new page HTML from the server, morph the current DOM
|
|
12
|
+
* into the new DOM surgically, and preserve island state across the transition.
|
|
13
|
+
*
|
|
14
|
+
* Why morphing instead of innerHTML replacement?
|
|
15
|
+
* innerHTML = destroy all islands + re-create = flicker + lost state
|
|
16
|
+
* morphing = surgical diff + update only what changed = smooth + preserved
|
|
17
|
+
*
|
|
18
|
+
* ─── THE ALGORITHM ────────────────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* 1. User clicks <a href="/new-route"> (or calls navigate('/new-route'))
|
|
21
|
+
* 2. Intercept: preventDefault(), push to history
|
|
22
|
+
* 3. Fetch: GET /_nexus/navigate?path=/new-route
|
|
23
|
+
* Server returns: { html, head, islands, props }
|
|
24
|
+
* 4. Diff <head>: update title, meta, canonical (via @nexus_js/head)
|
|
25
|
+
* 5. Morph <body>: walk the DOM tree
|
|
26
|
+
* a. Same node type + same [data-nx-key] → update attributes + children
|
|
27
|
+
* b. Island node ([data-nexus-island]) with same component path:
|
|
28
|
+
* → PRESERVE the island (skip re-hydration, keep its $state)
|
|
29
|
+
* c. New island in new page → mount fresh
|
|
30
|
+
* d. Removed island → destroy() cleanly
|
|
31
|
+
* 6. Update URL, fire 'nexus:navigate' event, restore scroll
|
|
32
|
+
*
|
|
33
|
+
* ─── STATE PRESERVATION RULES ─────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* Island state is preserved when ALL of these match:
|
|
36
|
+
* - Same [data-nexus-component] path (same component file)
|
|
37
|
+
* - Same [data-nx-key] if provided (explicit identity)
|
|
38
|
+
* - OR same position in the layout tree (implicit identity)
|
|
39
|
+
*
|
|
40
|
+
* State is reset when:
|
|
41
|
+
* - The component file changes
|
|
42
|
+
* - The user explicitly passes key={Math.random()} (force reset)
|
|
43
|
+
* - The island is in a part of the layout that changed
|
|
44
|
+
*
|
|
45
|
+
* ─── LAYOUT PERSISTENCE ───────────────────────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* Shared layouts (+layout.nx) are identified by [data-nx-layout="path"].
|
|
48
|
+
* The morphing algorithm skips islands inside unchanged layouts,
|
|
49
|
+
* achieving the SvelteKit-style "layout persistence" where the
|
|
50
|
+
* sidebar counter doesn't reset when navigating between pages.
|
|
51
|
+
*
|
|
52
|
+
* ─── PREFETCHING ──────────────────────────────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* Links get automatic prefetching based on:
|
|
55
|
+
* - data-nx-prefetch="hover" → prefetch on mouseenter (default)
|
|
56
|
+
* - data-nx-prefetch="load" → prefetch on page load
|
|
57
|
+
* - data-nx-prefetch="visible" → prefetch when link enters viewport
|
|
58
|
+
* - data-nx-prefetch="false" → disable prefetch for this link
|
|
59
|
+
*/
|
|
60
|
+
import { hydrateAll } from './island.js';
|
|
61
|
+
import { $state } from './runes.js';
|
|
62
|
+
import { snapshotStore, importStore } from './store.js';
|
|
63
|
+
/** Reactive navigation state — use in islands to show loading indicators */
|
|
64
|
+
export const navigation = {
|
|
65
|
+
url: $state(typeof location !== 'undefined' ? location.href : ''),
|
|
66
|
+
pending: $state(false),
|
|
67
|
+
error: $state(null),
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Programmatic SPA navigation.
|
|
71
|
+
* Equivalent to `<a href="/path">` but callable from island code.
|
|
72
|
+
*/
|
|
73
|
+
export async function navigate(path, opts = {}) {
|
|
74
|
+
await performNavigation(path, opts);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Prefetches a route in the background.
|
|
78
|
+
* Call this in `$effect` or on mouse hover for instant navigation.
|
|
79
|
+
*/
|
|
80
|
+
export function prefetch(path) {
|
|
81
|
+
if (typeof document === 'undefined')
|
|
82
|
+
return;
|
|
83
|
+
if (prefetchCache.has(path))
|
|
84
|
+
return;
|
|
85
|
+
// Use <link rel="prefetch"> for network-level prefetch
|
|
86
|
+
const link = document.createElement('link');
|
|
87
|
+
link.rel = 'prefetch';
|
|
88
|
+
link.href = navigateEndpoint(path);
|
|
89
|
+
link.as = 'fetch';
|
|
90
|
+
link.crossOrigin = 'same-origin';
|
|
91
|
+
document.head.appendChild(link);
|
|
92
|
+
// Also start the JSON fetch to warm up our cache
|
|
93
|
+
fetchRoute(path).then((data) => {
|
|
94
|
+
if (data)
|
|
95
|
+
prefetchCache.set(path, data);
|
|
96
|
+
}).catch(() => { });
|
|
97
|
+
}
|
|
98
|
+
// ── Initialization ────────────────────────────────────────────────────────────
|
|
99
|
+
/** Bootstrap: call once when the page loads */
|
|
100
|
+
export function initNavigation() {
|
|
101
|
+
if (typeof document === 'undefined')
|
|
102
|
+
return;
|
|
103
|
+
// Intercept all link clicks
|
|
104
|
+
document.addEventListener('click', handleLinkClick, { passive: false });
|
|
105
|
+
// Handle browser back/forward
|
|
106
|
+
window.addEventListener('popstate', handlePopState);
|
|
107
|
+
// Setup prefetch observers
|
|
108
|
+
setupPrefetchObservers();
|
|
109
|
+
// Store initial page state for back navigation
|
|
110
|
+
history.replaceState({ nx: true, path: location.pathname }, '', location.href);
|
|
111
|
+
}
|
|
112
|
+
// ── Internal navigation engine ────────────────────────────────────────────────
|
|
113
|
+
const NAVIGATE_ENDPOINT = '/_nexus/navigate';
|
|
114
|
+
const prefetchCache = new Map();
|
|
115
|
+
function navigateEndpoint(path) {
|
|
116
|
+
return `${NAVIGATE_ENDPOINT}?path=${encodeURIComponent(path)}`;
|
|
117
|
+
}
|
|
118
|
+
async function performNavigation(path, opts) {
|
|
119
|
+
if (navigation.pending.value)
|
|
120
|
+
return;
|
|
121
|
+
navigation.pending.value = true;
|
|
122
|
+
navigation.error.value = null;
|
|
123
|
+
try {
|
|
124
|
+
// Checkpoint all persisted island state before leaving the current page
|
|
125
|
+
snapshotStore();
|
|
126
|
+
// Check prefetch cache first
|
|
127
|
+
const cached = !opts.noCache ? prefetchCache.get(path) : null;
|
|
128
|
+
const payload = cached ?? (await fetchRoute(path));
|
|
129
|
+
if (!payload) {
|
|
130
|
+
throw new Error(`Failed to fetch route: ${path}`);
|
|
131
|
+
}
|
|
132
|
+
// Update history
|
|
133
|
+
if (opts.replace) {
|
|
134
|
+
history.replaceState({ nx: true, path }, '', path);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
history.pushState({ nx: true, path }, '', path);
|
|
138
|
+
}
|
|
139
|
+
// Apply the navigation
|
|
140
|
+
await applyNavigation(payload, opts);
|
|
141
|
+
navigation.url.value = location.href;
|
|
142
|
+
// Fire navigation event
|
|
143
|
+
document.dispatchEvent(new CustomEvent('nexus:navigate', { detail: { path, payload } }));
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
navigation.error.value = err instanceof Error ? err.message : String(err);
|
|
147
|
+
console.error('[Nexus Navigation]', err);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
navigation.pending.value = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function fetchRoute(path) {
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(navigateEndpoint(path), {
|
|
156
|
+
headers: {
|
|
157
|
+
'x-nexus-navigate': '1',
|
|
158
|
+
'accept': 'application/json',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok)
|
|
162
|
+
return null;
|
|
163
|
+
return await res.json();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function applyNavigation(payload, opts) {
|
|
170
|
+
// 0. Rehydrate global store from server snapshot (Hydration Miss = 0)
|
|
171
|
+
if (payload.storeSnapshot) {
|
|
172
|
+
importStore(payload.storeSnapshot);
|
|
173
|
+
}
|
|
174
|
+
// 1. Take a snapshot of current islands to preserve state
|
|
175
|
+
const preserved = snapshotIslands();
|
|
176
|
+
// 2. Update <head> (title, meta, etc.)
|
|
177
|
+
applyHeadUpdate(payload.headHTML);
|
|
178
|
+
// 3. Morph <body> — surgical DOM update
|
|
179
|
+
morphBody(payload.html, preserved);
|
|
180
|
+
// 4. Hydrate new islands (skip preserved ones)
|
|
181
|
+
hydrateAll();
|
|
182
|
+
// 5. Restore scroll position
|
|
183
|
+
if (opts.scroll !== false) {
|
|
184
|
+
const hash = location.hash;
|
|
185
|
+
if (hash) {
|
|
186
|
+
const target = document.querySelector(hash);
|
|
187
|
+
if (target) {
|
|
188
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
window.scrollTo({ top: 0, behavior: 'instant' });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function snapshotIslands() {
|
|
197
|
+
const snapshots = new Map();
|
|
198
|
+
document.querySelectorAll('[data-nexus-island]').forEach((el) => {
|
|
199
|
+
const id = el.getAttribute('data-nexus-island') ?? '';
|
|
200
|
+
const componentPath = el.getAttribute('data-nexus-component') ?? '';
|
|
201
|
+
const key = el.getAttribute('data-nx-key') ?? componentPath;
|
|
202
|
+
snapshots.set(key, { id, componentPath, key, element: el });
|
|
203
|
+
});
|
|
204
|
+
return snapshots;
|
|
205
|
+
}
|
|
206
|
+
// ── DOM Morphing ───────────────────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Morphs the current <body> into the new HTML.
|
|
209
|
+
* Preserves islands that survive the navigation.
|
|
210
|
+
*
|
|
211
|
+
* Algorithm: Walk both trees simultaneously.
|
|
212
|
+
* - Same tag + same key → patch attributes, recurse into children
|
|
213
|
+
* - Island with same component → skip (keep existing DOM element)
|
|
214
|
+
* - New node → insert
|
|
215
|
+
* - Removed node → remove (call island.destroy() if it's an island)
|
|
216
|
+
*/
|
|
217
|
+
function morphBody(newHTML, preserved) {
|
|
218
|
+
const parser = new DOMParser();
|
|
219
|
+
const newDoc = parser.parseFromString(newHTML, 'text/html');
|
|
220
|
+
const newBody = newDoc.body;
|
|
221
|
+
const oldBody = document.body;
|
|
222
|
+
morphNode(oldBody, newBody, preserved);
|
|
223
|
+
}
|
|
224
|
+
function morphNode(oldNode, newNode, preserved) {
|
|
225
|
+
// Update attributes
|
|
226
|
+
patchAttributes(oldNode, newNode);
|
|
227
|
+
const oldChildren = [...oldNode.childNodes];
|
|
228
|
+
const newChildren = [...newNode.childNodes];
|
|
229
|
+
let oldIdx = 0;
|
|
230
|
+
let newIdx = 0;
|
|
231
|
+
while (newIdx < newChildren.length) {
|
|
232
|
+
const newChild = newChildren[newIdx];
|
|
233
|
+
const oldChild = oldChildren[oldIdx];
|
|
234
|
+
if (!newChild)
|
|
235
|
+
break;
|
|
236
|
+
// Check if this new child is a preserved island
|
|
237
|
+
if (newChild instanceof Element) {
|
|
238
|
+
const newIslandId = newChild.getAttribute('data-nexus-island');
|
|
239
|
+
const newComponentPath = newChild.getAttribute('data-nexus-component');
|
|
240
|
+
const newKey = newChild.getAttribute('data-nx-key') ?? newComponentPath ?? '';
|
|
241
|
+
const snap = preserved.get(newKey);
|
|
242
|
+
if (snap && newComponentPath === snap.componentPath) {
|
|
243
|
+
// PRESERVE: replace new placeholder with existing island element
|
|
244
|
+
if (oldChild !== snap.element) {
|
|
245
|
+
oldNode.insertBefore(snap.element, oldChild ?? null);
|
|
246
|
+
}
|
|
247
|
+
oldIdx++;
|
|
248
|
+
newIdx++;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!oldChild) {
|
|
253
|
+
// New node has more children — append
|
|
254
|
+
oldNode.appendChild(newChild.cloneNode(true));
|
|
255
|
+
newIdx++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (oldChild.nodeType !== newChild.nodeType) {
|
|
259
|
+
// Different type — replace
|
|
260
|
+
oldNode.replaceChild(newChild.cloneNode(true), oldChild);
|
|
261
|
+
oldIdx++;
|
|
262
|
+
newIdx++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (oldChild instanceof Element && newChild instanceof Element) {
|
|
266
|
+
if (oldChild.tagName === newChild.tagName) {
|
|
267
|
+
// Same element — recurse
|
|
268
|
+
morphNode(oldChild, newChild, preserved);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Different tags — replace
|
|
272
|
+
oldNode.replaceChild(newChild.cloneNode(true), oldChild);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (oldChild.nodeType === Node.TEXT_NODE) {
|
|
276
|
+
// Text node — update content
|
|
277
|
+
if (oldChild.textContent !== newChild.textContent) {
|
|
278
|
+
oldChild.textContent = newChild.textContent;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
oldIdx++;
|
|
282
|
+
newIdx++;
|
|
283
|
+
}
|
|
284
|
+
// Remove extra old children
|
|
285
|
+
while (oldIdx < oldChildren.length) {
|
|
286
|
+
const toRemove = oldChildren[oldIdx];
|
|
287
|
+
if (toRemove) {
|
|
288
|
+
// Destroy island if applicable
|
|
289
|
+
const islandId = toRemove instanceof Element
|
|
290
|
+
? toRemove.getAttribute('data-nexus-island')
|
|
291
|
+
: null;
|
|
292
|
+
if (islandId) {
|
|
293
|
+
toRemove.dispatchEvent(new Event('nexus:destroy'));
|
|
294
|
+
}
|
|
295
|
+
oldNode.removeChild(toRemove);
|
|
296
|
+
}
|
|
297
|
+
oldIdx++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function patchAttributes(oldEl, newEl) {
|
|
301
|
+
// Add/update new attributes
|
|
302
|
+
for (const attr of newEl.attributes) {
|
|
303
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
304
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Remove old attributes not in new element
|
|
308
|
+
for (const attr of [...oldEl.attributes]) {
|
|
309
|
+
if (!newEl.hasAttribute(attr.name)) {
|
|
310
|
+
oldEl.removeAttribute(attr.name);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function applyHeadUpdate(headHTML) {
|
|
315
|
+
if (!headHTML)
|
|
316
|
+
return;
|
|
317
|
+
// Update title
|
|
318
|
+
const titleMatch = /<title>([^<]*)<\/title>/.exec(headHTML);
|
|
319
|
+
if (titleMatch?.[1])
|
|
320
|
+
document.title = titleMatch[1];
|
|
321
|
+
// Remove previous navigation-injected metas (marked with data-nx-nav)
|
|
322
|
+
document.querySelectorAll('[data-nx-nav]').forEach((el) => el.remove());
|
|
323
|
+
// Inject new metas
|
|
324
|
+
const parser = new DOMParser();
|
|
325
|
+
const doc = parser.parseFromString(`<head>${headHTML}</head>`, 'text/html');
|
|
326
|
+
for (const el of doc.head.children) {
|
|
327
|
+
if (el.tagName === 'TITLE')
|
|
328
|
+
continue;
|
|
329
|
+
el.setAttribute('data-nx-nav', '');
|
|
330
|
+
document.head.appendChild(el.cloneNode(true));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ── Event handlers ─────────────────────────────────────────────────────────────
|
|
334
|
+
function handleLinkClick(e) {
|
|
335
|
+
const target = e.target.closest('a');
|
|
336
|
+
if (!target)
|
|
337
|
+
return;
|
|
338
|
+
const href = target.getAttribute('href');
|
|
339
|
+
if (!href)
|
|
340
|
+
return;
|
|
341
|
+
// Skip: external, hash-only, download, target="_blank", data-nx-prefetch="false"
|
|
342
|
+
if (href.startsWith('http') ||
|
|
343
|
+
href.startsWith('mailto:') ||
|
|
344
|
+
href.startsWith('tel:') ||
|
|
345
|
+
href === '#' ||
|
|
346
|
+
target.hasAttribute('download') ||
|
|
347
|
+
target.getAttribute('target') === '_blank' ||
|
|
348
|
+
target.getAttribute('data-nx-prefetch') === 'false' ||
|
|
349
|
+
target.getAttribute('data-nx-external') !== null) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
navigate(href).catch(console.error);
|
|
354
|
+
}
|
|
355
|
+
function handlePopState(e) {
|
|
356
|
+
if (e.state?.nx) {
|
|
357
|
+
navigate(location.pathname, { replace: true, noCache: true }).catch(console.error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function setupPrefetchObservers() {
|
|
361
|
+
// Hover prefetch (default)
|
|
362
|
+
document.addEventListener('mouseover', (e) => {
|
|
363
|
+
const target = e.target.closest('a[href]');
|
|
364
|
+
if (!target)
|
|
365
|
+
return;
|
|
366
|
+
const href = target.getAttribute('href');
|
|
367
|
+
if (!href || href.startsWith('http') || href.startsWith('#'))
|
|
368
|
+
return;
|
|
369
|
+
const prefetchMode = target.getAttribute('data-nx-prefetch') ?? 'hover';
|
|
370
|
+
if (prefetchMode === 'hover' || prefetchMode === '') {
|
|
371
|
+
prefetch(href);
|
|
372
|
+
}
|
|
373
|
+
}, { passive: true });
|
|
374
|
+
// Viewport prefetch
|
|
375
|
+
const visibleObserver = new IntersectionObserver((entries) => {
|
|
376
|
+
for (const entry of entries) {
|
|
377
|
+
if (!entry.isIntersecting)
|
|
378
|
+
continue;
|
|
379
|
+
const target = entry.target;
|
|
380
|
+
const href = target.getAttribute('href') ?? '';
|
|
381
|
+
if (href && !href.startsWith('http'))
|
|
382
|
+
prefetch(href);
|
|
383
|
+
visibleObserver.unobserve(target);
|
|
384
|
+
}
|
|
385
|
+
}, { rootMargin: '100px' });
|
|
386
|
+
document.querySelectorAll('a[data-nx-prefetch="visible"]').forEach((el) => {
|
|
387
|
+
visibleObserver.observe(el);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAmBxD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;IACtB,KAAK,EAAE,MAAM,CAAgB,IAAI,CAAC;CACnC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAY,EACZ,OAAwB,EAAE;IAE1B,MAAM,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAC5C,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IAEpC,uDAAuD;IACvD,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,GAAG,GAAG,UAAU,CAAC;IACtB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC;IAClB,IAAI,CAAC,WAAW,GAAG,aAAa,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAEhC,iDAAiD;IACjD,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QAC7B,IAAI,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AACrB,CAAC;AAED,iFAAiF;AAEjF,+CAA+C;AAC/C,MAAM,UAAU,cAAc;IAC5B,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAE5C,4BAA4B;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAExE,8BAA8B;IAC9B,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IAEpD,2BAA2B;IAC3B,sBAAsB,EAAE,CAAC;IAEzB,+CAA+C;IAC/C,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC;AAED,iFAAiF;AAEjF,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAC7C,MAAM,aAAa,GAAG,IAAI,GAAG,EAA6B,CAAC;AAS3D,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,GAAG,iBAAiB,SAAS,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;AACjE,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,IAAY,EAAE,IAAqB;IAClE,IAAI,UAAU,CAAC,OAAO,CAAC,KAAK;QAAE,OAAO;IAErC,UAAU,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;IAE9B,IAAI,CAAC;QACH,wEAAwE;QACxE,aAAa,EAAE,CAAC;QAEhB,6BAA6B;QAC7B,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAEnD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QAED,iBAAiB;QACjB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAClD,CAAC;QAED,uBAAuB;QACvB,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAErC,UAAU,CAAC,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC;QAErC,wBAAwB;QACxB,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,CACjE,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1E,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC;IAC3C,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;IACnC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,IAAY;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE;YAC9C,OAAO,EAAE;gBACP,kBAAkB,EAAE,GAAG;gBACvB,QAAQ,EAAE,kBAAkB;aAC7B;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QACzB,OAAO,MAAM,GAAG,CAAC,IAAI,EAAuB,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,OAA0B,EAC1B,IAAqB;IAErB,sEAAsE;IACtE,IAAK,OAA0D,CAAC,aAAa,EAAE,CAAC;QAC9E,WAAW,CAAE,OAAyD,CAAC,aAAa,CAAC,CAAC;IACxF,CAAC;IAED,0DAA0D;IAC1D,MAAM,SAAS,GAAG,eAAe,EAAE,CAAC;IAEpC,uCAAuC;IACvC,eAAe,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAElC,wCAAwC;IACxC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAEnC,+CAA+C;IAC/C,UAAU,EAAE,CAAC;IAEb,6BAA6B;IAC7B,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;QAC3B,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;AACH,CAAC;AAWD,SAAS,eAAe;IACtB,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEpD,QAAQ,CAAC,gBAAgB,CAAC,qBAAqB,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QAC9D,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC;QACtD,MAAM,aAAa,GAAG,EAAE,CAAC,YAAY,CAAC,sBAAsB,CAAC,IAAI,EAAE,CAAC;QACpE,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC;QAE5D,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,kFAAkF;AAElF;;;;;;;;;GASG;AACH,SAAS,SAAS,CAAC,OAAe,EAAE,SAAsC;IACxE,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC;IAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC;IAE9B,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,SAAS,CAChB,OAAgB,EAChB,OAAgB,EAChB,SAAsC;IAEtC,oBAAoB;IACpB,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAElC,MAAM,WAAW,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAE5C,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,OAAO,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAErC,IAAI,CAAC,QAAQ;YAAE,MAAM;QAErB,gDAAgD;QAChD,IAAI,QAAQ,YAAY,OAAO,EAAE,CAAC;YAChC,MAAM,WAAW,GAAG,QAAQ,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC;YAC/D,MAAM,gBAAgB,GAAG,QAAQ,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;YACvE,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,gBAAgB,IAAI,EAAE,CAAC;YAE9E,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,IAAI,IAAI,gBAAgB,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;gBACpD,iEAAiE;gBACjE,IAAI,QAAQ,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;oBAC9B,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,IAAI,IAAI,CAAC,CAAC;gBACvD,CAAC;gBACD,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC;gBACT,SAAS;YACX,CAAC;QACH,CAAC;QAED,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,sCAAsC;YACtC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC9C,MAAM,EAAE,CAAC;YACT,SAAS;QACX,CAAC;QAED,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,EAAE,CAAC;YAC5C,2BAA2B;YAC3B,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;YACzD,MAAM,EAAE,CAAC;YACT,MAAM,EAAE,CAAC;YACT,SAAS;QACX,CAAC;QAED,IAAI,QAAQ,YAAY,OAAO,IAAI,QAAQ,YAAY,OAAO,EAAE,CAAC;YAC/D,IAAI,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;gBAC1C,yBAAyB;gBACzB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,2BAA2B;gBAC3B,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;aAAM,IAAI,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YAChD,6BAA6B;YAC7B,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,CAAC,WAAW,EAAE,CAAC;gBAClD,QAAQ,CAAC,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC;YAC9C,CAAC;QACH,CAAC;QAED,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,CAAC;IACX,CAAC;IAED,4BAA4B;IAC5B,OAAO,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,QAAQ,EAAE,CAAC;YACb,+BAA+B;YAC/B,MAAM,QAAQ,GAAG,QAAQ,YAAY,OAAO;gBAC1C,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,mBAAmB,CAAC;gBAC5C,CAAC,CAAC,IAAI,CAAC;YACT,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;YACrD,CAAC;YACD,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAc,EAAE,KAAc;IACrD,4BAA4B;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YACjD,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IACD,2CAA2C;IAC3C,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,IAAI,CAAC,QAAQ;QAAE,OAAO;IAEtB,eAAe;IACf,MAAM,UAAU,GAAG,yBAAyB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5D,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC;QAAE,QAAQ,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAEpD,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;IAExE,mBAAmB;IACnB,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,eAAe,CAAC,SAAS,QAAQ,SAAS,EAAE,WAAW,CAAC,CAAC;IAC5E,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QACrC,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QACnC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,kFAAkF;AAElF,SAAS,eAAe,CAAC,CAAa;IACpC,MAAM,MAAM,GAAI,CAAC,CAAC,MAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO;IAElB,iFAAiF;IACjF,IACE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QACvB,IAAI,KAAK,GAAG;QACZ,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC;QAC/B,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,QAAQ;QAC1C,MAAM,CAAC,YAAY,CAAC,kBAAkB,CAAC,KAAK,OAAO;QACnD,MAAM,CAAC,YAAY,CAAC,kBAAkB,CAAC,KAAK,IAAI,EAChD,CAAC;QACD,OAAO;IACT,CAAC;IAED,CAAC,CAAC,cAAc,EAAE,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,CAAgB;IACtC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC;QAChB,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACrF,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE;QAC3C,MAAM,MAAM,GAAI,CAAC,CAAC,MAAkB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO;QACrE,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC,kBAAkB,CAAC,IAAI,OAAO,CAAC;QACxE,IAAI,YAAY,KAAK,OAAO,IAAI,YAAY,KAAK,EAAE,EAAE,CAAC;YACpD,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtB,oBAAoB;IACpB,MAAM,eAAe,GAAG,IAAI,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,cAAc;gBAAE,SAAS;YACpC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAiB,CAAC;YACvC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC/C,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;YACrD,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IAE5B,QAAQ,CAAC,gBAAgB,CAAC,+BAA+B,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACxE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Optimistic UI — `$optimistic` rune.
|
|
3
|
+
*
|
|
4
|
+
* Updates the UI instantly before the server responds.
|
|
5
|
+
* Rolls back automatically if the server action fails.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const likes = $state(post.likes);
|
|
9
|
+
*
|
|
10
|
+
* function handleLike() {
|
|
11
|
+
* $optimistic(
|
|
12
|
+
* likes,
|
|
13
|
+
* () => likePost(post.id), // Server Action
|
|
14
|
+
* post.likes // rollback value
|
|
15
|
+
* );
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* What happens:
|
|
19
|
+
* 1. `likes.value` jumps to `likes.value + 1` immediately (UI updates)
|
|
20
|
+
* 2. Server Action runs in background
|
|
21
|
+
* 3a. Success → server value is confirmed (or replaced with server result)
|
|
22
|
+
* 3b. Failure → `likes.value` rolls back to `post.likes`
|
|
23
|
+
*/
|
|
24
|
+
export type OptimisticUpdate<T> = {
|
|
25
|
+
/** Current displayed value (may be optimistic) */
|
|
26
|
+
value: T;
|
|
27
|
+
/** True while the server action is in flight */
|
|
28
|
+
pending: boolean;
|
|
29
|
+
/** Error message if the action failed */
|
|
30
|
+
error: string | null;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Wraps a server action with optimistic UI.
|
|
34
|
+
* The signal is updated immediately; rolls back on error.
|
|
35
|
+
*
|
|
36
|
+
* @param signal - A $state signal to update optimistically
|
|
37
|
+
* @param action - Async function that calls the server (returns new value or void)
|
|
38
|
+
* @param rollback - Value to restore if the action fails (defaults to current value)
|
|
39
|
+
*/
|
|
40
|
+
export declare function $optimistic<T>(signal: {
|
|
41
|
+
value: T;
|
|
42
|
+
}, action: () => Promise<T | void>, rollback?: T): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Creates a full optimistic action controller with pending/error state.
|
|
45
|
+
* More powerful than bare $optimistic — gives you loading indicators too.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const likeAction = createOptimistic(likes, () => likePost(post.id));
|
|
49
|
+
* <button onclick={likeAction.execute} disabled={likeAction.pending}>
|
|
50
|
+
* {likeAction.pending ? '...' : likes.value} likes
|
|
51
|
+
* </button>
|
|
52
|
+
*/
|
|
53
|
+
export declare function createOptimistic<T>(signal: {
|
|
54
|
+
value: T;
|
|
55
|
+
}, action: (current: T) => Promise<T | void>, opts?: {
|
|
56
|
+
optimisticValue?: (current: T) => T;
|
|
57
|
+
onError?: (err: unknown) => void;
|
|
58
|
+
}): {
|
|
59
|
+
execute: () => Promise<void>;
|
|
60
|
+
pending: {
|
|
61
|
+
value: boolean;
|
|
62
|
+
};
|
|
63
|
+
error: {
|
|
64
|
+
value: string | null;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=optimistic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic.d.ts","sourceRoot":"","sources":["../src/optimistic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,kDAAkD;IAClD,KAAK,EAAE,CAAC,CAAC;IACT,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,yCAAyC;IACzC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,MAAM,EAAE;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,EACpB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAC/B,QAAQ,CAAC,EAAE,CAAC,GACX,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,MAAM,EAAE;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,EACpB,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EACzC,IAAI,GAAE;IACJ,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7B,GACL;IACD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5B,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACjC,CAgCA"}
|