@mandujs/mcp 0.9.19 → 0.9.21

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.
Files changed (122) hide show
  1. package/README.md +320 -0
  2. package/package.json +1 -1
  3. package/src/activity-monitor.ts +847 -231
  4. package/src/resources/handlers.ts +244 -0
  5. package/src/resources/skills/guides.ts +1136 -0
  6. package/src/resources/skills/index.ts +12 -0
  7. package/src/resources/skills/loader.ts +218 -0
  8. package/src/resources/skills/mandu-composition/SKILL.md +91 -0
  9. package/src/resources/skills/mandu-composition/metadata.json +13 -0
  10. package/src/resources/skills/mandu-composition/rules/_sections.md +26 -0
  11. package/src/resources/skills/mandu-composition/rules/_template.md +77 -0
  12. package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -0
  13. package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -0
  14. package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -0
  15. package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -0
  16. package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -0
  17. package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -0
  18. package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -0
  19. package/src/resources/skills/mandu-deployment/SKILL.md +92 -0
  20. package/src/resources/skills/mandu-deployment/_sections.md +41 -0
  21. package/src/resources/skills/mandu-deployment/_template.md +38 -0
  22. package/src/resources/skills/mandu-deployment/metadata.json +13 -0
  23. package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -0
  24. package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -0
  25. package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -0
  26. package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -0
  27. package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -0
  28. package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -0
  29. package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -0
  30. package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -0
  31. package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -0
  32. package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -0
  33. package/src/resources/skills/mandu-fs-routes/metadata.json +12 -0
  34. package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -0
  35. package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -0
  36. package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -0
  37. package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -0
  38. package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -0
  39. package/src/resources/skills/mandu-guard/SKILL.md +129 -0
  40. package/src/resources/skills/mandu-guard/metadata.json +12 -0
  41. package/src/resources/skills/mandu-guard/rules/_sections.md +36 -0
  42. package/src/resources/skills/mandu-guard/rules/_template.md +82 -0
  43. package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -0
  44. package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -0
  45. package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -0
  46. package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -0
  47. package/src/resources/skills/mandu-hydration/SKILL.md +91 -0
  48. package/src/resources/skills/mandu-hydration/metadata.json +12 -0
  49. package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -0
  50. package/src/resources/skills/mandu-hydration/rules/_template.md +72 -0
  51. package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -0
  52. package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -0
  53. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -0
  54. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -0
  55. package/src/resources/skills/mandu-performance/SKILL.md +85 -0
  56. package/src/resources/skills/mandu-performance/metadata.json +14 -0
  57. package/src/resources/skills/mandu-performance/rules/_sections.md +31 -0
  58. package/src/resources/skills/mandu-performance/rules/_template.md +64 -0
  59. package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -0
  60. package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -0
  61. package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -0
  62. package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -0
  63. package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -0
  64. package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -0
  65. package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -0
  66. package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -0
  67. package/src/resources/skills/mandu-security/SKILL.md +87 -0
  68. package/src/resources/skills/mandu-security/metadata.json +13 -0
  69. package/src/resources/skills/mandu-security/rules/_sections.md +31 -0
  70. package/src/resources/skills/mandu-security/rules/_template.md +74 -0
  71. package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -0
  72. package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -0
  73. package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -0
  74. package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -0
  75. package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -0
  76. package/src/resources/skills/mandu-slot/SKILL.md +85 -0
  77. package/src/resources/skills/mandu-slot/metadata.json +12 -0
  78. package/src/resources/skills/mandu-slot/rules/_sections.md +36 -0
  79. package/src/resources/skills/mandu-slot/rules/_template.md +63 -0
  80. package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -0
  81. package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -0
  82. package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -0
  83. package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -0
  84. package/src/resources/skills/mandu-styling/SKILL.md +118 -0
  85. package/src/resources/skills/mandu-styling/_sections.md +36 -0
  86. package/src/resources/skills/mandu-styling/_template.md +32 -0
  87. package/src/resources/skills/mandu-styling/metadata.json +13 -0
  88. package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -0
  89. package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -0
  90. package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -0
  91. package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -0
  92. package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -0
  93. package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -0
  94. package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -0
  95. package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -0
  96. package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -0
  97. package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -0
  98. package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +161 -0
  99. package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -0
  100. package/src/resources/skills/mandu-testing/SKILL.md +99 -0
  101. package/src/resources/skills/mandu-testing/metadata.json +13 -0
  102. package/src/resources/skills/mandu-testing/rules/_sections.md +26 -0
  103. package/src/resources/skills/mandu-testing/rules/_template.md +65 -0
  104. package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -0
  105. package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -0
  106. package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -0
  107. package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -0
  108. package/src/resources/skills/mandu-ui/SKILL.md +117 -0
  109. package/src/resources/skills/mandu-ui/_sections.md +23 -0
  110. package/src/resources/skills/mandu-ui/_template.md +32 -0
  111. package/src/resources/skills/mandu-ui/metadata.json +13 -0
  112. package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -0
  113. package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -0
  114. package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -0
  115. package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -0
  116. package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -0
  117. package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -0
  118. package/src/resources/skills/recipes.ts +932 -0
  119. package/src/server.ts +3 -0
  120. package/src/tools/hydration.ts +8 -8
  121. package/src/tools/index.ts +1 -0
  122. package/src/tools/seo.ts +417 -0
