@mandujs/mcp 0.13.0 → 0.17.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/README.md +102 -7
- package/package.json +3 -2
- package/src/adapters/index.ts +20 -20
- package/src/adapters/monitor-adapter.ts +100 -100
- package/src/adapters/tool-adapter.ts +88 -88
- package/src/executor/error-handler.ts +250 -250
- package/src/executor/index.ts +22 -22
- package/src/executor/tool-executor.ts +148 -148
- package/src/hooks/config-watcher.ts +174 -174
- package/src/hooks/index.ts +23 -23
- package/src/hooks/mcp-hooks.ts +227 -227
- package/src/logging/index.ts +15 -15
- package/src/logging/mcp-transport.ts +134 -134
- package/src/registry/index.ts +13 -13
- package/src/registry/mcp-tool-registry.ts +298 -298
- package/src/resources/skills/guides.ts +1136 -1136
- package/src/resources/skills/index.ts +12 -12
- package/src/resources/skills/loader.ts +218 -218
- package/src/resources/skills/mandu-composition/SKILL.md +91 -91
- package/src/resources/skills/mandu-composition/metadata.json +13 -13
- package/src/resources/skills/mandu-composition/rules/_sections.md +26 -26
- package/src/resources/skills/mandu-composition/rules/_template.md +77 -77
- package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -146
- package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -164
- package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -161
- package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -167
- package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -149
- package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -148
- package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -150
- package/src/resources/skills/mandu-deployment/SKILL.md +92 -92
- package/src/resources/skills/mandu-deployment/_sections.md +41 -41
- package/src/resources/skills/mandu-deployment/_template.md +38 -38
- package/src/resources/skills/mandu-deployment/metadata.json +13 -13
- package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -109
- package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -115
- package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -219
- package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -150
- package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -223
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -152
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -179
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -323
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -140
- package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -82
- package/src/resources/skills/mandu-fs-routes/metadata.json +12 -12
- package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -36
- package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -69
- package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -65
- package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -93
- package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -55
- package/src/resources/skills/mandu-guard/SKILL.md +129 -129
- package/src/resources/skills/mandu-guard/metadata.json +12 -12
- package/src/resources/skills/mandu-guard/rules/_sections.md +36 -36
- package/src/resources/skills/mandu-guard/rules/_template.md +82 -82
- package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -100
- package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -76
- package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -81
- package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -80
- package/src/resources/skills/mandu-hydration/SKILL.md +91 -91
- package/src/resources/skills/mandu-hydration/metadata.json +12 -12
- package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -31
- package/src/resources/skills/mandu-hydration/rules/_template.md +72 -72
- package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -109
- package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -55
- package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -113
- package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -68
- package/src/resources/skills/mandu-performance/SKILL.md +85 -85
- package/src/resources/skills/mandu-performance/metadata.json +14 -14
- package/src/resources/skills/mandu-performance/rules/_sections.md +31 -31
- package/src/resources/skills/mandu-performance/rules/_template.md +64 -64
- package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -103
- package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -95
- package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -124
- package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -125
- package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -80
- package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -145
- package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -98
- package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -154
- package/src/resources/skills/mandu-security/SKILL.md +87 -87
- package/src/resources/skills/mandu-security/metadata.json +13 -13
- package/src/resources/skills/mandu-security/rules/_sections.md +31 -31
- package/src/resources/skills/mandu-security/rules/_template.md +74 -74
- package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -127
- package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -133
- package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -148
- package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -146
- package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -138
- package/src/resources/skills/mandu-slot/SKILL.md +85 -85
- package/src/resources/skills/mandu-slot/metadata.json +12 -12
- package/src/resources/skills/mandu-slot/rules/_sections.md +36 -36
- package/src/resources/skills/mandu-slot/rules/_template.md +63 -63
- package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -38
- package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -56
- package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -59
- package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -64
- package/src/resources/skills/mandu-styling/SKILL.md +154 -154
- package/src/resources/skills/mandu-styling/_sections.md +43 -43
- package/src/resources/skills/mandu-styling/_template.md +32 -32
- package/src/resources/skills/mandu-styling/metadata.json +15 -15
- package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -235
- package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -255
- package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -205
- package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -272
- package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -167
- package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -221
- package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -209
- package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -192
- package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -162
- package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -164
- package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +170 -170
- package/src/resources/skills/mandu-styling/rules/style-tailwind-v4-gotchas.md +179 -179
- package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -229
- package/src/resources/skills/mandu-testing/SKILL.md +99 -99
- package/src/resources/skills/mandu-testing/metadata.json +13 -13
- package/src/resources/skills/mandu-testing/rules/_sections.md +26 -26
- package/src/resources/skills/mandu-testing/rules/_template.md +65 -65
- package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -195
- package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -196
- package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -219
- package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -192
- package/src/resources/skills/mandu-ui/SKILL.md +117 -117
- package/src/resources/skills/mandu-ui/_sections.md +23 -23
- package/src/resources/skills/mandu-ui/_template.md +32 -32
- package/src/resources/skills/mandu-ui/metadata.json +13 -13
- package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -232
- package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -238
- package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -259
- package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -258
- package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -213
- package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -209
- package/src/resources/skills/recipes.ts +932 -932
- package/src/tools/ate.ts +219 -0
- package/src/tools/index.ts +4 -1
- package/src/tools/project.ts +334 -334
- package/src/tools/runtime.ts +497 -497
- package/src/tools/seo.ts +417 -417
- package/src/utils/withWarnings.ts +83 -83
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Dark Mode Implementation
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: User preference and accessibility support
|
|
5
|
-
tags: styling, theme, darkmode, accessibility
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Dark Mode Implementation
|
|
9
|
-
|
|
10
|
-
**Impact: MEDIUM (User preference and accessibility support)**
|
|
11
|
-
|
|
12
|
-
다크모드를 구현하여 사용자 선호도와 접근성을 지원하세요.
|
|
13
|
-
|
|
14
|
-
## CSS Variables 기반 테마
|
|
15
|
-
|
|
16
|
-
```css
|
|
17
|
-
/* app/globals.css */
|
|
18
|
-
@layer base {
|
|
19
|
-
:root {
|
|
20
|
-
--background: 0 0% 100%;
|
|
21
|
-
--foreground: 222.2 84% 4.9%;
|
|
22
|
-
--card: 0 0% 100%;
|
|
23
|
-
--card-foreground: 222.2 84% 4.9%;
|
|
24
|
-
--primary: 221.2 83.2% 53.3%;
|
|
25
|
-
--primary-foreground: 210 40% 98%;
|
|
26
|
-
--muted: 210 40% 96.1%;
|
|
27
|
-
--muted-foreground: 215.4 16.3% 46.9%;
|
|
28
|
-
--border: 214.3 31.8% 91.4%;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.dark {
|
|
32
|
-
--background: 222.2 84% 4.9%;
|
|
33
|
-
--foreground: 210 40% 98%;
|
|
34
|
-
--card: 222.2 84% 4.9%;
|
|
35
|
-
--card-foreground: 210 40% 98%;
|
|
36
|
-
--primary: 217.2 91.2% 59.8%;
|
|
37
|
-
--primary-foreground: 222.2 47.4% 11.2%;
|
|
38
|
-
--muted: 217.2 32.6% 17.5%;
|
|
39
|
-
--muted-foreground: 215 20.2% 65.1%;
|
|
40
|
-
--border: 217.2 32.6% 17.5%;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Tailwind dark: 활성화
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// tailwind.config.ts
|
|
49
|
-
export default {
|
|
50
|
-
darkMode: "class", // 또는 "media"
|
|
51
|
-
// ...
|
|
52
|
-
};
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## 테마 Provider Island
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
// app/theme/client.tsx
|
|
59
|
-
"use client";
|
|
60
|
-
|
|
61
|
-
import { createContext, useContext, useEffect, useState } from "react";
|
|
62
|
-
|
|
63
|
-
type Theme = "light" | "dark" | "system";
|
|
64
|
-
|
|
65
|
-
interface ThemeContextValue {
|
|
66
|
-
theme: Theme;
|
|
67
|
-
setTheme: (theme: Theme) => void;
|
|
68
|
-
resolvedTheme: "light" | "dark";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
72
|
-
|
|
73
|
-
export function ThemeProviderIsland({ children }: { children: React.ReactNode }) {
|
|
74
|
-
const [theme, setTheme] = useState<Theme>("system");
|
|
75
|
-
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
|
|
76
|
-
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
// 저장된 테마 복원
|
|
79
|
-
const stored = localStorage.getItem("theme") as Theme | null;
|
|
80
|
-
if (stored) {
|
|
81
|
-
setTheme(stored);
|
|
82
|
-
}
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
const root = document.documentElement;
|
|
87
|
-
|
|
88
|
-
if (theme === "system") {
|
|
89
|
-
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
90
|
-
setResolvedTheme(systemDark ? "dark" : "light");
|
|
91
|
-
root.classList.toggle("dark", systemDark);
|
|
92
|
-
} else {
|
|
93
|
-
setResolvedTheme(theme);
|
|
94
|
-
root.classList.toggle("dark", theme === "dark");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
localStorage.setItem("theme", theme);
|
|
98
|
-
}, [theme]);
|
|
99
|
-
|
|
100
|
-
// System 변경 감지
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (theme !== "system") return;
|
|
103
|
-
|
|
104
|
-
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
105
|
-
const handler = (e: MediaQueryListEvent) => {
|
|
106
|
-
setResolvedTheme(e.matches ? "dark" : "light");
|
|
107
|
-
document.documentElement.classList.toggle("dark", e.matches);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
mq.addEventListener("change", handler);
|
|
111
|
-
return () => mq.removeEventListener("change", handler);
|
|
112
|
-
}, [theme]);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
|
|
116
|
-
{children}
|
|
117
|
-
</ThemeContext.Provider>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function useTheme() {
|
|
122
|
-
const context = useContext(ThemeContext);
|
|
123
|
-
if (!context) {
|
|
124
|
-
throw new Error("useTheme must be used within ThemeProviderIsland");
|
|
125
|
-
}
|
|
126
|
-
return context;
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## 테마 토글 컴포넌트
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
// app/theme/toggle.tsx
|
|
134
|
-
"use client";
|
|
135
|
-
|
|
136
|
-
import { useTheme } from "./client";
|
|
137
|
-
|
|
138
|
-
export function ThemeToggleIsland() {
|
|
139
|
-
const { theme, setTheme, resolvedTheme } = useTheme();
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<button
|
|
143
|
-
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
|
|
144
|
-
className="p-2 rounded-md hover:bg-muted"
|
|
145
|
-
aria-label={`Switch to ${resolvedTheme === "dark" ? "light" : "dark"} mode`}
|
|
146
|
-
>
|
|
147
|
-
{resolvedTheme === "dark" ? "🌙" : "☀️"}
|
|
148
|
-
</button>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## 플래시 방지 스크립트
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
// app/layout.tsx
|
|
157
|
-
export default function RootLayout({ children }) {
|
|
158
|
-
return (
|
|
159
|
-
<html lang="ko" suppressHydrationWarning>
|
|
160
|
-
<head>
|
|
161
|
-
{/* 다크모드 플래시 방지 */}
|
|
162
|
-
<script
|
|
163
|
-
dangerouslySetInnerHTML={{
|
|
164
|
-
__html: `
|
|
165
|
-
(function() {
|
|
166
|
-
const theme = localStorage.getItem('theme');
|
|
167
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
168
|
-
|
|
169
|
-
if (theme === 'dark' || (!theme && prefersDark)) {
|
|
170
|
-
document.documentElement.classList.add('dark');
|
|
171
|
-
}
|
|
172
|
-
})();
|
|
173
|
-
`,
|
|
174
|
-
}}
|
|
175
|
-
/>
|
|
176
|
-
</head>
|
|
177
|
-
<body>
|
|
178
|
-
<ThemeProviderIsland>
|
|
179
|
-
{children}
|
|
180
|
-
</ThemeProviderIsland>
|
|
181
|
-
</body>
|
|
182
|
-
</html>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## Island 간 테마 동기화
|
|
188
|
-
|
|
189
|
-
```tsx
|
|
190
|
-
// useIslandEvent로 테마 변경 전파
|
|
191
|
-
import { useIslandEvent } from "@mandujs/core/client";
|
|
192
|
-
|
|
193
|
-
export function ThemeProviderIsland({ children }) {
|
|
194
|
-
const { emit } = useIslandEvent<{ theme: string }>("theme-change");
|
|
195
|
-
const [theme, setTheme] = useState("system");
|
|
196
|
-
|
|
197
|
-
const handleSetTheme = (newTheme: Theme) => {
|
|
198
|
-
setTheme(newTheme);
|
|
199
|
-
emit({ theme: newTheme }); // 다른 Island에 전파
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// ...
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 다른 Island에서 수신
|
|
206
|
-
export function AnotherIsland() {
|
|
207
|
-
useIslandEvent<{ theme: string }>("theme-change", (data) => {
|
|
208
|
-
console.log("Theme changed to:", data.theme);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## 다크모드 특화 스타일
|
|
214
|
-
|
|
215
|
-
```tsx
|
|
216
|
-
// Tailwind dark: 접두사 사용
|
|
217
|
-
<div className="bg-white dark:bg-gray-900">
|
|
218
|
-
<p className="text-gray-900 dark:text-gray-100">
|
|
219
|
-
Content
|
|
220
|
-
</p>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
// 복잡한 경우 CSS Variables 활용
|
|
224
|
-
<div className="bg-[hsl(var(--background))]">
|
|
225
|
-
{/* 자동으로 테마 반영 */}
|
|
226
|
-
</div>
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Reference: [Tailwind Dark Mode](https://tailwindcss.com/docs/dark-mode)
|
|
1
|
+
---
|
|
2
|
+
title: Dark Mode Implementation
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: User preference and accessibility support
|
|
5
|
+
tags: styling, theme, darkmode, accessibility
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Dark Mode Implementation
|
|
9
|
+
|
|
10
|
+
**Impact: MEDIUM (User preference and accessibility support)**
|
|
11
|
+
|
|
12
|
+
다크모드를 구현하여 사용자 선호도와 접근성을 지원하세요.
|
|
13
|
+
|
|
14
|
+
## CSS Variables 기반 테마
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
/* app/globals.css */
|
|
18
|
+
@layer base {
|
|
19
|
+
:root {
|
|
20
|
+
--background: 0 0% 100%;
|
|
21
|
+
--foreground: 222.2 84% 4.9%;
|
|
22
|
+
--card: 0 0% 100%;
|
|
23
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
24
|
+
--primary: 221.2 83.2% 53.3%;
|
|
25
|
+
--primary-foreground: 210 40% 98%;
|
|
26
|
+
--muted: 210 40% 96.1%;
|
|
27
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
28
|
+
--border: 214.3 31.8% 91.4%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dark {
|
|
32
|
+
--background: 222.2 84% 4.9%;
|
|
33
|
+
--foreground: 210 40% 98%;
|
|
34
|
+
--card: 222.2 84% 4.9%;
|
|
35
|
+
--card-foreground: 210 40% 98%;
|
|
36
|
+
--primary: 217.2 91.2% 59.8%;
|
|
37
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
38
|
+
--muted: 217.2 32.6% 17.5%;
|
|
39
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
40
|
+
--border: 217.2 32.6% 17.5%;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Tailwind dark: 활성화
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// tailwind.config.ts
|
|
49
|
+
export default {
|
|
50
|
+
darkMode: "class", // 또는 "media"
|
|
51
|
+
// ...
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 테마 Provider Island
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// app/theme/client.tsx
|
|
59
|
+
"use client";
|
|
60
|
+
|
|
61
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
62
|
+
|
|
63
|
+
type Theme = "light" | "dark" | "system";
|
|
64
|
+
|
|
65
|
+
interface ThemeContextValue {
|
|
66
|
+
theme: Theme;
|
|
67
|
+
setTheme: (theme: Theme) => void;
|
|
68
|
+
resolvedTheme: "light" | "dark";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
72
|
+
|
|
73
|
+
export function ThemeProviderIsland({ children }: { children: React.ReactNode }) {
|
|
74
|
+
const [theme, setTheme] = useState<Theme>("system");
|
|
75
|
+
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
// 저장된 테마 복원
|
|
79
|
+
const stored = localStorage.getItem("theme") as Theme | null;
|
|
80
|
+
if (stored) {
|
|
81
|
+
setTheme(stored);
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const root = document.documentElement;
|
|
87
|
+
|
|
88
|
+
if (theme === "system") {
|
|
89
|
+
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
90
|
+
setResolvedTheme(systemDark ? "dark" : "light");
|
|
91
|
+
root.classList.toggle("dark", systemDark);
|
|
92
|
+
} else {
|
|
93
|
+
setResolvedTheme(theme);
|
|
94
|
+
root.classList.toggle("dark", theme === "dark");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
localStorage.setItem("theme", theme);
|
|
98
|
+
}, [theme]);
|
|
99
|
+
|
|
100
|
+
// System 변경 감지
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (theme !== "system") return;
|
|
103
|
+
|
|
104
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
105
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
106
|
+
setResolvedTheme(e.matches ? "dark" : "light");
|
|
107
|
+
document.documentElement.classList.toggle("dark", e.matches);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
mq.addEventListener("change", handler);
|
|
111
|
+
return () => mq.removeEventListener("change", handler);
|
|
112
|
+
}, [theme]);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
|
|
116
|
+
{children}
|
|
117
|
+
</ThemeContext.Provider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useTheme() {
|
|
122
|
+
const context = useContext(ThemeContext);
|
|
123
|
+
if (!context) {
|
|
124
|
+
throw new Error("useTheme must be used within ThemeProviderIsland");
|
|
125
|
+
}
|
|
126
|
+
return context;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 테마 토글 컴포넌트
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// app/theme/toggle.tsx
|
|
134
|
+
"use client";
|
|
135
|
+
|
|
136
|
+
import { useTheme } from "./client";
|
|
137
|
+
|
|
138
|
+
export function ThemeToggleIsland() {
|
|
139
|
+
const { theme, setTheme, resolvedTheme } = useTheme();
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
|
|
144
|
+
className="p-2 rounded-md hover:bg-muted"
|
|
145
|
+
aria-label={`Switch to ${resolvedTheme === "dark" ? "light" : "dark"} mode`}
|
|
146
|
+
>
|
|
147
|
+
{resolvedTheme === "dark" ? "🌙" : "☀️"}
|
|
148
|
+
</button>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 플래시 방지 스크립트
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// app/layout.tsx
|
|
157
|
+
export default function RootLayout({ children }) {
|
|
158
|
+
return (
|
|
159
|
+
<html lang="ko" suppressHydrationWarning>
|
|
160
|
+
<head>
|
|
161
|
+
{/* 다크모드 플래시 방지 */}
|
|
162
|
+
<script
|
|
163
|
+
dangerouslySetInnerHTML={{
|
|
164
|
+
__html: `
|
|
165
|
+
(function() {
|
|
166
|
+
const theme = localStorage.getItem('theme');
|
|
167
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
168
|
+
|
|
169
|
+
if (theme === 'dark' || (!theme && prefersDark)) {
|
|
170
|
+
document.documentElement.classList.add('dark');
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
`,
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</head>
|
|
177
|
+
<body>
|
|
178
|
+
<ThemeProviderIsland>
|
|
179
|
+
{children}
|
|
180
|
+
</ThemeProviderIsland>
|
|
181
|
+
</body>
|
|
182
|
+
</html>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Island 간 테마 동기화
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// useIslandEvent로 테마 변경 전파
|
|
191
|
+
import { useIslandEvent } from "@mandujs/core/client";
|
|
192
|
+
|
|
193
|
+
export function ThemeProviderIsland({ children }) {
|
|
194
|
+
const { emit } = useIslandEvent<{ theme: string }>("theme-change");
|
|
195
|
+
const [theme, setTheme] = useState("system");
|
|
196
|
+
|
|
197
|
+
const handleSetTheme = (newTheme: Theme) => {
|
|
198
|
+
setTheme(newTheme);
|
|
199
|
+
emit({ theme: newTheme }); // 다른 Island에 전파
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// ...
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 다른 Island에서 수신
|
|
206
|
+
export function AnotherIsland() {
|
|
207
|
+
useIslandEvent<{ theme: string }>("theme-change", (data) => {
|
|
208
|
+
console.log("Theme changed to:", data.theme);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 다크모드 특화 스타일
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// Tailwind dark: 접두사 사용
|
|
217
|
+
<div className="bg-white dark:bg-gray-900">
|
|
218
|
+
<p className="text-gray-900 dark:text-gray-100">
|
|
219
|
+
Content
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
// 복잡한 경우 CSS Variables 활용
|
|
224
|
+
<div className="bg-[hsl(var(--background))]">
|
|
225
|
+
{/* 자동으로 테마 반영 */}
|
|
226
|
+
</div>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Reference: [Tailwind Dark Mode](https://tailwindcss.com/docs/dark-mode)
|
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: mandu-testing
|
|
3
|
-
description: |
|
|
4
|
-
Testing patterns for Mandu applications. Use when writing unit tests,
|
|
5
|
-
integration tests, or E2E tests. Triggers on test, spec, Bun test,
|
|
6
|
-
Playwright, or testing tasks.
|
|
7
|
-
license: MIT
|
|
8
|
-
metadata:
|
|
9
|
-
author: mandu
|
|
10
|
-
version: "1.0.0"
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Mandu Testing
|
|
14
|
-
|
|
15
|
-
Mandu 애플리케이션의 테스트 패턴 가이드. Bun test를 활용한 단위 테스트, slot 테스트, Island 컴포넌트 테스트, Playwright E2E 테스트를 다룹니다.
|
|
16
|
-
|
|
17
|
-
## When to Apply
|
|
18
|
-
|
|
19
|
-
Reference these guidelines when:
|
|
20
|
-
- Writing unit tests for slots
|
|
21
|
-
- Testing Island components
|
|
22
|
-
- Setting up E2E tests with Playwright
|
|
23
|
-
- Mocking external dependencies
|
|
24
|
-
- Testing authentication flows
|
|
25
|
-
|
|
26
|
-
## Rule Categories by Priority
|
|
27
|
-
|
|
28
|
-
| Priority | Category | Impact | Prefix |
|
|
29
|
-
|----------|----------|--------|--------|
|
|
30
|
-
| 1 | Slot Testing | HIGH | `test-slot-` |
|
|
31
|
-
| 2 | Component Testing | HIGH | `test-component-` |
|
|
32
|
-
| 3 | E2E Testing | MEDIUM | `test-e2e-` |
|
|
33
|
-
| 4 | Mocking | MEDIUM | `test-mock-` |
|
|
34
|
-
|
|
35
|
-
## Quick Reference
|
|
36
|
-
|
|
37
|
-
### 1. Slot Testing (HIGH)
|
|
38
|
-
|
|
39
|
-
- `test-slot-unit` - Unit test slot handlers
|
|
40
|
-
- `test-slot-guard` - Test guard authentication
|
|
41
|
-
- `test-slot-integration` - Integration test with database
|
|
42
|
-
|
|
43
|
-
### 2. Component Testing (HIGH)
|
|
44
|
-
|
|
45
|
-
- `test-component-island` - Test Island components
|
|
46
|
-
- `test-component-render` - Test rendering output
|
|
47
|
-
- `test-component-interaction` - Test user interactions
|
|
48
|
-
|
|
49
|
-
### 3. E2E Testing (MEDIUM)
|
|
50
|
-
|
|
51
|
-
- `test-e2e-playwright` - Playwright setup and patterns
|
|
52
|
-
- `test-e2e-auth` - Test authentication flows
|
|
53
|
-
- `test-e2e-navigation` - Test page navigation
|
|
54
|
-
|
|
55
|
-
### 4. Mocking (MEDIUM)
|
|
56
|
-
|
|
57
|
-
- `test-mock-fetch` - Mock fetch requests
|
|
58
|
-
- `test-mock-database` - Mock database operations
|
|
59
|
-
|
|
60
|
-
## Bun Test Quick Start
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
# Run all tests
|
|
64
|
-
bun test
|
|
65
|
-
|
|
66
|
-
# Run specific test file
|
|
67
|
-
bun test src/slots/user.test.ts
|
|
68
|
-
|
|
69
|
-
# Watch mode
|
|
70
|
-
bun test --watch
|
|
71
|
-
|
|
72
|
-
# Coverage
|
|
73
|
-
bun test --coverage
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Test File Convention
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
spec/slots/
|
|
80
|
-
├── users.slot.ts
|
|
81
|
-
├── users.slot.test.ts # Slot tests
|
|
82
|
-
app/
|
|
83
|
-
├── dashboard/
|
|
84
|
-
│ ├── client.tsx
|
|
85
|
-
│ └── client.test.tsx # Component tests
|
|
86
|
-
tests/
|
|
87
|
-
└── e2e/
|
|
88
|
-
└── auth.spec.ts # E2E tests
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## How to Use
|
|
92
|
-
|
|
93
|
-
Read individual rule files for detailed explanations:
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
rules/test-slot-unit.md
|
|
97
|
-
rules/test-component-island.md
|
|
98
|
-
rules/test-e2e-playwright.md
|
|
99
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: mandu-testing
|
|
3
|
+
description: |
|
|
4
|
+
Testing patterns for Mandu applications. Use when writing unit tests,
|
|
5
|
+
integration tests, or E2E tests. Triggers on test, spec, Bun test,
|
|
6
|
+
Playwright, or testing tasks.
|
|
7
|
+
license: MIT
|
|
8
|
+
metadata:
|
|
9
|
+
author: mandu
|
|
10
|
+
version: "1.0.0"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Mandu Testing
|
|
14
|
+
|
|
15
|
+
Mandu 애플리케이션의 테스트 패턴 가이드. Bun test를 활용한 단위 테스트, slot 테스트, Island 컴포넌트 테스트, Playwright E2E 테스트를 다룹니다.
|
|
16
|
+
|
|
17
|
+
## When to Apply
|
|
18
|
+
|
|
19
|
+
Reference these guidelines when:
|
|
20
|
+
- Writing unit tests for slots
|
|
21
|
+
- Testing Island components
|
|
22
|
+
- Setting up E2E tests with Playwright
|
|
23
|
+
- Mocking external dependencies
|
|
24
|
+
- Testing authentication flows
|
|
25
|
+
|
|
26
|
+
## Rule Categories by Priority
|
|
27
|
+
|
|
28
|
+
| Priority | Category | Impact | Prefix |
|
|
29
|
+
|----------|----------|--------|--------|
|
|
30
|
+
| 1 | Slot Testing | HIGH | `test-slot-` |
|
|
31
|
+
| 2 | Component Testing | HIGH | `test-component-` |
|
|
32
|
+
| 3 | E2E Testing | MEDIUM | `test-e2e-` |
|
|
33
|
+
| 4 | Mocking | MEDIUM | `test-mock-` |
|
|
34
|
+
|
|
35
|
+
## Quick Reference
|
|
36
|
+
|
|
37
|
+
### 1. Slot Testing (HIGH)
|
|
38
|
+
|
|
39
|
+
- `test-slot-unit` - Unit test slot handlers
|
|
40
|
+
- `test-slot-guard` - Test guard authentication
|
|
41
|
+
- `test-slot-integration` - Integration test with database
|
|
42
|
+
|
|
43
|
+
### 2. Component Testing (HIGH)
|
|
44
|
+
|
|
45
|
+
- `test-component-island` - Test Island components
|
|
46
|
+
- `test-component-render` - Test rendering output
|
|
47
|
+
- `test-component-interaction` - Test user interactions
|
|
48
|
+
|
|
49
|
+
### 3. E2E Testing (MEDIUM)
|
|
50
|
+
|
|
51
|
+
- `test-e2e-playwright` - Playwright setup and patterns
|
|
52
|
+
- `test-e2e-auth` - Test authentication flows
|
|
53
|
+
- `test-e2e-navigation` - Test page navigation
|
|
54
|
+
|
|
55
|
+
### 4. Mocking (MEDIUM)
|
|
56
|
+
|
|
57
|
+
- `test-mock-fetch` - Mock fetch requests
|
|
58
|
+
- `test-mock-database` - Mock database operations
|
|
59
|
+
|
|
60
|
+
## Bun Test Quick Start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Run all tests
|
|
64
|
+
bun test
|
|
65
|
+
|
|
66
|
+
# Run specific test file
|
|
67
|
+
bun test src/slots/user.test.ts
|
|
68
|
+
|
|
69
|
+
# Watch mode
|
|
70
|
+
bun test --watch
|
|
71
|
+
|
|
72
|
+
# Coverage
|
|
73
|
+
bun test --coverage
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Test File Convention
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
spec/slots/
|
|
80
|
+
├── users.slot.ts
|
|
81
|
+
├── users.slot.test.ts # Slot tests
|
|
82
|
+
app/
|
|
83
|
+
├── dashboard/
|
|
84
|
+
│ ├── client.tsx
|
|
85
|
+
│ └── client.test.tsx # Component tests
|
|
86
|
+
tests/
|
|
87
|
+
└── e2e/
|
|
88
|
+
└── auth.spec.ts # E2E tests
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How to Use
|
|
92
|
+
|
|
93
|
+
Read individual rule files for detailed explanations:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
rules/test-slot-unit.md
|
|
97
|
+
rules/test-component-island.md
|
|
98
|
+
rules/test-e2e-playwright.md
|
|
99
|
+
```
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "1.0.0",
|
|
3
|
-
"organization": "Mandu Framework",
|
|
4
|
-
"date": "February 2026",
|
|
5
|
-
"abstract": "Mandu 애플리케이션 테스트 패턴 가이드. Bun test를 활용한 slot 단위/통합 테스트, Island 컴포넌트 테스트, Playwright E2E 테스트, 모킹 패턴을 다룹니다.",
|
|
6
|
-
"references": [
|
|
7
|
-
"https://bun.sh/docs/cli/test",
|
|
8
|
-
"https://playwright.dev/docs/intro",
|
|
9
|
-
"https://testing-library.com/docs/react-testing-library/intro",
|
|
10
|
-
"https://kentcdodds.com/blog/write-tests"
|
|
11
|
-
],
|
|
12
|
-
"tags": ["testing", "bun-test", "playwright", "e2e", "unit", "mandu"]
|
|
13
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"organization": "Mandu Framework",
|
|
4
|
+
"date": "February 2026",
|
|
5
|
+
"abstract": "Mandu 애플리케이션 테스트 패턴 가이드. Bun test를 활용한 slot 단위/통합 테스트, Island 컴포넌트 테스트, Playwright E2E 테스트, 모킹 패턴을 다룹니다.",
|
|
6
|
+
"references": [
|
|
7
|
+
"https://bun.sh/docs/cli/test",
|
|
8
|
+
"https://playwright.dev/docs/intro",
|
|
9
|
+
"https://testing-library.com/docs/react-testing-library/intro",
|
|
10
|
+
"https://kentcdodds.com/blog/write-tests"
|
|
11
|
+
],
|
|
12
|
+
"tags": ["testing", "bun-test", "playwright", "e2e", "unit", "mandu"]
|
|
13
|
+
}
|