@mandujs/core 0.5.7 โ†’ 0.7.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,267 @@
1
+ /**
2
+ * Mandu Router Hooks ๐Ÿช
3
+ * React hooks for client-side routing
4
+ */
5
+
6
+ import { useState, useEffect, useCallback, useSyncExternalStore } from "react";
7
+ import {
8
+ subscribe,
9
+ getRouterState,
10
+ getCurrentRoute,
11
+ getLoaderData,
12
+ getNavigationState,
13
+ navigate,
14
+ type RouteInfo,
15
+ type NavigationState,
16
+ type NavigateOptions,
17
+ } from "./router";
18
+
19
+ /**
20
+ * ๋ผ์šฐํ„ฐ ์ƒํƒœ ์ „์ฒด ์ ‘๊ทผ
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const { currentRoute, loaderData, navigation } = useRouterState();
25
+ * ```
26
+ */
27
+ export function useRouterState() {
28
+ return useSyncExternalStore(
29
+ subscribe,
30
+ getRouterState,
31
+ getRouterState // SSR์—์„œ๋„ ๋™์ผ
32
+ );
33
+ }
34
+
35
+ /**
36
+ * ํ˜„์žฌ ๋ผ์šฐํŠธ ์ •๋ณด
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const route = useRoute();
41
+ * console.log(route?.id, route?.params);
42
+ * ```
43
+ */
44
+ export function useRoute(): RouteInfo | null {
45
+ const state = useRouterState();
46
+ return state.currentRoute;
47
+ }
48
+
49
+ /**
50
+ * URL ํŒŒ๋ผ๋ฏธํ„ฐ ์ ‘๊ทผ
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * // URL: /users/123
55
+ * const { id } = useParams<{ id: string }>();
56
+ * console.log(id); // "123"
57
+ * ```
58
+ */
59
+ export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
60
+ const route = useRoute();
61
+ return (route?.params ?? {}) as T;
62
+ }
63
+
64
+ /**
65
+ * ํ˜„์žฌ ๊ฒฝ๋กœ๋ช…
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * const pathname = usePathname();
70
+ * console.log(pathname); // "/users/123"
71
+ * ```
72
+ */
73
+ export function usePathname(): string {
74
+ const [pathname, setPathname] = useState(() =>
75
+ typeof window !== "undefined" ? window.location.pathname : "/"
76
+ );
77
+
78
+ useEffect(() => {
79
+ const handleChange = () => {
80
+ setPathname(window.location.pathname);
81
+ };
82
+
83
+ window.addEventListener("popstate", handleChange);
84
+
85
+ // ๋ผ์šฐํ„ฐ ์ƒํƒœ ๋ณ€๊ฒฝ ๊ตฌ๋…
86
+ const unsubscribe = subscribe(() => {
87
+ setPathname(window.location.pathname);
88
+ });
89
+
90
+ return () => {
91
+ window.removeEventListener("popstate", handleChange);
92
+ unsubscribe();
93
+ };
94
+ }, []);
95
+
96
+ return pathname;
97
+ }
98
+
99
+ /**
100
+ * ํ˜„์žฌ ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ (์ฟผ๋ฆฌ ์ŠคํŠธ๋ง)
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * // URL: /search?q=hello&page=2
105
+ * const searchParams = useSearchParams();
106
+ * console.log(searchParams.get("q")); // "hello"
107
+ * ```
108
+ */
109
+ export function useSearchParams(): URLSearchParams {
110
+ const [searchParams, setSearchParams] = useState(() =>
111
+ typeof window !== "undefined"
112
+ ? new URLSearchParams(window.location.search)
113
+ : new URLSearchParams()
114
+ );
115
+
116
+ useEffect(() => {
117
+ const handleChange = () => {
118
+ setSearchParams(new URLSearchParams(window.location.search));
119
+ };
120
+
121
+ window.addEventListener("popstate", handleChange);
122
+
123
+ const unsubscribe = subscribe(() => {
124
+ setSearchParams(new URLSearchParams(window.location.search));
125
+ });
126
+
127
+ return () => {
128
+ window.removeEventListener("popstate", handleChange);
129
+ unsubscribe();
130
+ };
131
+ }, []);
132
+
133
+ return searchParams;
134
+ }
135
+
136
+ /**
137
+ * Loader ๋ฐ์ดํ„ฐ ์ ‘๊ทผ
138
+ *
139
+ * @example
140
+ * ```tsx
141
+ * interface UserData { name: string; email: string; }
142
+ * const data = useLoaderData<UserData>();
143
+ * ```
144
+ */
145
+ export function useLoaderData<T = unknown>(): T | undefined {
146
+ const state = useRouterState();
147
+ return state.loaderData as T | undefined;
148
+ }
149
+
150
+ /**
151
+ * ๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒํƒœ (๋กœ๋”ฉ ์—ฌ๋ถ€)
152
+ *
153
+ * @example
154
+ * ```tsx
155
+ * const { state, location } = useNavigation();
156
+ *
157
+ * if (state === "loading") {
158
+ * return <Spinner />;
159
+ * }
160
+ * ```
161
+ */
162
+ export function useNavigation(): NavigationState {
163
+ const state = useRouterState();
164
+ return state.navigation;
165
+ }
166
+
167
+ /**
168
+ * ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ๋„ค๋น„๊ฒŒ์ด์…˜
169
+ *
170
+ * @example
171
+ * ```tsx
172
+ * const navigate = useNavigate();
173
+ *
174
+ * const handleClick = () => {
175
+ * navigate("/dashboard");
176
+ * };
177
+ *
178
+ * const handleSubmit = () => {
179
+ * navigate("/success", { replace: true });
180
+ * };
181
+ * ```
182
+ */
183
+ export function useNavigate(): (to: string, options?: NavigateOptions) => Promise<void> {
184
+ return useCallback((to: string, options?: NavigateOptions) => {
185
+ return navigate(to, options);
186
+ }, []);
187
+ }
188
+
189
+ /**
190
+ * ๋ผ์šฐํ„ฐ ํ†ตํ•ฉ ํ›… (ํŽธ์˜์šฉ)
191
+ *
192
+ * @example
193
+ * ```tsx
194
+ * const {
195
+ * pathname,
196
+ * params,
197
+ * searchParams,
198
+ * navigate,
199
+ * isNavigating
200
+ * } = useRouter();
201
+ * ```
202
+ */
203
+ export function useRouter() {
204
+ const pathname = usePathname();
205
+ const params = useParams();
206
+ const searchParams = useSearchParams();
207
+ const navigation = useNavigation();
208
+ const navigateFn = useNavigate();
209
+
210
+ return {
211
+ /** ํ˜„์žฌ ๊ฒฝ๋กœ๋ช… */
212
+ pathname,
213
+ /** URL ํŒŒ๋ผ๋ฏธํ„ฐ */
214
+ params,
215
+ /** ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ (์ฟผ๋ฆฌ ์ŠคํŠธ๋ง) */
216
+ searchParams,
217
+ /** ๋„ค๋น„๊ฒŒ์ด์…˜ ํ•จ์ˆ˜ */
218
+ navigate: navigateFn,
219
+ /** ๋„ค๋น„๊ฒŒ์ด์…˜ ์ค‘ ์—ฌ๋ถ€ */
220
+ isNavigating: navigation.state === "loading",
221
+ /** ๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒํƒœ ์ƒ์„ธ */
222
+ navigation,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * ํŠน์ • ๊ฒฝ๋กœ์™€ ํ˜„์žฌ ๊ฒฝ๋กœ ์ผ์น˜ ์—ฌ๋ถ€
228
+ *
229
+ * @example
230
+ * ```tsx
231
+ * const isActive = useMatch("/about");
232
+ * const isUsersPage = useMatch("/users/:id");
233
+ * ```
234
+ */
235
+ export function useMatch(pattern: string): boolean {
236
+ const pathname = usePathname();
237
+
238
+ // ๊ฐ„๋‹จํ•œ ํŒจํ„ด ๋งค์นญ (ํŒŒ๋ผ๋ฏธํ„ฐ ๊ณ ๋ ค)
239
+ const regexStr = pattern
240
+ .replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, "[^/]+")
241
+ .replace(/\//g, "\\/");
242
+
243
+ const regex = new RegExp(`^${regexStr}$`);
244
+ return regex.test(pathname);
245
+ }
246
+
247
+ /**
248
+ * ๋’ค๋กœ ๊ฐ€๊ธฐ
249
+ */
250
+ export function useGoBack(): () => void {
251
+ return useCallback(() => {
252
+ if (typeof window !== "undefined") {
253
+ window.history.back();
254
+ }
255
+ }, []);
256
+ }
257
+
258
+ /**
259
+ * ์•ž์œผ๋กœ ๊ฐ€๊ธฐ
260
+ */
261
+ export function useGoForward(): () => void {
262
+ return useCallback(() => {
263
+ if (typeof window !== "undefined") {
264
+ window.history.forward();
265
+ }
266
+ }, []);
267
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Mandu Client Module ๐Ÿ๏ธ
3
- * ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ hydration์„ ์œ„ํ•œ API
3
+ * ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ hydration ๋ฐ ๋ผ์šฐํŒ…์„ ์œ„ํ•œ API
4
4
  *
5
5
  * @example
6
6
  * ```typescript
7
- * // spec/slots/todos.client.ts
7
+ * // Island ์ปดํฌ๋„ŒํŠธ
8
8
  * import { Mandu } from "@mandujs/core/client";
9
9
  *
10
10
  * export default Mandu.island<TodosData>({
@@ -12,6 +12,17 @@
12
12
  * render: (props) => <TodoList {...props} />
13
13
  * });
14
14
  * ```
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Client-side ๋ผ์šฐํŒ…
19
+ * import { Link, useRouter } from "@mandujs/core/client";
20
+ *
21
+ * function Nav() {
22
+ * const { pathname, navigate } = useRouter();
23
+ * return <Link href="/about">About</Link>;
24
+ * }
25
+ * ```
15
26
  */
16
27
 
17
28
  // Island API
@@ -42,9 +53,56 @@ export {
42
53
  type IslandLoader,
43
54
  } from "./runtime";
44
55
 
56
+ // Client-side Router API
57
+ export {
58
+ navigate,
59
+ prefetch,
60
+ subscribe,
61
+ getRouterState,
62
+ getCurrentRoute,
63
+ getLoaderData,
64
+ getNavigationState,
65
+ initializeRouter,
66
+ cleanupRouter,
67
+ type RouteInfo,
68
+ type NavigationState,
69
+ type RouterState,
70
+ type NavigateOptions,
71
+ } from "./router";
72
+
73
+ // Link Components
74
+ export { Link, NavLink, type LinkProps, type NavLinkProps } from "./Link";
75
+
76
+ // Router Hooks
77
+ export {
78
+ useRouter,
79
+ useRoute,
80
+ useParams,
81
+ usePathname,
82
+ useSearchParams,
83
+ useLoaderData,
84
+ useNavigation,
85
+ useNavigate,
86
+ useMatch,
87
+ useGoBack,
88
+ useGoForward,
89
+ useRouterState,
90
+ } from "./hooks";
91
+
92
+ // Props Serialization (Fresh ์Šคํƒ€์ผ)
93
+ export {
94
+ serializeProps,
95
+ deserializeProps,
96
+ isSerializable,
97
+ generatePropsScript,
98
+ parsePropsScript,
99
+ } from "./serialize";
100
+
45
101
  // Re-export as Mandu namespace for consistent API
46
102
  import { island, wrapComponent } from "./island";
47
103
  import { hydrateIslands, initializeRuntime } from "./runtime";
104
+ import { navigate, prefetch, initializeRouter } from "./router";
105
+ import { Link, NavLink } from "./Link";
48
106
 
49
107
  /**
50
108
  * Mandu Client namespace
@@ -73,4 +131,34 @@ export const Mandu = {
73
131
  * @see initializeRuntime
74
132
  */
75
133
  init: initializeRuntime,
134
+
135
+ /**
136
+ * Navigate to a URL (client-side)
137
+ * @see navigate
138
+ */
139
+ navigate,
140
+
141
+ /**
142
+ * Prefetch a URL for faster navigation
143
+ * @see prefetch
144
+ */
145
+ prefetch,
146
+
147
+ /**
148
+ * Initialize the client-side router
149
+ * @see initializeRouter
150
+ */
151
+ initRouter: initializeRouter,
152
+
153
+ /**
154
+ * Link component for client-side navigation
155
+ * @see Link
156
+ */
157
+ Link,
158
+
159
+ /**
160
+ * NavLink component with active state
161
+ * @see NavLink
162
+ */
163
+ NavLink,
76
164
  };