@mandujs/core 0.13.0 → 0.13.2
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/README.ko.md +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +9 -0
- package/src/config/validate.ts +12 -0
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
package/src/client/Link.tsx
CHANGED
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Link Component 🔗
|
|
3
|
-
* Client-side 네비게이션을 위한 Link 컴포넌트
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, {
|
|
7
|
-
type AnchorHTMLAttributes,
|
|
8
|
-
type MouseEvent,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
useCallback,
|
|
11
|
-
useEffect,
|
|
12
|
-
useRef,
|
|
13
|
-
} from "react";
|
|
14
|
-
import { navigate, prefetch } from "./router";
|
|
15
|
-
|
|
16
|
-
export interface LinkProps
|
|
17
|
-
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
18
|
-
/** 이동할 URL */
|
|
19
|
-
href: string;
|
|
20
|
-
/** history.replaceState 사용 여부 */
|
|
21
|
-
replace?: boolean;
|
|
22
|
-
/** 마우스 hover 시 prefetch 여부 */
|
|
23
|
-
prefetch?: boolean;
|
|
24
|
-
/** 스크롤 위치 복원 여부 (기본: true) */
|
|
25
|
-
scroll?: boolean;
|
|
26
|
-
/** 자식 요소 */
|
|
27
|
-
children?: ReactNode;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Client-side 네비게이션 Link 컴포넌트
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```tsx
|
|
35
|
-
* import { Link } from "@mandujs/core/client";
|
|
36
|
-
*
|
|
37
|
-
* // 기본 사용
|
|
38
|
-
* <Link href="/about">About</Link>
|
|
39
|
-
*
|
|
40
|
-
* // Prefetch 활성화
|
|
41
|
-
* <Link href="/users" prefetch>Users</Link>
|
|
42
|
-
*
|
|
43
|
-
* // Replace 모드 (뒤로가기 히스토리 없음)
|
|
44
|
-
* <Link href="/login" replace>Login</Link>
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export function Link({
|
|
48
|
-
href,
|
|
49
|
-
replace = false,
|
|
50
|
-
prefetch: shouldPrefetch = false,
|
|
51
|
-
scroll = true,
|
|
52
|
-
children,
|
|
53
|
-
onClick,
|
|
54
|
-
onMouseEnter,
|
|
55
|
-
onFocus,
|
|
56
|
-
...rest
|
|
57
|
-
}: LinkProps): React.ReactElement {
|
|
58
|
-
const prefetchedRef = useRef(false);
|
|
59
|
-
|
|
60
|
-
// 클릭 핸들러
|
|
61
|
-
const handleClick = useCallback(
|
|
62
|
-
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
63
|
-
// 사용자 정의 onClick 먼저 실행
|
|
64
|
-
onClick?.(event);
|
|
65
|
-
|
|
66
|
-
// 기본 동작 방지 조건
|
|
67
|
-
if (
|
|
68
|
-
event.defaultPrevented ||
|
|
69
|
-
event.button !== 0 ||
|
|
70
|
-
event.metaKey ||
|
|
71
|
-
event.altKey ||
|
|
72
|
-
event.ctrlKey ||
|
|
73
|
-
event.shiftKey
|
|
74
|
-
) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 외부 링크 체크
|
|
79
|
-
try {
|
|
80
|
-
const url = new URL(href, window.location.origin);
|
|
81
|
-
if (url.origin !== window.location.origin) {
|
|
82
|
-
return; // 외부 링크는 기본 동작
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Client-side 네비게이션
|
|
89
|
-
event.preventDefault();
|
|
90
|
-
navigate(href, { replace, scroll });
|
|
91
|
-
},
|
|
92
|
-
[href, replace, scroll, onClick]
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
// Prefetch 실행
|
|
96
|
-
const doPrefetch = useCallback(() => {
|
|
97
|
-
if (!shouldPrefetch || prefetchedRef.current) return;
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const url = new URL(href, window.location.origin);
|
|
101
|
-
if (url.origin === window.location.origin) {
|
|
102
|
-
prefetch(href);
|
|
103
|
-
prefetchedRef.current = true;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
// 무시
|
|
107
|
-
}
|
|
108
|
-
}, [href, shouldPrefetch]);
|
|
109
|
-
|
|
110
|
-
// 마우스 hover 핸들러
|
|
111
|
-
const handleMouseEnter = useCallback(
|
|
112
|
-
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
113
|
-
onMouseEnter?.(event);
|
|
114
|
-
doPrefetch();
|
|
115
|
-
},
|
|
116
|
-
[onMouseEnter, doPrefetch]
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// 포커스 핸들러 (키보드 네비게이션)
|
|
120
|
-
const handleFocus = useCallback(
|
|
121
|
-
(event: React.FocusEvent<HTMLAnchorElement>) => {
|
|
122
|
-
onFocus?.(event);
|
|
123
|
-
doPrefetch();
|
|
124
|
-
},
|
|
125
|
-
[onFocus, doPrefetch]
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Viewport 진입 시 prefetch (IntersectionObserver)
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
if (!shouldPrefetch || typeof IntersectionObserver === "undefined") {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ref가 없으면 무시 (SSR)
|
|
135
|
-
return;
|
|
136
|
-
}, [shouldPrefetch]);
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<a
|
|
140
|
-
href={href}
|
|
141
|
-
onClick={handleClick}
|
|
142
|
-
onMouseEnter={handleMouseEnter}
|
|
143
|
-
onFocus={handleFocus}
|
|
144
|
-
data-mandu-link=""
|
|
145
|
-
{...rest}
|
|
146
|
-
>
|
|
147
|
-
{children}
|
|
148
|
-
</a>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* NavLink - 현재 경로와 일치할 때 활성 스타일 적용
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
* ```tsx
|
|
157
|
-
* import { NavLink } from "@mandujs/core/client";
|
|
158
|
-
*
|
|
159
|
-
* <NavLink
|
|
160
|
-
* href="/about"
|
|
161
|
-
* className={({ isActive }) => isActive ? "active" : ""}
|
|
162
|
-
* >
|
|
163
|
-
* About
|
|
164
|
-
* </NavLink>
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
export interface NavLinkProps extends Omit<LinkProps, "className" | "style"> {
|
|
168
|
-
/** 활성 상태에 따른 className */
|
|
169
|
-
className?: string | ((props: { isActive: boolean }) => string);
|
|
170
|
-
/** 활성 상태에 따른 style */
|
|
171
|
-
style?:
|
|
172
|
-
| React.CSSProperties
|
|
173
|
-
| ((props: { isActive: boolean }) => React.CSSProperties);
|
|
174
|
-
/** 활성 상태일 때 적용할 style (style과 병합됨) */
|
|
175
|
-
activeStyle?: React.CSSProperties;
|
|
176
|
-
/** 활성 상태일 때 추가할 className */
|
|
177
|
-
activeClassName?: string;
|
|
178
|
-
/** 정확히 일치해야 활성화 (기본: false) */
|
|
179
|
-
exact?: boolean;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function NavLink({
|
|
183
|
-
href,
|
|
184
|
-
className,
|
|
185
|
-
style,
|
|
186
|
-
activeStyle,
|
|
187
|
-
activeClassName,
|
|
188
|
-
exact = false,
|
|
189
|
-
...rest
|
|
190
|
-
}: NavLinkProps): React.ReactElement {
|
|
191
|
-
// 현재 경로와 비교
|
|
192
|
-
const isActive =
|
|
193
|
-
typeof window !== "undefined"
|
|
194
|
-
? exact
|
|
195
|
-
? window.location.pathname === href
|
|
196
|
-
: window.location.pathname.startsWith(href)
|
|
197
|
-
: false;
|
|
198
|
-
|
|
199
|
-
// className 처리
|
|
200
|
-
let resolvedClassName =
|
|
201
|
-
typeof className === "function" ? className({ isActive }) : className;
|
|
202
|
-
|
|
203
|
-
if (isActive && activeClassName) {
|
|
204
|
-
resolvedClassName = resolvedClassName
|
|
205
|
-
? `${resolvedClassName} ${activeClassName}`
|
|
206
|
-
: activeClassName;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// style 처리
|
|
210
|
-
let resolvedStyle =
|
|
211
|
-
typeof style === "function" ? style({ isActive }) : style;
|
|
212
|
-
|
|
213
|
-
if (isActive && activeStyle) {
|
|
214
|
-
resolvedStyle = { ...resolvedStyle, ...activeStyle };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<Link
|
|
219
|
-
href={href}
|
|
220
|
-
className={resolvedClassName}
|
|
221
|
-
style={resolvedStyle}
|
|
222
|
-
{...rest}
|
|
223
|
-
/>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export default Link;
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Link Component 🔗
|
|
3
|
+
* Client-side 네비게이션을 위한 Link 컴포넌트
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, {
|
|
7
|
+
type AnchorHTMLAttributes,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { navigate, prefetch } from "./router";
|
|
15
|
+
|
|
16
|
+
export interface LinkProps
|
|
17
|
+
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
18
|
+
/** 이동할 URL */
|
|
19
|
+
href: string;
|
|
20
|
+
/** history.replaceState 사용 여부 */
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
/** 마우스 hover 시 prefetch 여부 */
|
|
23
|
+
prefetch?: boolean;
|
|
24
|
+
/** 스크롤 위치 복원 여부 (기본: true) */
|
|
25
|
+
scroll?: boolean;
|
|
26
|
+
/** 자식 요소 */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Client-side 네비게이션 Link 컴포넌트
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { Link } from "@mandujs/core/client";
|
|
36
|
+
*
|
|
37
|
+
* // 기본 사용
|
|
38
|
+
* <Link href="/about">About</Link>
|
|
39
|
+
*
|
|
40
|
+
* // Prefetch 활성화
|
|
41
|
+
* <Link href="/users" prefetch>Users</Link>
|
|
42
|
+
*
|
|
43
|
+
* // Replace 모드 (뒤로가기 히스토리 없음)
|
|
44
|
+
* <Link href="/login" replace>Login</Link>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function Link({
|
|
48
|
+
href,
|
|
49
|
+
replace = false,
|
|
50
|
+
prefetch: shouldPrefetch = false,
|
|
51
|
+
scroll = true,
|
|
52
|
+
children,
|
|
53
|
+
onClick,
|
|
54
|
+
onMouseEnter,
|
|
55
|
+
onFocus,
|
|
56
|
+
...rest
|
|
57
|
+
}: LinkProps): React.ReactElement {
|
|
58
|
+
const prefetchedRef = useRef(false);
|
|
59
|
+
|
|
60
|
+
// 클릭 핸들러
|
|
61
|
+
const handleClick = useCallback(
|
|
62
|
+
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
63
|
+
// 사용자 정의 onClick 먼저 실행
|
|
64
|
+
onClick?.(event);
|
|
65
|
+
|
|
66
|
+
// 기본 동작 방지 조건
|
|
67
|
+
if (
|
|
68
|
+
event.defaultPrevented ||
|
|
69
|
+
event.button !== 0 ||
|
|
70
|
+
event.metaKey ||
|
|
71
|
+
event.altKey ||
|
|
72
|
+
event.ctrlKey ||
|
|
73
|
+
event.shiftKey
|
|
74
|
+
) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 외부 링크 체크
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(href, window.location.origin);
|
|
81
|
+
if (url.origin !== window.location.origin) {
|
|
82
|
+
return; // 외부 링크는 기본 동작
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Client-side 네비게이션
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
navigate(href, { replace, scroll });
|
|
91
|
+
},
|
|
92
|
+
[href, replace, scroll, onClick]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Prefetch 실행
|
|
96
|
+
const doPrefetch = useCallback(() => {
|
|
97
|
+
if (!shouldPrefetch || prefetchedRef.current) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(href, window.location.origin);
|
|
101
|
+
if (url.origin === window.location.origin) {
|
|
102
|
+
prefetch(href);
|
|
103
|
+
prefetchedRef.current = true;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// 무시
|
|
107
|
+
}
|
|
108
|
+
}, [href, shouldPrefetch]);
|
|
109
|
+
|
|
110
|
+
// 마우스 hover 핸들러
|
|
111
|
+
const handleMouseEnter = useCallback(
|
|
112
|
+
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
113
|
+
onMouseEnter?.(event);
|
|
114
|
+
doPrefetch();
|
|
115
|
+
},
|
|
116
|
+
[onMouseEnter, doPrefetch]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 포커스 핸들러 (키보드 네비게이션)
|
|
120
|
+
const handleFocus = useCallback(
|
|
121
|
+
(event: React.FocusEvent<HTMLAnchorElement>) => {
|
|
122
|
+
onFocus?.(event);
|
|
123
|
+
doPrefetch();
|
|
124
|
+
},
|
|
125
|
+
[onFocus, doPrefetch]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Viewport 진입 시 prefetch (IntersectionObserver)
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!shouldPrefetch || typeof IntersectionObserver === "undefined") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ref가 없으면 무시 (SSR)
|
|
135
|
+
return;
|
|
136
|
+
}, [shouldPrefetch]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<a
|
|
140
|
+
href={href}
|
|
141
|
+
onClick={handleClick}
|
|
142
|
+
onMouseEnter={handleMouseEnter}
|
|
143
|
+
onFocus={handleFocus}
|
|
144
|
+
data-mandu-link=""
|
|
145
|
+
{...rest}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</a>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* NavLink - 현재 경로와 일치할 때 활성 스타일 적용
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```tsx
|
|
157
|
+
* import { NavLink } from "@mandujs/core/client";
|
|
158
|
+
*
|
|
159
|
+
* <NavLink
|
|
160
|
+
* href="/about"
|
|
161
|
+
* className={({ isActive }) => isActive ? "active" : ""}
|
|
162
|
+
* >
|
|
163
|
+
* About
|
|
164
|
+
* </NavLink>
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export interface NavLinkProps extends Omit<LinkProps, "className" | "style"> {
|
|
168
|
+
/** 활성 상태에 따른 className */
|
|
169
|
+
className?: string | ((props: { isActive: boolean }) => string);
|
|
170
|
+
/** 활성 상태에 따른 style */
|
|
171
|
+
style?:
|
|
172
|
+
| React.CSSProperties
|
|
173
|
+
| ((props: { isActive: boolean }) => React.CSSProperties);
|
|
174
|
+
/** 활성 상태일 때 적용할 style (style과 병합됨) */
|
|
175
|
+
activeStyle?: React.CSSProperties;
|
|
176
|
+
/** 활성 상태일 때 추가할 className */
|
|
177
|
+
activeClassName?: string;
|
|
178
|
+
/** 정확히 일치해야 활성화 (기본: false) */
|
|
179
|
+
exact?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function NavLink({
|
|
183
|
+
href,
|
|
184
|
+
className,
|
|
185
|
+
style,
|
|
186
|
+
activeStyle,
|
|
187
|
+
activeClassName,
|
|
188
|
+
exact = false,
|
|
189
|
+
...rest
|
|
190
|
+
}: NavLinkProps): React.ReactElement {
|
|
191
|
+
// 현재 경로와 비교
|
|
192
|
+
const isActive =
|
|
193
|
+
typeof window !== "undefined"
|
|
194
|
+
? exact
|
|
195
|
+
? window.location.pathname === href
|
|
196
|
+
: window.location.pathname.startsWith(href)
|
|
197
|
+
: false;
|
|
198
|
+
|
|
199
|
+
// className 처리
|
|
200
|
+
let resolvedClassName =
|
|
201
|
+
typeof className === "function" ? className({ isActive }) : className;
|
|
202
|
+
|
|
203
|
+
if (isActive && activeClassName) {
|
|
204
|
+
resolvedClassName = resolvedClassName
|
|
205
|
+
? `${resolvedClassName} ${activeClassName}`
|
|
206
|
+
: activeClassName;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// style 처리
|
|
210
|
+
let resolvedStyle =
|
|
211
|
+
typeof style === "function" ? style({ isActive }) : style;
|
|
212
|
+
|
|
213
|
+
if (isActive && activeStyle) {
|
|
214
|
+
resolvedStyle = { ...resolvedStyle, ...activeStyle };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Link
|
|
219
|
+
href={href}
|
|
220
|
+
className={resolvedClassName}
|
|
221
|
+
style={resolvedStyle}
|
|
222
|
+
{...rest}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default Link;
|
package/src/client/globals.ts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu 전역 타입 선언
|
|
3
|
-
* 클라이언트 측 전역 상태의 타입 정의
|
|
4
|
-
*/
|
|
5
|
-
import type { Root } from "react-dom/client";
|
|
6
|
-
import type { RouterState } from "./router";
|
|
7
|
-
|
|
8
|
-
interface ManduRouteInfo {
|
|
9
|
-
id: string;
|
|
10
|
-
pattern: string;
|
|
11
|
-
params: Record<string, string>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ManduDataEntry {
|
|
15
|
-
serverData: unknown;
|
|
16
|
-
timestamp?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
declare global {
|
|
20
|
-
interface Window {
|
|
21
|
-
/** 서버에서 전달된 데이터 (routeId → data) */
|
|
22
|
-
__MANDU_DATA__?: Record<string, ManduDataEntry>;
|
|
23
|
-
|
|
24
|
-
/** 직렬화된 서버 데이터 (raw JSON) */
|
|
25
|
-
__MANDU_DATA_RAW__?: string;
|
|
26
|
-
|
|
27
|
-
/** 현재 라우트 정보 */
|
|
28
|
-
__MANDU_ROUTE__?: ManduRouteInfo;
|
|
29
|
-
|
|
30
|
-
/** 클라이언트 라우터 상태 */
|
|
31
|
-
__MANDU_ROUTER_STATE__?: RouterState;
|
|
32
|
-
|
|
33
|
-
/** 라우터 상태 변경 리스너 */
|
|
34
|
-
__MANDU_ROUTER_LISTENERS__?: Set<(state: RouterState) => void>;
|
|
35
|
-
|
|
36
|
-
/** Hydrated roots 추적 (unmount용) */
|
|
37
|
-
__MANDU_ROOTS__?: Map<string, Root>;
|
|
38
|
-
|
|
39
|
-
/** React 인스턴스 공유 */
|
|
40
|
-
__MANDU_REACT__?: typeof import("react");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export {};
|
|
1
|
+
/**
|
|
2
|
+
* Mandu 전역 타입 선언
|
|
3
|
+
* 클라이언트 측 전역 상태의 타입 정의
|
|
4
|
+
*/
|
|
5
|
+
import type { Root } from "react-dom/client";
|
|
6
|
+
import type { RouterState } from "./router";
|
|
7
|
+
|
|
8
|
+
interface ManduRouteInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
pattern: string;
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ManduDataEntry {
|
|
15
|
+
serverData: unknown;
|
|
16
|
+
timestamp?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare global {
|
|
20
|
+
interface Window {
|
|
21
|
+
/** 서버에서 전달된 데이터 (routeId → data) */
|
|
22
|
+
__MANDU_DATA__?: Record<string, ManduDataEntry>;
|
|
23
|
+
|
|
24
|
+
/** 직렬화된 서버 데이터 (raw JSON) */
|
|
25
|
+
__MANDU_DATA_RAW__?: string;
|
|
26
|
+
|
|
27
|
+
/** 현재 라우트 정보 */
|
|
28
|
+
__MANDU_ROUTE__?: ManduRouteInfo;
|
|
29
|
+
|
|
30
|
+
/** 클라이언트 라우터 상태 */
|
|
31
|
+
__MANDU_ROUTER_STATE__?: RouterState;
|
|
32
|
+
|
|
33
|
+
/** 라우터 상태 변경 리스너 */
|
|
34
|
+
__MANDU_ROUTER_LISTENERS__?: Set<(state: RouterState) => void>;
|
|
35
|
+
|
|
36
|
+
/** Hydrated roots 추적 (unmount용) */
|
|
37
|
+
__MANDU_ROOTS__?: Map<string, Root>;
|
|
38
|
+
|
|
39
|
+
/** React 인스턴스 공유 */
|
|
40
|
+
__MANDU_REACT__?: typeof import("react");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {};
|