@@ -0,0 +1,258 @@
1
+ ---
2
+ title: UI Library Island Integration
3
+ impact: MEDIUM
4
+ impactDescription: Proper integration of UI components with Island architecture
5
+ tags: ui, island, integration, client
6
+ ---
7
+
8
+ ## UI Library Island Integration
9
+
10
+ **Impact: MEDIUM (Proper integration of UI components with Island architecture)**
11
+
12
+ UI 라이브러리 컴포넌트를 Mandu Island 아키텍처에 올바르게 통합하세요.
13
+
14
+ ## "use client" 경계
15
+
16
+ ```tsx
17
+ // components/ui/button.tsx
18
+ // shadcn/ui 컴포넌트는 이미 클라이언트 컴포넌트
19
+ "use client";
20
+
21
+ import { cn } from "@/lib/utils";
22
+ // ...
23
+ ```
24
+
25
+ ```tsx
26
+ // app/dashboard/page.tsx (서버 컴포넌트)
27
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
28
+ import { DashboardActionsIsland } from "./client";
29
+
30
+ export default function DashboardPage() {
31
+ return (
32
+ <div>
33
+ {/* Card는 서버에서 렌더링 (정적 마크업) */}
34
+ <Card>
35
+ <CardHeader>
36
+ <CardTitle>Dashboard</CardTitle>
37
+ </CardHeader>
38
+ <CardContent>
39
+ {/* 인터랙티브 부분만 Island */}
40
+ <DashboardActionsIsland />
41
+ </CardContent>
42
+ </Card>
43
+ </div>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ## 서버 데이터 → Island 전달
49
+
50
+ ```tsx
51
+ // app/users/page.tsx
52
+ import { UserTableIsland } from "./client";
53
+ import { getUsers } from "@/lib/db";
54
+
55
+ export default async function UsersPage() {
56
+ const users = await getUsers();
57
+
58
+ return (
59
+ <div>
60
+ <h1>Users</h1>
61
+ {/* 초기 데이터를 props로 전달 */}
62
+ <UserTableIsland initialUsers={users} />
63
+ </div>
64
+ );
65
+ }
66
+ ```
67
+
68
+ ```tsx
69
+ // app/users/client.tsx
70
+ "use client";
71
+
72
+ import { useState } from "react";
73
+ import {
74
+ Table,
75
+ TableBody,
76
+ TableCell,
77
+ TableHead,
78
+ TableHeader,
79
+ TableRow,
80
+ } from "@/components/ui/table";
81
+ import { Button } from "@/components/ui/button";
82
+
83
+ export function UserTableIsland({ initialUsers }) {
84
+ const [users, setUsers] = useState(initialUsers);
85
+
86
+ const handleDelete = async (id: string) => {
87
+ await fetch(`/api/users/${id}`, { method: "DELETE" });
88
+ setUsers(users.filter(u => u.id !== id));
89
+ };
90
+
91
+ return (
92
+ <Table>
93
+ <TableHeader>
94
+ <TableRow>
95
+ <TableHead>Name</TableHead>
96
+ <TableHead>Email</TableHead>
97
+ <TableHead>Actions</TableHead>
98
+ </TableRow>
99
+ </TableHeader>
100
+ <TableBody>
101
+ {users.map(user => (
102
+ <TableRow key={user.id}>
103
+ <TableCell>{user.name}</TableCell>
104
+ <TableCell>{user.email}</TableCell>
105
+ <TableCell>
106
+ <Button
107
+ variant="destructive"
108
+ size="sm"
109
+ onClick={() => handleDelete(user.id)}
110
+ >
111
+ Delete
112
+ </Button>
113
+ </TableCell>
114
+ </TableRow>
115
+ ))}
116
+ </TableBody>
117
+ </Table>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ## Dialog/Modal Island 패턴
123
+
124
+ ```tsx
125
+ // app/users/client.tsx
126
+ "use client";
127
+
128
+ import { useState } from "react";
129
+ import { Button } from "@/components/ui/button";
130
+ import {
131
+ Dialog,
132
+ DialogContent,
133
+ DialogHeader,
134
+ DialogTitle,
135
+ DialogTrigger,
136
+ } from "@/components/ui/dialog";
137
+
138
+ export function CreateUserIsland() {
139
+ const [open, setOpen] = useState(false);
140
+
141
+ const handleSubmit = async (data: FormData) => {
142
+ await fetch("/api/users", {
143
+ method: "POST",
144
+ body: data,
145
+ });
146
+ setOpen(false);
147
+ };
148
+
149
+ return (
150
+ <Dialog open={open} onOpenChange={setOpen}>
151
+ <DialogTrigger asChild>
152
+ <Button>Add User</Button>
153
+ </DialogTrigger>
154
+ <DialogContent>
155
+ <DialogHeader>
156
+ <DialogTitle>Create New User</DialogTitle>
157
+ </DialogHeader>
158
+ <form action={handleSubmit}>
159
+ {/* Form fields */}
160
+ </form>
161
+ </DialogContent>
162
+ </Dialog>
163
+ );
164
+ }
165
+ ```
166
+
167
+ ## Toast Island
168
+
169
+ ```tsx
170
+ // app/layout.tsx
171
+ import { Toaster } from "@/components/ui/toaster";
172
+
173
+ export default function RootLayout({ children }) {
174
+ return (
175
+ <html>
176
+ <body>
177
+ {children}
178
+ {/* Toast는 전역 Island */}
179
+ <Toaster />
180
+ </body>
181
+ </html>
182
+ );
183
+ }
184
+ ```
185
+
186
+ ```tsx
187
+ // app/form/client.tsx
188
+ "use client";
189
+
190
+ import { useToast } from "@/components/ui/use-toast";
191
+
192
+ export function FormIsland() {
193
+ const { toast } = useToast();
194
+
195
+ const handleSubmit = async () => {
196
+ try {
197
+ await submitForm();
198
+ toast({
199
+ title: "Success",
200
+ description: "Form submitted successfully",
201
+ });
202
+ } catch {
203
+ toast({
204
+ title: "Error",
205
+ description: "Something went wrong",
206
+ variant: "destructive",
207
+ });
208
+ }
209
+ };
210
+
211
+ // ...
212
+ }
213
+ ```
214
+
215
+ ## Island 간 UI 상태 공유
216
+
217
+ ```tsx
218
+ // app/sidebar/client.tsx
219
+ "use client";
220
+
221
+ import { useIslandEvent } from "@mandujs/core/client";
222
+
223
+ export function SidebarIsland() {
224
+ const [isOpen, setIsOpen] = useState(true);
225
+
226
+ useIslandEvent("toggle-sidebar", () => {
227
+ setIsOpen(prev => !prev);
228
+ });
229
+
230
+ return (
231
+ <aside className={cn(isOpen ? "w-64" : "w-0", "transition-all")}>
232
+ {/* Sidebar content */}
233
+ </aside>
234
+ );
235
+ }
236
+ ```
237
+
238
+ ```tsx
239
+ // app/header/client.tsx
240
+ "use client";
241
+
242
+ import { useIslandEvent } from "@mandujs/core/client";
243
+ import { Button } from "@/components/ui/button";
244
+
245
+ export function HeaderIsland() {
246
+ const { emit } = useIslandEvent("toggle-sidebar");
247
+
248
+ return (
249
+ <header>
250
+ <Button variant="ghost" size="icon" onClick={() => emit({})}>
251
+ <MenuIcon />
252
+ </Button>
253
+ </header>
254
+ );
255
+ }
256
+ ```
257
+
258
+ Reference: [React Server Components](https://react.dev/reference/rsc/server-components)
@@ -0,0 +1,213 @@
1
+ ---
2
+ title: Radix UI Patterns
3
+ impact: HIGH
4
+ impactDescription: Accessible headless primitives for custom components
5
+ tags: ui, radix, headless, primitives
6
+ ---
7
+
8
+ ## Radix UI Patterns
9
+
10
+ **Impact: HIGH (Accessible headless primitives for custom components)**
11
+
12
+ Radix UI를 직접 사용하여 커스텀 컴포넌트를 만들 때의 패턴입니다.
13
+
14
+ **설치:**
15
+
16
+ ```bash
17
+ # 개별 패키지
18
+ bun add @radix-ui/react-dialog
19
+ bun add @radix-ui/react-dropdown-menu
20
+ bun add @radix-ui/react-popover
21
+ bun add @radix-ui/react-tabs
22
+ bun add @radix-ui/react-tooltip
23
+ ```
24
+
25
+ ## Dialog 커스텀 구현
26
+
27
+ ```tsx
28
+ // components/ui/custom-dialog.tsx
29
+ "use client";
30
+
31
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
32
+ import { X } from "lucide-react";
33
+ import { cn } from "@/lib/utils";
34
+
35
+ const Dialog = DialogPrimitive.Root;
36
+ const DialogTrigger = DialogPrimitive.Trigger;
37
+ const DialogPortal = DialogPrimitive.Portal;
38
+ const DialogClose = DialogPrimitive.Close;
39
+
40
+ const DialogOverlay = ({ className, ...props }) => (
41
+ <DialogPrimitive.Overlay
42
+ className={cn(
43
+ "fixed inset-0 z-50 bg-black/80",
44
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
45
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+
52
+ const DialogContent = ({ className, children, ...props }) => (
53
+ <DialogPortal>
54
+ <DialogOverlay />
55
+ <DialogPrimitive.Content
56
+ className={cn(
57
+ "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
58
+ "rounded-lg border bg-background p-6 shadow-lg",
59
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
60
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
61
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
62
+ className
63
+ )}
64
+ {...props}
65
+ >
66
+ {children}
67
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
68
+ <X className="h-4 w-4" />
69
+ <span className="sr-only">Close</span>
70
+ </DialogPrimitive.Close>
71
+ </DialogPrimitive.Content>
72
+ </DialogPortal>
73
+ );
74
+
75
+ export { Dialog, DialogTrigger, DialogContent, DialogClose };
76
+ ```
77
+
78
+ ## Dropdown Menu 패턴
79
+
80
+ ```tsx
81
+ // components/ui/custom-dropdown.tsx
82
+ "use client";
83
+
84
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
85
+ import { cn } from "@/lib/utils";
86
+
87
+ const DropdownMenu = DropdownMenuPrimitive.Root;
88
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
89
+
90
+ const DropdownMenuContent = ({ className, sideOffset = 4, ...props }) => (
91
+ <DropdownMenuPrimitive.Portal>
92
+ <DropdownMenuPrimitive.Content
93
+ sideOffset={sideOffset}
94
+ className={cn(
95
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 shadow-md",
96
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
97
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
98
+ "data-[side=bottom]:slide-in-from-top-2",
99
+ "data-[side=top]:slide-in-from-bottom-2",
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ </DropdownMenuPrimitive.Portal>
105
+ );
106
+
107
+ const DropdownMenuItem = ({ className, ...props }) => (
108
+ <DropdownMenuPrimitive.Item
109
+ className={cn(
110
+ "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
111
+ "focus:bg-accent focus:text-accent-foreground",
112
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ );
118
+
119
+ export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem };
120
+ ```
121
+
122
+ ## Island에서 사용
123
+
124
+ ```tsx
125
+ // app/user-menu/client.tsx
126
+ "use client";
127
+
128
+ import {
129
+ DropdownMenu,
130
+ DropdownMenuTrigger,
131
+ DropdownMenuContent,
132
+ DropdownMenuItem,
133
+ } from "@/components/ui/custom-dropdown";
134
+
135
+ export function UserMenuIsland({ user }: { user: { name: string; avatar: string } }) {
136
+ return (
137
+ <DropdownMenu>
138
+ <DropdownMenuTrigger asChild>
139
+ <button className="flex items-center gap-2 rounded-full p-1 hover:bg-accent">
140
+ <img src={user.avatar} alt="" className="h-8 w-8 rounded-full" />
141
+ <span className="sr-only">User menu</span>
142
+ </button>
143
+ </DropdownMenuTrigger>
144
+
145
+ <DropdownMenuContent align="end">
146
+ <DropdownMenuItem onSelect={() => navigate("/profile")}>
147
+ Profile
148
+ </DropdownMenuItem>
149
+ <DropdownMenuItem onSelect={() => navigate("/settings")}>
150
+ Settings
151
+ </DropdownMenuItem>
152
+ <DropdownMenuItem
153
+ onSelect={() => logout()}
154
+ className="text-destructive"
155
+ >
156
+ Logout
157
+ </DropdownMenuItem>
158
+ </DropdownMenuContent>
159
+ </DropdownMenu>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ## Tooltip 패턴
165
+
166
+ ```tsx
167
+ // components/ui/tooltip.tsx
168
+ "use client";
169
+
170
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
171
+ import { cn } from "@/lib/utils";
172
+
173
+ const TooltipProvider = TooltipPrimitive.Provider;
174
+ const Tooltip = TooltipPrimitive.Root;
175
+ const TooltipTrigger = TooltipPrimitive.Trigger;
176
+
177
+ const TooltipContent = ({ className, sideOffset = 4, ...props }) => (
178
+ <TooltipPrimitive.Content
179
+ sideOffset={sideOffset}
180
+ className={cn(
181
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm shadow-md",
182
+ "animate-in fade-in-0 zoom-in-95",
183
+ className
184
+ )}
185
+ {...props}
186
+ />
187
+ );
188
+
189
+ // 사용
190
+ <TooltipProvider>
191
+ <Tooltip>
192
+ <TooltipTrigger asChild>
193
+ <button>Hover me</button>
194
+ </TooltipTrigger>
195
+ <TooltipContent>
196
+ <p>Tooltip text</p>
197
+ </TooltipContent>
198
+ </Tooltip>
199
+ </TooltipProvider>
200
+ ```
201
+
202
+ ## data-state 스타일링
203
+
204
+ ```css
205
+ /* Radix는 상태를 data-* 속성으로 노출 */
206
+ [data-state="open"] { /* 열림 상태 */ }
207
+ [data-state="closed"] { /* 닫힘 상태 */ }
208
+ [data-state="active"] { /* 활성 상태 */ }
209
+ [data-disabled] { /* 비활성화 */ }
210
+ [data-highlighted] { /* 키보드 포커스 */ }
211
+ ```
212
+
213
+ Reference: [Radix UI Documentation](https://www.radix-ui.com/primitives)
@@ -0,0 +1,209 @@
1
+ ---
2
+ title: shadcn/ui Setup
3
+ impact: HIGH
4
+ impactDescription: Production-ready UI components with full customization
5
+ tags: ui, shadcn, setup, components
6
+ ---
7
+
8
+ ## shadcn/ui Setup
9
+
10
+ **Impact: HIGH (Production-ready UI components with full customization)**
11
+
12
+ shadcn/ui를 Mandu 프로젝트에 설정하세요. 컴포넌트를 직접 소유하고 커스터마이징할 수 있습니다.
13
+
14
+ **초기화:**
15
+
16
+ ```bash
17
+ bunx shadcn-ui@latest init
18
+ ```
19
+
20
+ **선택 옵션:**
21
+ ```
22
+ ✔ TypeScript: yes
23
+ ✔ Style: Default (또는 New York)
24
+ ✔ Base color: Slate
25
+ ✔ Global CSS: app/globals.css
26
+ ✔ CSS variables: yes
27
+ ✔ tailwind.config: tailwind.config.ts
28
+ ✔ Components alias: @/components
29
+ ✔ Utils alias: @/lib/utils
30
+ ```
31
+
32
+ ## 컴포넌트 추가
33
+
34
+ ```bash
35
+ # 자주 사용하는 컴포넌트
36
+ bunx shadcn-ui@latest add button
37
+ bunx shadcn-ui@latest add input
38
+ bunx shadcn-ui@latest add card
39
+ bunx shadcn-ui@latest add dialog
40
+ bunx shadcn-ui@latest add dropdown-menu
41
+ bunx shadcn-ui@latest add form
42
+ bunx shadcn-ui@latest add toast
43
+
44
+ # 한 번에 여러 개
45
+ bunx shadcn-ui@latest add button input card dialog
46
+ ```
47
+
48
+ ## 생성된 구조
49
+
50
+ ```
51
+ components/
52
+ └── ui/
53
+ ├── button.tsx # 직접 수정 가능
54
+ ├── input.tsx
55
+ ├── card.tsx
56
+ └── ...
57
+
58
+ lib/
59
+ └── utils.ts # cn() 함수
60
+ ```
61
+
62
+ ## 컴포넌트 커스터마이징
63
+
64
+ ```tsx
65
+ // components/ui/button.tsx
66
+ import { cva, type VariantProps } from "class-variance-authority";
67
+
68
+ const buttonVariants = cva(
69
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
70
+ {
71
+ variants: {
72
+ variant: {
73
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
74
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
75
+ outline: "border border-input bg-background hover:bg-accent",
76
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
77
+ ghost: "hover:bg-accent hover:text-accent-foreground",
78
+ link: "text-primary underline-offset-4 hover:underline",
79
+ // 커스텀 variant 추가
80
+ mandu: "bg-blue-500 text-white hover:bg-blue-600",
81
+ },
82
+ size: {
83
+ default: "h-10 px-4 py-2",
84
+ sm: "h-9 rounded-md px-3",
85
+ lg: "h-11 rounded-md px-8",
86
+ icon: "h-10 w-10",
87
+ // 커스텀 size 추가
88
+ xl: "h-14 rounded-lg px-10 text-lg",
89
+ },
90
+ },
91
+ defaultVariants: {
92
+ variant: "default",
93
+ size: "default",
94
+ },
95
+ }
96
+ );
97
+
98
+ // ... 컴포넌트 코드
99
+ ```
100
+
101
+ ## Island에서 사용
102
+
103
+ ```tsx
104
+ // app/actions/client.tsx
105
+ "use client";
106
+
107
+ import { Button } from "@/components/ui/button";
108
+ import { useState } from "react";
109
+
110
+ export function ActionButtonsIsland() {
111
+ const [loading, setLoading] = useState(false);
112
+
113
+ const handleAction = async () => {
114
+ setLoading(true);
115
+ try {
116
+ await doSomething();
117
+ } finally {
118
+ setLoading(false);
119
+ }
120
+ };
121
+
122
+ return (
123
+ <div className="flex gap-2">
124
+ <Button onClick={handleAction} disabled={loading}>
125
+ {loading ? "Processing..." : "Submit"}
126
+ </Button>
127
+ <Button variant="outline">Cancel</Button>
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ## Form 통합
134
+
135
+ ```bash
136
+ bunx shadcn-ui@latest add form
137
+ bun add react-hook-form @hookform/resolvers zod
138
+ ```
139
+
140
+ ```tsx
141
+ // app/contact/client.tsx
142
+ "use client";
143
+
144
+ import { useForm } from "react-hook-form";
145
+ import { zodResolver } from "@hookform/resolvers/zod";
146
+ import { z } from "zod";
147
+ import { Button } from "@/components/ui/button";
148
+ import {
149
+ Form,
150
+ FormControl,
151
+ FormField,
152
+ FormItem,
153
+ FormLabel,
154
+ FormMessage,
155
+ } from "@/components/ui/form";
156
+ import { Input } from "@/components/ui/input";
157
+
158
+ const formSchema = z.object({
159
+ email: z.string().email("Invalid email"),
160
+ message: z.string().min(10, "Minimum 10 characters"),
161
+ });
162
+
163
+ export function ContactFormIsland() {
164
+ const form = useForm<z.infer<typeof formSchema>>({
165
+ resolver: zodResolver(formSchema),
166
+ defaultValues: { email: "", message: "" },
167
+ });
168
+
169
+ const onSubmit = async (values: z.infer<typeof formSchema>) => {
170
+ await fetch("/api/contact", {
171
+ method: "POST",
172
+ body: JSON.stringify(values),
173
+ });
174
+ };
175
+
176
+ return (
177
+ <Form {...form}>
178
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
179
+ <FormField
180
+ control={form.control}
181
+ name="email"
182
+ render={({ field }) => (
183
+ <FormItem>
184
+ <FormLabel>Email</FormLabel>
185
+ <FormControl>
186
+ <Input placeholder="you@example.com" {...field} />
187
+ </FormControl>
188
+ <FormMessage />
189
+ </FormItem>
190
+ )}
191
+ />
192
+ <Button type="submit">Send</Button>
193
+ </form>
194
+ </Form>
195
+ );
196
+ }
197
+ ```
198
+
199
+ ## 업데이트
200
+
201
+ ```bash
202
+ # 컴포넌트 업데이트 (덮어쓰기 주의!)
203
+ bunx shadcn-ui@latest add button --overwrite
204
+
205
+ # diff 확인 후 수동 병합 권장
206
+ bunx shadcn-ui@latest diff button
207
+ ```
208
+
209
+ Reference: [shadcn/ui Documentation](https://ui.shadcn.com/docs)