@void/react 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.
@@ -0,0 +1,492 @@
1
+ import { i as SharedContext, n as NavigationContext, r as RouterContext } from "./context-BCeFV8Jy.mjs";
2
+ import "./action-BFWtavbf.mjs";
3
+ import { useActionState, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ import { VoidActionError, categorizeActionError, isAbortError, isEqualFormValue, ssrProxy, submitAction } from "void/pages-client";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/runtime/link.tsx
7
+ function appendQueryValue(params, key, value) {
8
+ if (value === null || value === void 0) return false;
9
+ if (value instanceof Date) {
10
+ params.append(key, value.toISOString());
11
+ return true;
12
+ }
13
+ if (Array.isArray(value)) {
14
+ let appended = false;
15
+ for (const item of value) appended = appendQueryValue(params, key, item) || appended;
16
+ return appended;
17
+ }
18
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
19
+ params.append(key, String(value));
20
+ return true;
21
+ }
22
+ throw new Error(`Link: GET data only supports primitive values and arrays. Remove nested data for '${key}'.`);
23
+ }
24
+ function mergeDataIntoHref(href, data) {
25
+ if (!data) return href;
26
+ const hashIndex = href.indexOf("#");
27
+ const beforeHash = hashIndex === -1 ? href : href.slice(0, hashIndex);
28
+ const hash = hashIndex === -1 ? "" : href.slice(hashIndex);
29
+ const searchIndex = beforeHash.indexOf("?");
30
+ const path = searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex);
31
+ const search = searchIndex === -1 ? "" : beforeHash.slice(searchIndex + 1);
32
+ const params = new URLSearchParams(search);
33
+ let appended = false;
34
+ for (const [key, value] of Object.entries(data)) appended = appendQueryValue(params, key, value) || appended;
35
+ if (!appended) return href;
36
+ const query = params.toString();
37
+ return `${path}${query ? `?${query}` : ""}${hash}`;
38
+ }
39
+ function hasPrefetch(prefetch) {
40
+ return prefetch !== false;
41
+ }
42
+ function isModifiedEvent(e) {
43
+ const target = e.currentTarget.getAttribute("target");
44
+ return target !== null && target !== "_self" || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || e.button !== 0;
45
+ }
46
+ function Link({ href, method = "GET", data, preserveScroll = false, preserveState = false, replace = false, reloadDocument = false, viewTransition, prefetch: prefetchProp = false, cacheFor, onNavigate, children, style, onClick: onClickProp, onMouseEnter: onMouseEnterProp, onMouseLeave: onMouseLeaveProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp, onTouchStart: onTouchStartProp, ...rest }) {
47
+ const router = useContext(RouterContext);
48
+ const elementRef = useRef(null);
49
+ const hoverTimerRef = useRef(null);
50
+ const normalizedMethod = useMemo(() => method.toUpperCase(), [method]);
51
+ const isGet = normalizedMethod === "GET";
52
+ const hrefWithData = useMemo(() => isGet ? mergeDataIntoHref(href, data) : href, [
53
+ data,
54
+ href,
55
+ isGet
56
+ ]);
57
+ const strategies = useMemo(() => {
58
+ if (prefetchProp === false || reloadDocument) return [];
59
+ if (prefetchProp === true) return ["hover"];
60
+ if (typeof prefetchProp === "string") return [prefetchProp];
61
+ return prefetchProp;
62
+ }, [prefetchProp, reloadDocument]);
63
+ const hoverDelay = router?._hoverDelay ?? 75;
64
+ if (hasPrefetch(prefetchProp) && !isGet) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
65
+ if (reloadDocument && !isGet) throw new Error("Link: reloadDocument only supports GET links.");
66
+ const doPrefetch = useCallback(() => {
67
+ if (!router?.prefetch) return;
68
+ if (hrefWithData === window.location.pathname + window.location.search) return;
69
+ const options = {};
70
+ if (cacheFor !== void 0) options.cacheFor = cacheFor;
71
+ if (normalizedMethod) options.method = normalizedMethod;
72
+ router.prefetch(hrefWithData, options);
73
+ }, [
74
+ cacheFor,
75
+ hrefWithData,
76
+ normalizedMethod,
77
+ router
78
+ ]);
79
+ const shouldHandleClick = useCallback((event) => {
80
+ if (reloadDocument || !router || event.defaultPrevented || isModifiedEvent(event)) return false;
81
+ if (event.currentTarget.hasAttribute("download")) return false;
82
+ if (new URL(hrefWithData, window.location.origin).origin !== window.location.origin) return false;
83
+ return true;
84
+ }, [
85
+ hrefWithData,
86
+ reloadDocument,
87
+ router
88
+ ]);
89
+ const onClick = useCallback((event) => {
90
+ onClickProp?.(event);
91
+ if (!shouldHandleClick(event)) return;
92
+ let navigatePrevented = false;
93
+ onNavigate?.({ preventDefault() {
94
+ navigatePrevented = true;
95
+ } });
96
+ if (navigatePrevented) {
97
+ event.preventDefault();
98
+ return;
99
+ }
100
+ event.preventDefault();
101
+ router?.visit(hrefWithData, {
102
+ method: normalizedMethod,
103
+ data: isGet ? void 0 : data,
104
+ preserveScroll,
105
+ preserveState,
106
+ replace,
107
+ viewTransition
108
+ });
109
+ }, [
110
+ data,
111
+ hrefWithData,
112
+ isGet,
113
+ normalizedMethod,
114
+ onClickProp,
115
+ onNavigate,
116
+ preserveScroll,
117
+ preserveState,
118
+ replace,
119
+ router,
120
+ shouldHandleClick,
121
+ viewTransition
122
+ ]);
123
+ const onMouseEnter = useCallback((event) => {
124
+ onMouseEnterProp?.(event);
125
+ if (event.defaultPrevented) return;
126
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
127
+ hoverTimerRef.current = setTimeout(doPrefetch, hoverDelay);
128
+ }, [
129
+ doPrefetch,
130
+ hoverDelay,
131
+ onMouseEnterProp
132
+ ]);
133
+ const onMouseLeave = useCallback((event) => {
134
+ onMouseLeaveProp?.(event);
135
+ if (hoverTimerRef.current) {
136
+ clearTimeout(hoverTimerRef.current);
137
+ hoverTimerRef.current = null;
138
+ }
139
+ }, [onMouseLeaveProp]);
140
+ const onFocus = useCallback((event) => {
141
+ onFocusProp?.(event);
142
+ if (!event.defaultPrevented) doPrefetch();
143
+ }, [doPrefetch, onFocusProp]);
144
+ const onMouseDown = useCallback((event) => {
145
+ onMouseDownProp?.(event);
146
+ if (!event.defaultPrevented) doPrefetch();
147
+ }, [doPrefetch, onMouseDownProp]);
148
+ const onTouchStart = useCallback((event) => {
149
+ onTouchStartProp?.(event);
150
+ if (!event.defaultPrevented) doPrefetch();
151
+ }, [doPrefetch, onTouchStartProp]);
152
+ useEffect(() => {
153
+ if (strategies.length === 0) return;
154
+ if (strategies.includes("mount")) doPrefetch();
155
+ let observer = null;
156
+ if (strategies.includes("visible") && elementRef.current) {
157
+ observer = new IntersectionObserver((entries) => {
158
+ if (entries.some((e) => e.isIntersecting)) {
159
+ doPrefetch();
160
+ observer?.disconnect();
161
+ observer = null;
162
+ }
163
+ }, { rootMargin: "0px" });
164
+ observer.observe(elementRef.current);
165
+ }
166
+ return () => {
167
+ if (hoverTimerRef.current) {
168
+ clearTimeout(hoverTimerRef.current);
169
+ hoverTimerRef.current = null;
170
+ }
171
+ if (observer) observer.disconnect();
172
+ };
173
+ }, [doPrefetch, strategies]);
174
+ const eventHandlers = useMemo(() => {
175
+ const handlers = {
176
+ onClick,
177
+ onFocus: onFocusProp,
178
+ onMouseDown: onMouseDownProp,
179
+ onMouseEnter: onMouseEnterProp,
180
+ onMouseLeave: onMouseLeaveProp,
181
+ onTouchStart: onTouchStartProp
182
+ };
183
+ if (strategies.includes("hover")) {
184
+ handlers.onMouseEnter = onMouseEnter;
185
+ handlers.onMouseLeave = onMouseLeave;
186
+ handlers.onFocus = onFocus;
187
+ handlers.onTouchStart = onTouchStart;
188
+ }
189
+ if (strategies.includes("click")) {
190
+ handlers.onMouseDown = onMouseDown;
191
+ handlers.onTouchStart = onTouchStart;
192
+ }
193
+ return handlers;
194
+ }, [
195
+ onClick,
196
+ onFocus,
197
+ onFocusProp,
198
+ onMouseDown,
199
+ onMouseDownProp,
200
+ onMouseEnter,
201
+ onMouseEnterProp,
202
+ onMouseLeave,
203
+ onMouseLeaveProp,
204
+ onTouchStart,
205
+ onTouchStartProp,
206
+ strategies
207
+ ]);
208
+ const resolvedStyle = useMemo(() => ({
209
+ touchAction: "manipulation",
210
+ ...style
211
+ }), [style]);
212
+ if (isGet) return /* @__PURE__ */ jsx("a", {
213
+ ref: elementRef,
214
+ href: hrefWithData,
215
+ style: resolvedStyle,
216
+ ...rest,
217
+ ...eventHandlers,
218
+ children
219
+ });
220
+ return /* @__PURE__ */ jsx("button", {
221
+ ref: elementRef,
222
+ type: "button",
223
+ style: resolvedStyle,
224
+ ...rest,
225
+ ...eventHandlers,
226
+ children
227
+ });
228
+ }
229
+ //#endregion
230
+ //#region src/runtime/use-router.ts
231
+ function useRouter() {
232
+ return useContext(RouterContext) ?? ssrProxy;
233
+ }
234
+ //#endregion
235
+ //#region src/runtime/use-shared.ts
236
+ function useShared() {
237
+ const shared = useContext(SharedContext);
238
+ if (!shared) throw new Error("useShared(): must be used inside a Void page.");
239
+ return shared;
240
+ }
241
+ //#endregion
242
+ //#region src/runtime/use-form.ts
243
+ function getValidationErrors$1(error) {
244
+ const body = error.body;
245
+ if (body && typeof body === "object" && "errors" in body && body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)) return body.errors;
246
+ return null;
247
+ }
248
+ function useForm(url, defaults, options) {
249
+ let resolvedUrl = url;
250
+ let actionQuery = "";
251
+ const qIdx = resolvedUrl.indexOf("?");
252
+ if (qIdx !== -1) {
253
+ actionQuery = resolvedUrl.slice(qIdx);
254
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
255
+ }
256
+ if (options?.params) for (const [key, value] of Object.entries(options.params)) resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
257
+ resolvedUrl = resolvedUrl + actionQuery;
258
+ const router = useContext(RouterContext);
259
+ const [data, setDataState] = useState(() => ({ ...defaults }));
260
+ const [defaultsState, setDefaultsState] = useState(() => ({ ...defaults }));
261
+ const [errors, setErrors] = useState({});
262
+ const [error, setError] = useState(null);
263
+ const [wasSuccessful, setWasSuccessful] = useState(false);
264
+ const [recentlySuccessful, setRecentlySuccessful] = useState(false);
265
+ const defaultsRef = useRef(defaultsState);
266
+ const dataRef = useRef(data);
267
+ const successTimeoutRef = useRef(null);
268
+ const hasChanges = useMemo(() => {
269
+ return Object.keys(defaultsState).some((key) => !isEqualFormValue(data[key], defaultsState[key]));
270
+ }, [data, defaultsState]);
271
+ const setData = useCallback((field, value) => {
272
+ dataRef.current = {
273
+ ...dataRef.current,
274
+ [field]: value
275
+ };
276
+ setDataState(dataRef.current);
277
+ }, []);
278
+ const reset = useCallback((...fields) => {
279
+ if (fields.length === 0) dataRef.current = { ...defaultsRef.current };
280
+ else {
281
+ const next = { ...dataRef.current };
282
+ for (const field of fields) next[field] = defaultsRef.current[field];
283
+ dataRef.current = next;
284
+ }
285
+ setDataState(dataRef.current);
286
+ }, []);
287
+ const clearErrors = useCallback((...fields) => {
288
+ if (fields.length === 0) setErrors({});
289
+ else setErrors((prev) => {
290
+ const next = { ...prev };
291
+ for (const field of fields) delete next[field];
292
+ return next;
293
+ });
294
+ }, []);
295
+ const clearError = useCallback(() => {
296
+ setError(null);
297
+ }, []);
298
+ const [, dispatchAction, pending] = useActionState(async (previousSuccessVersion, payload) => {
299
+ if (!router) throw new Error("useForm(): requires the Void Router.");
300
+ const method = payload instanceof FormData ? "POST" : payload.method;
301
+ setWasSuccessful(false);
302
+ setRecentlySuccessful(false);
303
+ if (successTimeoutRef.current) {
304
+ clearTimeout(successTimeoutRef.current);
305
+ successTimeoutRef.current = null;
306
+ }
307
+ setErrors({});
308
+ setError(null);
309
+ let result;
310
+ try {
311
+ result = await submitAction(router, resolvedUrl, {
312
+ method,
313
+ data: { ...dataRef.current },
314
+ _resolveOnShell: true
315
+ });
316
+ } catch (submitError) {
317
+ if (isAbortError(submitError)) return previousSuccessVersion;
318
+ throw submitError;
319
+ }
320
+ if (!result.ok) {
321
+ const validationErrors = getValidationErrors$1(result.error);
322
+ if (validationErrors) setErrors(validationErrors);
323
+ else setError(result.error);
324
+ return previousSuccessVersion;
325
+ }
326
+ const nextDefaults = { ...dataRef.current };
327
+ defaultsRef.current = nextDefaults;
328
+ setDefaultsState(nextDefaults);
329
+ setWasSuccessful(true);
330
+ setRecentlySuccessful(true);
331
+ successTimeoutRef.current = setTimeout(() => {
332
+ setRecentlySuccessful(false);
333
+ successTimeoutRef.current = null;
334
+ }, 2e3);
335
+ return previousSuccessVersion + 1;
336
+ }, 0);
337
+ useEffect(() => {
338
+ return () => {
339
+ if (successTimeoutRef.current) {
340
+ clearTimeout(successTimeoutRef.current);
341
+ successTimeoutRef.current = null;
342
+ }
343
+ };
344
+ }, []);
345
+ const dispatchMethodAction = useCallback((method) => (_formData) => {
346
+ dispatchAction({ method });
347
+ }, [dispatchAction]);
348
+ return {
349
+ data,
350
+ setData,
351
+ errors,
352
+ error,
353
+ pending,
354
+ hasChanges,
355
+ wasSuccessful,
356
+ recentlySuccessful,
357
+ post: dispatchAction,
358
+ put: useMemo(() => dispatchMethodAction("PUT"), [dispatchMethodAction]),
359
+ patch: useMemo(() => dispatchMethodAction("PATCH"), [dispatchMethodAction]),
360
+ delete: useMemo(() => dispatchMethodAction("DELETE"), [dispatchMethodAction]),
361
+ reset,
362
+ clearErrors,
363
+ clearError
364
+ };
365
+ }
366
+ //#endregion
367
+ //#region src/runtime/use-navigation.ts
368
+ function useNavigation() {
369
+ return useContext(NavigationContext);
370
+ }
371
+ //#endregion
372
+ //#region src/runtime/use-island-form.ts
373
+ function getValidationErrors(body) {
374
+ if (body && typeof body === "object" && "errors" in body && body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)) return body.errors;
375
+ return null;
376
+ }
377
+ async function readActionErrorBody(response) {
378
+ const contentType = response.headers.get("content-type") || "";
379
+ try {
380
+ if (contentType.includes("application/json")) return await response.json();
381
+ return await response.text() || null;
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+ /**
387
+ * Form hook for island pages. Uses fetch + page reload instead of the
388
+ * Void Router (which is not available in island mode).
389
+ */
390
+ function useIslandForm(defaults) {
391
+ const [data, setDataState] = useState(() => ({ ...defaults }));
392
+ const [defaultsState, setDefaultsState] = useState(() => ({ ...defaults }));
393
+ const [errors, setErrors] = useState({});
394
+ const [error, setError] = useState(null);
395
+ const [pending, setPending] = useState(false);
396
+ const [wasSuccessful, setWasSuccessful] = useState(false);
397
+ const [recentlySuccessful, setRecentlySuccessful] = useState(false);
398
+ const defaultsRef = useRef(defaultsState);
399
+ const successTimeoutRef = useRef(null);
400
+ const hasChanges = useMemo(() => {
401
+ return Object.keys(defaultsState).some((key) => !isEqualFormValue(data[key], defaultsState[key]));
402
+ }, [data, defaultsState]);
403
+ const setData = useCallback((field, value) => {
404
+ setDataState((prev) => ({
405
+ ...prev,
406
+ [field]: value
407
+ }));
408
+ }, []);
409
+ const reset = useCallback((...fields) => {
410
+ if (fields.length === 0) setDataState({ ...defaultsRef.current });
411
+ else setDataState((prev) => {
412
+ const next = { ...prev };
413
+ for (const field of fields) next[field] = defaultsRef.current[field];
414
+ return next;
415
+ });
416
+ }, []);
417
+ const clearErrors = useCallback((...fields) => {
418
+ if (fields.length === 0) setErrors({});
419
+ else setErrors((prev) => {
420
+ const next = { ...prev };
421
+ for (const f of fields) delete next[f];
422
+ return next;
423
+ });
424
+ }, []);
425
+ const clearError = useCallback(() => {
426
+ setError(null);
427
+ }, []);
428
+ const submit = useCallback(async (url, method) => {
429
+ setPending(true);
430
+ setWasSuccessful(false);
431
+ setRecentlySuccessful(false);
432
+ if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
433
+ setErrors({});
434
+ setError(null);
435
+ try {
436
+ const response = await fetch(url, {
437
+ method,
438
+ headers: { "Content-Type": "application/json" },
439
+ body: JSON.stringify(data)
440
+ });
441
+ if (response.redirected) {
442
+ window.location.href = response.url;
443
+ return;
444
+ }
445
+ if (response.ok) {
446
+ setWasSuccessful(true);
447
+ setRecentlySuccessful(true);
448
+ const nextDefaults = { ...data };
449
+ defaultsRef.current = nextDefaults;
450
+ setDefaultsState(nextDefaults);
451
+ successTimeoutRef.current = setTimeout(() => setRecentlySuccessful(false), 2e3);
452
+ window.location.reload();
453
+ } else if (!response.ok) {
454
+ const body = await readActionErrorBody(response);
455
+ const actionError = new VoidActionError({
456
+ body,
457
+ status: response.status,
458
+ statusText: response.statusText,
459
+ url
460
+ });
461
+ const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
462
+ if (validationErrors) {
463
+ setErrors(validationErrors);
464
+ return;
465
+ }
466
+ if (categorizeActionError(response.status) === "boundary") throw actionError;
467
+ setError(actionError);
468
+ }
469
+ } finally {
470
+ setPending(false);
471
+ }
472
+ }, [data]);
473
+ return {
474
+ data,
475
+ setData,
476
+ errors,
477
+ error,
478
+ pending,
479
+ hasChanges,
480
+ wasSuccessful,
481
+ recentlySuccessful,
482
+ reset,
483
+ clearErrors,
484
+ clearError,
485
+ post: (url) => submit(url, "POST"),
486
+ put: (url) => submit(url, "PUT"),
487
+ patch: (url) => submit(url, "PATCH"),
488
+ delete: (url) => submit(url, "DELETE")
489
+ };
490
+ }
491
+ //#endregion
492
+ export { useRouter as a, useShared as i, useNavigation as n, Link as o, useForm as r, useIslandForm as t };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@void/react",
3
+ "version": "0.0.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ },
13
+ "./plugin": {
14
+ "types": "./dist/plugin.d.mts",
15
+ "import": "./dist/plugin.mjs"
16
+ },
17
+ "./runtime": {
18
+ "types": "./dist/runtime/index.d.mts",
19
+ "import": "./dist/runtime/index.mjs"
20
+ },
21
+ "./prefetch": {
22
+ "types": "./dist/runtime/prefetch.d.mts",
23
+ "import": "./dist/runtime/prefetch.mjs"
24
+ },
25
+ "./pages-client": {
26
+ "types": "./dist/runtime/pages-client.d.mts",
27
+ "import": "./dist/runtime/pages-client.mjs"
28
+ },
29
+ "./pages-server": {
30
+ "types": "./dist/runtime/pages-server.d.mts",
31
+ "import": "./dist/runtime/pages-server.mjs"
32
+ }
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "dev": "tsdown --watch",
40
+ "typecheck": "tsgo --noEmit"
41
+ },
42
+ "dependencies": {
43
+ "@vitejs/plugin-react": "^6.0.1"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "catalog:",
47
+ "@types/react": "^19.2.14",
48
+ "@types/react-dom": "^19.2.3",
49
+ "pathe": "catalog:",
50
+ "react": "^19.2.5",
51
+ "react-dom": "^19.2.5",
52
+ "tsdown": "catalog:",
53
+ "vite": "catalog:",
54
+ "void": "workspace:*"
55
+ },
56
+ "peerDependencies": {
57
+ "react": "^19.0.0",
58
+ "react-dom": "^19.0.0",
59
+ "vite": "catalog:",
60
+ "void": "workspace:*"
61
+ }
62
+ }