@devmunna/agent-skillkit 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Forms Reference — React Hook Form v7 + Zod v4
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Schema File — {feature}.schema.ts
|
|
6
|
+
|
|
7
|
+
Define Zod schemas once. Derive TypeScript types with `z.infer<>`. Share types between forms, API calls, and hooks.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// features/users/users.schema.ts
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
export const createUserSchema = z.object({
|
|
14
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
15
|
+
email: z.string().email('Invalid email address'),
|
|
16
|
+
role: z.enum(['USER', 'ADMIN', 'MANAGER']).default('USER'),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const updateUserSchema = createUserSchema.partial();
|
|
20
|
+
|
|
21
|
+
// Types derived from schema — single source of truth
|
|
22
|
+
export type CreateUserDto = z.infer<typeof createUserSchema>;
|
|
23
|
+
export type UpdateUserDto = z.infer<typeof updateUserSchema>;
|
|
24
|
+
export type User = { id: string; createdAt: string } & CreateUserDto;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Create Form
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// features/users/components/CreateUserForm.tsx
|
|
33
|
+
import { useForm } from 'react-hook-form';
|
|
34
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
35
|
+
import { createUserSchema, type CreateUserDto } from '../users.schema';
|
|
36
|
+
import { useCreateUser } from '../users.hooks';
|
|
37
|
+
|
|
38
|
+
interface Props {
|
|
39
|
+
onSuccess?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function CreateUserForm({ onSuccess }: Props) {
|
|
43
|
+
const createUser = useCreateUser();
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
register,
|
|
47
|
+
handleSubmit,
|
|
48
|
+
reset,
|
|
49
|
+
formState: { errors, isSubmitting },
|
|
50
|
+
} = useForm<CreateUserDto>({
|
|
51
|
+
resolver: zodResolver(createUserSchema),
|
|
52
|
+
defaultValues: { role: 'USER' },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const onSubmit = async (data: CreateUserDto) => {
|
|
56
|
+
await createUser.mutateAsync(data);
|
|
57
|
+
reset();
|
|
58
|
+
onSuccess?.();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
63
|
+
<div>
|
|
64
|
+
<label>Name</label>
|
|
65
|
+
<input {...register('name')} placeholder="Full name" />
|
|
66
|
+
{errors.name && <span className="error">{errors.name.message}</span>}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div>
|
|
70
|
+
<label>Email</label>
|
|
71
|
+
<input {...register('email')} type="email" placeholder="email@example.com" />
|
|
72
|
+
{errors.email && <span className="error">{errors.email.message}</span>}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div>
|
|
76
|
+
<label>Role</label>
|
|
77
|
+
<select {...register('role')}>
|
|
78
|
+
<option value="USER">User</option>
|
|
79
|
+
<option value="ADMIN">Admin</option>
|
|
80
|
+
<option value="MANAGER">Manager</option>
|
|
81
|
+
</select>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{createUser.error && (
|
|
85
|
+
<p className="error">{(createUser.error as any).message}</p>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<button type="submit" disabled={isSubmitting || createUser.isPending}>
|
|
89
|
+
{createUser.isPending ? 'Creating...' : 'Create User'}
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Edit Form (pre-populated)
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// features/users/components/EditUserForm.tsx
|
|
102
|
+
import { useForm } from 'react-hook-form';
|
|
103
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
104
|
+
import { updateUserSchema, type UpdateUserDto, type User } from '../users.schema';
|
|
105
|
+
import { useUpdateUser } from '../users.hooks';
|
|
106
|
+
|
|
107
|
+
export function EditUserForm({ user, onSuccess }: { user: User; onSuccess?: () => void }) {
|
|
108
|
+
const updateUser = useUpdateUser();
|
|
109
|
+
|
|
110
|
+
const {
|
|
111
|
+
register,
|
|
112
|
+
handleSubmit,
|
|
113
|
+
formState: { errors, isDirty, isSubmitting },
|
|
114
|
+
} = useForm<UpdateUserDto>({
|
|
115
|
+
resolver: zodResolver(updateUserSchema),
|
|
116
|
+
defaultValues: { name: user.name, email: user.email, role: user.role },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const onSubmit = async (data: UpdateUserDto) => {
|
|
120
|
+
await updateUser.mutateAsync({ id: user.id, data });
|
|
121
|
+
onSuccess?.();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
126
|
+
<input {...register('name')} />
|
|
127
|
+
{errors.name && <span>{errors.name.message}</span>}
|
|
128
|
+
|
|
129
|
+
<input {...register('email')} type="email" />
|
|
130
|
+
{errors.email && <span>{errors.email.message}</span>}
|
|
131
|
+
|
|
132
|
+
<button type="submit" disabled={!isDirty || isSubmitting || updateUser.isPending}>
|
|
133
|
+
{updateUser.isPending ? 'Saving...' : 'Save Changes'}
|
|
134
|
+
</button>
|
|
135
|
+
</form>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Cross-Field Validation
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const passwordSchema = z.object({
|
|
146
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
147
|
+
confirmPassword: z.string(),
|
|
148
|
+
}).refine((d) => d.password === d.confirmPassword, {
|
|
149
|
+
message: 'Passwords do not match',
|
|
150
|
+
path: ['confirmPassword'],
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## React 19 useActionState Form (no RHF — simple cases only)
|
|
157
|
+
|
|
158
|
+
Use RHF for complex forms. Use `useActionState` only for simple forms where progressive enhancement is needed.
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { useActionState } from 'react';
|
|
162
|
+
import { createUserSchema } from '../users.schema';
|
|
163
|
+
import { usersApi } from '../users.api';
|
|
164
|
+
|
|
165
|
+
async function submitAction(_prev: any, formData: FormData) {
|
|
166
|
+
const result = createUserSchema.safeParse({
|
|
167
|
+
name: formData.get('name'),
|
|
168
|
+
email: formData.get('email'),
|
|
169
|
+
});
|
|
170
|
+
if (!result.success) return { error: result.error.issues[0].message };
|
|
171
|
+
try {
|
|
172
|
+
await usersApi.create(result.data);
|
|
173
|
+
return { success: true };
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
return { error: err.message ?? 'Failed to create user' };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function SimpleCreateForm() {
|
|
180
|
+
const [state, formAction, isPending] = useActionState(submitAction, null);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<form action={formAction}>
|
|
184
|
+
<input name="name" required />
|
|
185
|
+
<input name="email" type="email" required />
|
|
186
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
187
|
+
{state?.success && <p className="success">User created!</p>}
|
|
188
|
+
<button type="submit" disabled={isPending}>
|
|
189
|
+
{isPending ? 'Saving...' : 'Create'}
|
|
190
|
+
</button>
|
|
191
|
+
</form>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Routing Reference — React Router v7
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## src/app/router.tsx
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { createBrowserRouter, Navigate } from 'react-router';
|
|
9
|
+
import { lazy, Suspense } from 'react';
|
|
10
|
+
import { RootLayout } from '@/components/layout/RootLayout';
|
|
11
|
+
import { AuthLayout } from '@/components/layout/AuthLayout';
|
|
12
|
+
import { DashLayout } from '@/components/layout/DashLayout';
|
|
13
|
+
import { ProtectedRoute } from '@/components/layout/ProtectedRoute';
|
|
14
|
+
import { PageSpinner } from '@/components/ui/PageSpinner';
|
|
15
|
+
|
|
16
|
+
// Lazy-loaded route components — code-split per route
|
|
17
|
+
const LoginPage = lazy(() => import('@/features/auth/auth.page').then(m => ({ default: m.LoginPage })));
|
|
18
|
+
const RegisterPage = lazy(() => import('@/features/auth/register.page').then(m => ({ default: m.RegisterPage })));
|
|
19
|
+
const DashboardPage = lazy(() => import('@/features/dashboard/dashboard.page').then(m => ({ default: m.DashboardPage })));
|
|
20
|
+
const UsersPage = lazy(() => import('@/features/users/users.page').then(m => ({ default: m.UsersPage })));
|
|
21
|
+
const UserDetailPage = lazy(() => import('@/features/users/user-detail.page').then(m => ({ default: m.UserDetailPage })));
|
|
22
|
+
|
|
23
|
+
const withSuspense = (Component: React.ComponentType) => (
|
|
24
|
+
<Suspense fallback={<PageSpinner />}>
|
|
25
|
+
<Component />
|
|
26
|
+
</Suspense>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const router = createBrowserRouter([
|
|
30
|
+
{
|
|
31
|
+
path: '/',
|
|
32
|
+
element: <RootLayout />,
|
|
33
|
+
children: [
|
|
34
|
+
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
|
35
|
+
|
|
36
|
+
// Public routes
|
|
37
|
+
{
|
|
38
|
+
path: 'auth',
|
|
39
|
+
element: <AuthLayout />,
|
|
40
|
+
children: [
|
|
41
|
+
{ path: 'login', element: withSuspense(LoginPage) },
|
|
42
|
+
{ path: 'register', element: withSuspense(RegisterPage) },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Protected routes
|
|
47
|
+
{
|
|
48
|
+
element: <ProtectedRoute />,
|
|
49
|
+
children: [
|
|
50
|
+
{
|
|
51
|
+
element: <DashLayout />,
|
|
52
|
+
children: [
|
|
53
|
+
{ path: 'dashboard', element: withSuspense(DashboardPage) },
|
|
54
|
+
{ path: 'users', element: withSuspense(UsersPage) },
|
|
55
|
+
{ path: 'users/:id', element: withSuspense(UserDetailPage) },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Role-restricted routes
|
|
62
|
+
{
|
|
63
|
+
element: <ProtectedRoute allowedRoles={['ADMIN']} />,
|
|
64
|
+
children: [
|
|
65
|
+
{ path: 'admin', element: withSuspense(AdminPage) },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## src/app/App.tsx
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { RouterProvider } from 'react-router';
|
|
79
|
+
import { router } from './router';
|
|
80
|
+
import { Providers } from './providers';
|
|
81
|
+
|
|
82
|
+
export default function App() {
|
|
83
|
+
return (
|
|
84
|
+
<Providers>
|
|
85
|
+
<RouterProvider router={router} />
|
|
86
|
+
</Providers>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ProtectedRoute Component
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// components/layout/ProtectedRoute.tsx
|
|
97
|
+
import { Navigate, Outlet } from 'react-router';
|
|
98
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
99
|
+
import { useAuthStore } from '@/store/auth.store';
|
|
100
|
+
|
|
101
|
+
interface Props {
|
|
102
|
+
allowedRoles?: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ProtectedRoute({ allowedRoles }: Props) {
|
|
106
|
+
const { isAuthenticated, user } = useAuthStore(
|
|
107
|
+
useShallow((s) => ({ isAuthenticated: s.isAuthenticated, user: s.user })),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!isAuthenticated) return <Navigate to="/auth/login" replace />;
|
|
111
|
+
|
|
112
|
+
if (allowedRoles && user && !allowedRoles.includes(user.role)) {
|
|
113
|
+
return <Navigate to="/unauthorized" replace />;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return <Outlet />;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Navigation Hooks
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { useNavigate, useParams, useSearchParams, Link, NavLink } from 'react-router';
|
|
126
|
+
|
|
127
|
+
// Navigate programmatically
|
|
128
|
+
const navigate = useNavigate();
|
|
129
|
+
navigate('/users');
|
|
130
|
+
navigate(`/users/${id}`);
|
|
131
|
+
navigate(-1); // go back
|
|
132
|
+
|
|
133
|
+
// Route params
|
|
134
|
+
const { id } = useParams<{ id: string }>();
|
|
135
|
+
|
|
136
|
+
// Search params — for filters, pagination, tabs
|
|
137
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
138
|
+
const page = Number(searchParams.get('page') ?? 1);
|
|
139
|
+
const tab = searchParams.get('tab') ?? 'all';
|
|
140
|
+
|
|
141
|
+
setSearchParams({ page: String(page + 1), tab });
|
|
142
|
+
|
|
143
|
+
// Link vs NavLink
|
|
144
|
+
<Link to={`/users/${user.id}`}>{user.name}</Link>
|
|
145
|
+
<NavLink to="/dashboard" className={({ isActive }) => isActive ? 'active' : ''}>
|
|
146
|
+
Dashboard
|
|
147
|
+
</NavLink>
|
|
148
|
+
```
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# State Management Reference — Zustand v5
|
|
2
|
+
|
|
3
|
+
Zustand manages **global UI state only** (auth session, theme, sidebar).
|
|
4
|
+
Server data lives in TanStack Query — never duplicate API responses in Zustand.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Auth Store
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// src/store/auth.store.ts
|
|
12
|
+
import { create } from 'zustand';
|
|
13
|
+
import { persist } from 'zustand/middleware';
|
|
14
|
+
|
|
15
|
+
interface User {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
role: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AuthState {
|
|
23
|
+
user: User | null;
|
|
24
|
+
token: string | null;
|
|
25
|
+
isAuthenticated: boolean;
|
|
26
|
+
login: (user: User, token: string) => void;
|
|
27
|
+
logout: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const useAuthStore = create<AuthState>()(
|
|
31
|
+
persist(
|
|
32
|
+
(set) => ({
|
|
33
|
+
user: null,
|
|
34
|
+
token: null,
|
|
35
|
+
isAuthenticated: false,
|
|
36
|
+
login: (user, token) => set({ user, token, isAuthenticated: true }),
|
|
37
|
+
logout: () => set({ user: null, token: null, isAuthenticated: false }),
|
|
38
|
+
}),
|
|
39
|
+
{ name: 'auth-storage' }, // persisted to localStorage
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## UI Store
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// src/store/ui.store.ts
|
|
50
|
+
import { create } from 'zustand';
|
|
51
|
+
|
|
52
|
+
interface UIState {
|
|
53
|
+
sidebarOpen: boolean;
|
|
54
|
+
theme: 'light' | 'dark';
|
|
55
|
+
toggleSidebar: () => void;
|
|
56
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const useUIStore = create<UIState>()((set) => ({
|
|
60
|
+
sidebarOpen: true,
|
|
61
|
+
theme: 'light',
|
|
62
|
+
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
|
63
|
+
setTheme: (theme) => set({ theme }),
|
|
64
|
+
}));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Access State Outside React (e.g., axios interceptor)
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// Zustand v5 — getState() for accessing store outside a React component
|
|
73
|
+
import { useAuthStore } from '@/store/auth.store';
|
|
74
|
+
|
|
75
|
+
const token = useAuthStore.getState().token;
|
|
76
|
+
const logout = useAuthStore.getState().logout;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Shallow Comparison (prevent extra re-renders)
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
85
|
+
|
|
86
|
+
// Re-renders only when user OR isAuthenticated changes
|
|
87
|
+
const { user, isAuthenticated } = useAuthStore(
|
|
88
|
+
useShallow((s) => ({ user: s.user, isAuthenticated: s.isAuthenticated })),
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Usage in Components
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// Selecting a single value — no shallow needed
|
|
98
|
+
const token = useAuthStore((s) => s.token);
|
|
99
|
+
|
|
100
|
+
// Selecting an action — always stable, no re-render risk
|
|
101
|
+
const logout = useAuthStore((s) => s.logout);
|
|
102
|
+
|
|
103
|
+
// Selecting multiple values — use useShallow
|
|
104
|
+
const { user, isAuthenticated } = useAuthStore(
|
|
105
|
+
useShallow((s) => ({ user: s.user, isAuthenticated: s.isAuthenticated })),
|
|
106
|
+
);
|
|
107
|
+
```
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tailwind-css
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Use this skill for any Tailwind CSS task: project setup, design tokens, component styling, responsive design, dark mode, animations, or utility patterns. Triggers: "add Tailwind", "style with Tailwind", "dark mode", "responsive layout", "component variants", "design system", "tailwind v4", "cn helper", "CVA", "container queries", "shadcn".
|
|
6
|
+
stack: [tailwind, css]
|
|
7
|
+
depends: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Tailwind CSS v4 — Production Skill
|
|
11
|
+
|
|
12
|
+
**Version target:** Tailwind CSS v4 · @tailwindcss/vite or @tailwindcss/postcss · TypeScript 5
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Tech Stack
|
|
17
|
+
|
|
18
|
+
| Tool | Package |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Core | tailwindcss v4 |
|
|
21
|
+
| Vite integration | @tailwindcss/vite |
|
|
22
|
+
| Next.js integration | @tailwindcss/postcss |
|
|
23
|
+
| Class merging | clsx + tailwind-merge |
|
|
24
|
+
| Component variants | class-variance-authority (CVA) |
|
|
25
|
+
| Component library | shadcn/ui (optional) |
|
|
26
|
+
| Typography | @tailwindcss/typography |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## v4 vs v3 — Critical Differences
|
|
31
|
+
|
|
32
|
+
| Feature | v3 | v4 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Import | `@tailwind base/components/utilities` | `@import "tailwindcss"` (single line) |
|
|
35
|
+
| Config file | `tailwind.config.js` | CSS `@theme` directive (no JS config needed) |
|
|
36
|
+
| Custom tokens | JS `theme.extend` | `@theme` block in CSS |
|
|
37
|
+
| Custom utilities | Plugin system | `@utility` directive |
|
|
38
|
+
| Custom variants | Plugin `addVariant` | `@custom-variant` directive |
|
|
39
|
+
| Dark mode config | `darkMode: 'class'` in JS | `@custom-variant dark` in CSS |
|
|
40
|
+
| Container queries | External plugin | Built-in (`@container`, `@sm:`, `@md:`) |
|
|
41
|
+
| Gradients | `bg-gradient-to-r` | `bg-linear-to-r` |
|
|
42
|
+
| Content detection | `content: [...]` in config | Automatic — no config needed |
|
|
43
|
+
| Colors | HEX / HSL | HEX / HSL / **OKLCH** (preferred) |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Setup
|
|
48
|
+
|
|
49
|
+
### Vite + React
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install tailwindcss @tailwindcss/vite
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**vite.config.ts:**
|
|
56
|
+
```ts
|
|
57
|
+
import { defineConfig } from 'vite';
|
|
58
|
+
import react from '@vitejs/plugin-react';
|
|
59
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
plugins: [react(), tailwindcss()],
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**src/index.css:**
|
|
67
|
+
```css
|
|
68
|
+
@import "tailwindcss";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Next.js 15
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install tailwindcss @tailwindcss/postcss postcss
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**postcss.config.mjs:**
|
|
78
|
+
```js
|
|
79
|
+
export default { plugins: { '@tailwindcss/postcss': {} } };
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**src/app/globals.css:**
|
|
83
|
+
```css
|
|
84
|
+
@import "tailwindcss";
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
See `references/setup.md` for full setup including path aliases, typography plugin, and shadcn/ui init.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Design Tokens — @theme
|
|
92
|
+
|
|
93
|
+
All design tokens live in `@theme` in your global CSS file. Tailwind auto-generates utility classes from them.
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
@import "tailwindcss";
|
|
97
|
+
|
|
98
|
+
@theme {
|
|
99
|
+
/* Colors — OKLCH preferred for perceptual uniformity */
|
|
100
|
+
--color-primary: oklch(0.55 0.25 262);
|
|
101
|
+
--color-secondary: oklch(0.84 0.16 84);
|
|
102
|
+
--color-danger: oklch(0.55 0.25 27);
|
|
103
|
+
|
|
104
|
+
/* Fonts */
|
|
105
|
+
--font-sans: 'Inter', sans-serif;
|
|
106
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
107
|
+
|
|
108
|
+
/* Border radius */
|
|
109
|
+
--radius-card: 0.75rem;
|
|
110
|
+
--radius-pill: 9999px;
|
|
111
|
+
|
|
112
|
+
/* Custom spacing */
|
|
113
|
+
--spacing-page: 2rem;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Generated utilities: `bg-primary`, `text-secondary`, `border-danger`, `font-sans`, `rounded-card`, `p-page`, etc.
|
|
118
|
+
|
|
119
|
+
See `references/design-tokens.md` for the full token reference.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Component Patterns
|
|
124
|
+
|
|
125
|
+
### cn() Helper — Required for all components
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// src/lib/utils.ts
|
|
129
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
130
|
+
import { twMerge } from 'tailwind-merge';
|
|
131
|
+
|
|
132
|
+
export function cn(...inputs: ClassValue[]) {
|
|
133
|
+
return twMerge(clsx(inputs));
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Allows callers to override classes without CSS conflicts:
|
|
138
|
+
```tsx
|
|
139
|
+
<Button className="w-full" /> // overrides default width cleanly
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### CVA — Component Variants
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
146
|
+
|
|
147
|
+
const button = cva(
|
|
148
|
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
|
|
149
|
+
{
|
|
150
|
+
variants: {
|
|
151
|
+
variant: {
|
|
152
|
+
default: 'bg-primary text-white hover:bg-primary/90',
|
|
153
|
+
outline: 'border border-current bg-transparent hover:bg-primary/10',
|
|
154
|
+
ghost: 'hover:bg-primary/10 hover:text-primary',
|
|
155
|
+
destructive: 'bg-danger text-white hover:bg-danger/90',
|
|
156
|
+
},
|
|
157
|
+
size: {
|
|
158
|
+
sm: 'h-8 px-3 text-sm',
|
|
159
|
+
md: 'h-10 px-4',
|
|
160
|
+
lg: 'h-12 px-6 text-lg',
|
|
161
|
+
icon: 'h-10 w-10',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
defaultVariants: { variant: 'default', size: 'md' },
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
interface ButtonProps
|
|
169
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
170
|
+
VariantProps<typeof button> {}
|
|
171
|
+
|
|
172
|
+
export function Button({ className, variant, size, ...props }: ButtonProps) {
|
|
173
|
+
return (
|
|
174
|
+
<button className={cn(button({ variant, size }), className)} {...props} />
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
See `references/components.md` for Card, Input, Badge, and Modal patterns.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Responsive Design
|
|
184
|
+
|
|
185
|
+
**Mobile-first — always start with base (mobile), add larger breakpoints:**
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<!-- Mobile: 1 col → md: 2 col → lg: 3 col -->
|
|
189
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
190
|
+
|
|
191
|
+
<!-- Mobile: full width → sm: auto -->
|
|
192
|
+
<button class="w-full sm:w-auto">Submit</button>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Container queries (v4 built-in) — adapt to parent size, not viewport:**
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<div class="@container">
|
|
199
|
+
<div class="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3">
|
|
200
|
+
<!-- Layout based on container width, not screen width -->
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Dark Mode
|
|
208
|
+
|
|
209
|
+
```css
|
|
210
|
+
/* Class-based dark mode in globals.css */
|
|
211
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Toggle on <html> element
|
|
216
|
+
document.documentElement.classList.toggle('dark');
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<div class="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
|
|
221
|
+
Content
|
|
222
|
+
</div>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
See `references/patterns.md` for full dark mode strategy, animations, and layout patterns.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Core References
|
|
230
|
+
|
|
231
|
+
| Topic | File |
|
|
232
|
+
|---|---|
|
|
233
|
+
| Setup — Vite, Next.js, shadcn/ui | `references/setup.md` |
|
|
234
|
+
| Design tokens — @theme, colors, fonts, spacing | `references/design-tokens.md` |
|
|
235
|
+
| Components — cn(), CVA, Button, Card, Input | `references/components.md` |
|
|
236
|
+
| Patterns — responsive, dark mode, animations, typography | `references/patterns.md` |
|