@void/svelte 0.0.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/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@void/svelte",
3
+ "version": "0.0.0",
4
+ "files": [
5
+ "dist",
6
+ "src/runtime"
7
+ ],
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "svelte": "./src/runtime/index.ts",
12
+ "default": "./src/runtime/index.ts"
13
+ },
14
+ "./plugin": {
15
+ "types": "./dist/plugin.d.mts",
16
+ "import": "./dist/plugin.mjs"
17
+ },
18
+ "./runtime": {
19
+ "svelte": "./src/runtime/index.ts",
20
+ "default": "./src/runtime/index.ts"
21
+ },
22
+ "./App.svelte": {
23
+ "svelte": "./src/runtime/App.svelte",
24
+ "default": "./src/runtime/App.svelte"
25
+ },
26
+ "./prefetch": {
27
+ "default": "./src/runtime/prefetch.ts"
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "dev": "tsdown --watch",
36
+ "typecheck": "tsgo --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "@sveltejs/vite-plugin-svelte": "^7.1.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "catalog:",
43
+ "pathe": "catalog:",
44
+ "svelte": "^5.55.5",
45
+ "tsdown": "catalog:",
46
+ "vite": "catalog:",
47
+ "void": "workspace:*"
48
+ },
49
+ "peerDependencies": {
50
+ "svelte": "^5.0.0",
51
+ "vite": "catalog:",
52
+ "void": "workspace:*"
53
+ }
54
+ }
@@ -0,0 +1,81 @@
1
+ <script>
2
+ import { setContext, onMount, untrack } from "svelte";
3
+ import { createRouterFacade, idleNavigationState } from "void/pages-client";
4
+
5
+ let props = $props();
6
+
7
+ // Extract initial values without reactive tracking.
8
+ // These are one-time snapshots used to seed $state — on the client,
9
+ // store subscriptions in onMount take over reactivity.
10
+ const init = untrack(() => ({
11
+ page: props.page,
12
+ layouts: props.layouts,
13
+ pageProps: props.pageProps,
14
+ shared: props.shared,
15
+ errors: props.errors,
16
+ router: props.router,
17
+ routeUrl: props.routeUrl,
18
+ navigation: props.navigation,
19
+ stores: props.stores,
20
+ }));
21
+
22
+ // Local reactive state — initialized from props, updated by stores on client
23
+ let currentPage = $state(init.page);
24
+ let currentLayouts = $state(init.layouts);
25
+ let currentPageProps = $state(init.pageProps);
26
+ let shared = $state(init.shared);
27
+ let errors = $state(init.errors);
28
+ let routeUrl = $state(init.routeUrl || "/");
29
+ let navigation = $state(init.navigation || idleNavigationState());
30
+ const router = createRouterFacade(init.router, () => routeUrl);
31
+
32
+ // Capitalized alias for dynamic component rendering
33
+ let Page = $derived(currentPage);
34
+
35
+ // Provide context for child components (useShared, useRouter, useForm)
36
+ setContext("__void_shared", {
37
+ get value() { return shared; },
38
+ });
39
+ setContext("__void_errors", {
40
+ get value() { return errors; },
41
+ });
42
+ setContext("__void_router", router);
43
+ setContext("__void_navigation", {
44
+ get value() { return navigation; },
45
+ });
46
+
47
+ // Client mode: subscribe to stores for live updates after hydration.
48
+ // onMount only runs on the client, not during SSR.
49
+ onMount(() => {
50
+ if (!init.stores) {return;}
51
+ const unsubs = [
52
+ init.stores.page.subscribe((v) => { currentPage = v; }),
53
+ init.stores.props.subscribe((v) => { currentPageProps = v; }),
54
+ init.stores.layouts.subscribe((v) => { currentLayouts = v; }),
55
+ init.stores.shared.subscribe((v) => { shared = v; }),
56
+ init.stores.errors.subscribe((v) => { errors = v; }),
57
+ init.stores.route.subscribe((v) => { routeUrl = v; }),
58
+ init.stores.navigation.subscribe((v) => { navigation = v; }),
59
+ ];
60
+ return () => unsubs.forEach((u) => u());
61
+ });
62
+ </script>
63
+
64
+ {#if Page}
65
+ {#if currentLayouts.length > 0}
66
+ {@render layoutChain(currentLayouts, 0)}
67
+ {:else}
68
+ <Page {...currentPageProps} />
69
+ {/if}
70
+ {/if}
71
+
72
+ {#snippet layoutChain(layouts, index)}
73
+ {#if index < layouts.length}
74
+ {@const Layout = layouts[index]}
75
+ <Layout>
76
+ {@render layoutChain(layouts, index + 1)}
77
+ </Layout>
78
+ {:else}
79
+ <Page {...currentPageProps} />
80
+ {/if}
81
+ {/snippet}
@@ -0,0 +1,316 @@
1
+ <script lang="ts">
2
+ import { getContext, onDestroy, onMount, type Snippet } from "svelte";
3
+ import type { VoidRouter } from "void/pages-client";
4
+
5
+ type PrefetchStrategy = "hover" | "click" | "mount" | "visible";
6
+ type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
7
+ type NavigateEvent = { preventDefault(): void };
8
+ type EventHandler<T extends Event> = (event: T) => void;
9
+ type PrefetchOptions = {
10
+ cacheFor?: number | string | [string, string];
11
+ method?: LinkMethod;
12
+ };
13
+
14
+ let {
15
+ href,
16
+ method = "GET",
17
+ data = undefined,
18
+ preserveScroll = false,
19
+ preserveState = false,
20
+ replace = false,
21
+ reloadDocument = false,
22
+ viewTransition = undefined,
23
+ prefetch: prefetchProp = false,
24
+ cacheFor = undefined,
25
+ onNavigate = undefined,
26
+ children,
27
+ onclick: onclickProp = undefined,
28
+ onmouseenter: onMouseEnterProp = undefined,
29
+ onmouseleave: onMouseLeaveProp = undefined,
30
+ onfocus: onFocusProp = undefined,
31
+ onmousedown: onMouseDownProp = undefined,
32
+ ontouchstart: onTouchStartProp = undefined,
33
+ style = undefined,
34
+ ...rest
35
+ }: {
36
+ href: string;
37
+ method?: LinkMethod | Lowercase<LinkMethod>;
38
+ data?: Record<string, unknown>;
39
+ preserveScroll?: boolean;
40
+ preserveState?: boolean;
41
+ replace?: boolean;
42
+ reloadDocument?: boolean;
43
+ viewTransition?: boolean;
44
+ prefetch?: boolean | PrefetchStrategy | Array<PrefetchStrategy>;
45
+ cacheFor?: number | string | [string, string];
46
+ onNavigate?: (event: NavigateEvent) => void;
47
+ children?: Snippet;
48
+ onclick?: EventHandler<MouseEvent>;
49
+ onmouseenter?: EventHandler<MouseEvent>;
50
+ onmouseleave?: EventHandler<MouseEvent>;
51
+ onfocus?: EventHandler<FocusEvent>;
52
+ onmousedown?: EventHandler<MouseEvent>;
53
+ ontouchstart?: EventHandler<TouchEvent>;
54
+ style?: string;
55
+ [key: string]: unknown;
56
+ } = $props();
57
+
58
+ const router = getContext<VoidRouter>("__void_router");
59
+
60
+ function appendQueryValue(params: URLSearchParams, key: string, value: unknown): boolean {
61
+ if (value === null || value === undefined) {
62
+ return false;
63
+ }
64
+ if (value instanceof Date) {
65
+ params.append(key, value.toISOString());
66
+ return true;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ let appended = false;
70
+ for (const item of value) {
71
+ appended = appendQueryValue(params, key, item) || appended;
72
+ }
73
+ return appended;
74
+ }
75
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
76
+ params.append(key, String(value));
77
+ return true;
78
+ }
79
+ throw new Error(
80
+ `Link: GET data only supports primitive values and arrays. Remove nested data for '${key}'.`,
81
+ );
82
+ }
83
+
84
+ function mergeDataIntoHref(hrefValue: string, dataValue?: Record<string, unknown>): string {
85
+ if (!dataValue) {
86
+ return hrefValue;
87
+ }
88
+
89
+ const hashIndex = hrefValue.indexOf("#");
90
+ const beforeHash = hashIndex === -1 ? hrefValue : hrefValue.slice(0, hashIndex);
91
+ const hash = hashIndex === -1 ? "" : hrefValue.slice(hashIndex);
92
+ const searchIndex = beforeHash.indexOf("?");
93
+ const path = searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex);
94
+ const search = searchIndex === -1 ? "" : beforeHash.slice(searchIndex + 1);
95
+ const params = new URLSearchParams(search);
96
+ let appended = false;
97
+
98
+ for (const [key, value] of Object.entries(dataValue)) {
99
+ appended = appendQueryValue(params, key, value) || appended;
100
+ }
101
+
102
+ if (!appended) {
103
+ return hrefValue;
104
+ }
105
+
106
+ const query = params.toString();
107
+ return `${path}${query ? `?${query}` : ""}${hash}`;
108
+ }
109
+
110
+ function hasPrefetch(prefetch: boolean | PrefetchStrategy | Array<PrefetchStrategy>): boolean {
111
+ return prefetch !== false;
112
+ }
113
+
114
+ function isModifiedEvent(e: MouseEvent): boolean {
115
+ const target = (e.currentTarget as HTMLElement).getAttribute("target");
116
+ return (
117
+ (target !== null && target !== "_self") ||
118
+ e.ctrlKey ||
119
+ e.metaKey ||
120
+ e.shiftKey ||
121
+ e.altKey ||
122
+ e.button !== 0
123
+ );
124
+ }
125
+
126
+ function getStrategies(): Array<PrefetchStrategy> {
127
+ if (hasPrefetch(prefetchProp) && !isGet) {
128
+ throw new Error(
129
+ 'Link: prefetch only supports GET links. Remove `prefetch` or use method="GET".',
130
+ );
131
+ }
132
+ if (reloadDocument && !isGet) {
133
+ throw new Error("Link: reloadDocument only supports GET links.");
134
+ }
135
+ if (prefetchProp === false || reloadDocument) {
136
+ return [];
137
+ }
138
+ if (prefetchProp === true) {
139
+ return ["hover"];
140
+ }
141
+ if (typeof prefetchProp === "string") {
142
+ return [prefetchProp];
143
+ }
144
+ return prefetchProp;
145
+ }
146
+
147
+ function doPrefetch() {
148
+ if (!router?.prefetch) {
149
+ return;
150
+ }
151
+ const current = window.location.pathname + window.location.search;
152
+ if (hrefWithData === current) {
153
+ return;
154
+ }
155
+ const options: PrefetchOptions = {};
156
+ if (cacheFor !== undefined) {
157
+ options.cacheFor = cacheFor;
158
+ }
159
+ if (normalizedMethod) {
160
+ options.method = normalizedMethod;
161
+ }
162
+ router.prefetch(hrefWithData, options);
163
+ }
164
+
165
+ function onClick(e: MouseEvent) {
166
+ onclickProp?.(e);
167
+ if (reloadDocument || !router || e.defaultPrevented || isModifiedEvent(e)) {
168
+ return;
169
+ }
170
+ if ((e.currentTarget as HTMLElement).hasAttribute("download")) {
171
+ return;
172
+ }
173
+ const url = new URL(hrefWithData, window.location.origin);
174
+ if (url.origin !== window.location.origin) {
175
+ return;
176
+ }
177
+
178
+ let navigatePrevented = false;
179
+ onNavigate?.({
180
+ preventDefault() {
181
+ navigatePrevented = true;
182
+ },
183
+ });
184
+ if (navigatePrevented) {
185
+ e.preventDefault();
186
+ return;
187
+ }
188
+
189
+ e.preventDefault();
190
+ router.visit(hrefWithData, {
191
+ method: normalizedMethod,
192
+ data: isGet ? undefined : data,
193
+ preserveScroll,
194
+ preserveState,
195
+ replace,
196
+ viewTransition,
197
+ });
198
+ }
199
+
200
+ let hoverTimer: ReturnType<typeof setTimeout> | null = null;
201
+ let observer: IntersectionObserver | null = null;
202
+ let el = $state<HTMLElement | null>(null);
203
+
204
+ function onMouseEnter(e: MouseEvent) {
205
+ onMouseEnterProp?.(e);
206
+ if (e.defaultPrevented) {
207
+ return;
208
+ }
209
+ if (hoverTimer) {
210
+ clearTimeout(hoverTimer);
211
+ }
212
+ hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
213
+ }
214
+
215
+ function onMouseLeave(e: MouseEvent) {
216
+ onMouseLeaveProp?.(e);
217
+ if (hoverTimer) {
218
+ clearTimeout(hoverTimer);
219
+ hoverTimer = null;
220
+ }
221
+ }
222
+
223
+ function onFocus(e: FocusEvent) {
224
+ onFocusProp?.(e);
225
+ if (!e.defaultPrevented) {
226
+ doPrefetch();
227
+ }
228
+ }
229
+
230
+ function onMouseDown(e: MouseEvent) {
231
+ onMouseDownProp?.(e);
232
+ if (!e.defaultPrevented) {
233
+ doPrefetch();
234
+ }
235
+ }
236
+
237
+ function onTouchStart(e: TouchEvent) {
238
+ onTouchStartProp?.(e);
239
+ if (!e.defaultPrevented) {
240
+ doPrefetch();
241
+ }
242
+ }
243
+
244
+ onMount(() => {
245
+ if (strategies.includes("mount")) {
246
+ doPrefetch();
247
+ }
248
+ if (strategies.includes("visible") && el) {
249
+ observer = new IntersectionObserver(
250
+ (entries) => {
251
+ if (entries.some((e) => e.isIntersecting)) {
252
+ doPrefetch();
253
+ observer?.disconnect();
254
+ observer = null;
255
+ }
256
+ },
257
+ { rootMargin: "0px" },
258
+ );
259
+ observer.observe(el);
260
+ }
261
+ });
262
+
263
+ onDestroy(() => {
264
+ if (hoverTimer) {
265
+ clearTimeout(hoverTimer);
266
+ }
267
+ if (observer) {
268
+ observer.disconnect();
269
+ observer = null;
270
+ }
271
+ });
272
+
273
+ const normalizedMethod = $derived(method.toUpperCase() as LinkMethod);
274
+ const isGet = $derived(normalizedMethod === "GET");
275
+ const hrefWithData = $derived(isGet ? mergeDataIntoHref(href, data) : href);
276
+ const strategies = $derived(getStrategies());
277
+ const resolvedStyle = $derived(
278
+ style ? `touch-action: manipulation; ${style}` : "touch-action: manipulation",
279
+ );
280
+ </script>
281
+
282
+ {#if isGet}
283
+ <a
284
+ bind:this={el}
285
+ href={hrefWithData}
286
+ {...rest}
287
+ style={resolvedStyle}
288
+ onclick={onClick}
289
+ onmouseenter={strategies.includes("hover") ? onMouseEnter : onMouseEnterProp}
290
+ onmouseleave={strategies.includes("hover") ? onMouseLeave : onMouseLeaveProp}
291
+ onfocus={strategies.includes("hover") ? onFocus : onFocusProp}
292
+ onmousedown={strategies.includes("click") ? onMouseDown : onMouseDownProp}
293
+ ontouchstart={strategies.includes("hover") || strategies.includes("click")
294
+ ? onTouchStart
295
+ : onTouchStartProp}
296
+ >
297
+ {@render children()}
298
+ </a>
299
+ {:else}
300
+ <button
301
+ bind:this={el}
302
+ type="button"
303
+ {...rest}
304
+ style={resolvedStyle}
305
+ onclick={onClick}
306
+ onmouseenter={strategies.includes("hover") ? onMouseEnter : onMouseEnterProp}
307
+ onmouseleave={strategies.includes("hover") ? onMouseLeave : onMouseLeaveProp}
308
+ onfocus={strategies.includes("hover") ? onFocus : onFocusProp}
309
+ onmousedown={strategies.includes("click") ? onMouseDown : onMouseDownProp}
310
+ ontouchstart={strategies.includes("hover") || strategies.includes("click")
311
+ ? onTouchStart
312
+ : onTouchStartProp}
313
+ >
314
+ {@render children()}
315
+ </button>
316
+ {/if}
@@ -0,0 +1,50 @@
1
+ import { submitAction, type ActionResult, type VoidRouter } from 'void/pages-client';
2
+ import type { ActionUrl, ResolveActionBody, ResolveActionParams } from 'void/routes';
3
+
4
+ type ActionMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
+
6
+ type HasParams<U extends string> = [ResolveActionParams<U>] extends [never] ? false : true;
7
+
8
+ type ActionOptions<U extends string> = {
9
+ data?: ResolveActionBody<U>;
10
+ method?: ActionMethod;
11
+ } & (HasParams<U> extends true ? { params: ResolveActionParams<U> } : { params?: never });
12
+
13
+ let _router: VoidRouter | null = null;
14
+
15
+ /** Called during app initialization to store the router instance */
16
+ export function setActionRouter(router: VoidRouter): void {
17
+ _router = router;
18
+ }
19
+
20
+ /** Programmatic one-shot page action call with Inertia page update */
21
+ export async function action<U extends ActionUrl & string>(
22
+ url: U,
23
+ ...args: HasParams<U> extends true ? [options: ActionOptions<U>] : [options?: ActionOptions<U>]
24
+ ): Promise<ActionResult> {
25
+ if (!_router) {
26
+ throw new Error('action(): called before router initialization.');
27
+ }
28
+
29
+ const options = args[0] as ActionOptions<U> | undefined;
30
+ const data = options?.data;
31
+ const method = options?.method || 'POST';
32
+
33
+ let resolvedUrl: string = url;
34
+ let actionQuery = '';
35
+
36
+ const qIdx = resolvedUrl.indexOf('?');
37
+ if (qIdx !== -1) {
38
+ actionQuery = resolvedUrl.slice(qIdx);
39
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
40
+ }
41
+
42
+ if (options?.params) {
43
+ for (const [key, value] of Object.entries(options.params)) {
44
+ resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value as string));
45
+ }
46
+ }
47
+
48
+ resolvedUrl = resolvedUrl + actionQuery;
49
+ return submitAction(_router, resolvedUrl, { method, data });
50
+ }
@@ -0,0 +1,8 @@
1
+ export { default as Link } from './Link.svelte';
2
+ export { useRouter } from './use-router.ts';
3
+ export { useShared } from './use-shared.ts';
4
+ export { useForm } from './use-form.svelte.ts';
5
+ export { useNavigation } from './use-navigation.ts';
6
+ export { useIslandForm } from './use-island-form.svelte.ts';
7
+ export { action, setActionRouter } from './action.ts';
8
+ export type { DeferredState as Deferred, InferProps } from 'void';
@@ -0,0 +1,2 @@
1
+ export { PrefetchCache, parseCacheFor } from 'void/pages-prefetch';
2
+ export type { CacheForResult, CacheForInput, CacheEntry } from 'void/pages-prefetch';