@mandujs/core 0.9.2 โ†’ 0.9.4

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.
@@ -1,267 +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
+ /**
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
+ }
@@ -103,8 +103,9 @@ import { Link, NavLink } from "./Link";
103
103
  /**
104
104
  * Mandu Client namespace
105
105
  * v0.8.0: Hydration์€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋จ (generateRuntimeSource์—์„œ ์ƒ์„ฑ)
106
+ * Note: Use `ManduClient` to avoid conflict with other Mandu exports
106
107
  */
107
- export const Mandu = {
108
+ export const ManduClient = {
108
109
  /**
109
110
  * Create an island component
110
111
  * @see island