@folpe/loom 0.2.0 → 0.3.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 +82 -16
- package/data/agents/backend/AGENT.md +12 -0
- package/data/agents/database/AGENT.md +3 -0
- package/data/agents/frontend/AGENT.md +10 -0
- package/data/agents/marketing/AGENT.md +3 -0
- package/data/agents/orchestrator/AGENT.md +1 -19
- package/data/agents/security/AGENT.md +3 -0
- package/data/agents/tests/AGENT.md +2 -0
- package/data/agents/ux-ui/AGENT.md +5 -0
- package/data/presets/api-backend.yaml +0 -3
- package/data/presets/chrome-extension.yaml +0 -3
- package/data/presets/cli-tool.yaml +0 -3
- package/data/presets/e-commerce.yaml +0 -3
- package/data/presets/expo-mobile.yaml +0 -3
- package/data/presets/fullstack-auth.yaml +0 -3
- package/data/presets/landing-page.yaml +0 -3
- package/data/presets/mvp-lean.yaml +0 -3
- package/data/presets/saas-default.yaml +3 -4
- package/data/presets/saas-full.yaml +71 -0
- package/data/skills/api-design/SKILL.md +43 -2
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +13 -6
- package/data/skills/cli-development/SKILL.md +11 -3
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +10 -8
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +10 -2
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +46 -12
- package/data/skills/stripe-integration/SKILL.md +11 -3
- package/data/skills/supabase-patterns/SKILL.md +23 -6
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +10 -5
- package/dist/index.js +254 -100
- package/package.json +2 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: form-validation
|
|
3
|
+
description: "Zod dual validation patterns for client and server with react-hook-form and ShadCN Form. Use when building forms, implementing validation, or creating input schemas with i18n error messages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Form Validation Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **One Zod schema, two validations** — share between client and server.
|
|
11
|
+
- **Schema files in `src/schemas/`** — never inline schemas in components.
|
|
12
|
+
- **Use ShadCN Form components** — `FormField`, `FormMessage` for consistent UX.
|
|
13
|
+
- **Always show inline errors** — next to the field, not in toasts.
|
|
14
|
+
- **Disable submit while pending** — prevent double submissions.
|
|
15
|
+
- **Default values required** — every field needs a `defaultValues` entry in useForm.
|
|
16
|
+
|
|
17
|
+
## Dual Validation Strategy
|
|
18
|
+
|
|
19
|
+
Every form must validate on **both client and server**:
|
|
20
|
+
- **Client**: instant feedback, UX quality.
|
|
21
|
+
- **Server**: security, data integrity — never trust client validation alone.
|
|
22
|
+
|
|
23
|
+
Use a single Zod schema shared between client and server.
|
|
24
|
+
|
|
25
|
+
## Shared Schema Definition
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// src/schemas/user.schema.ts
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
|
|
31
|
+
export const createUserSchema = z.object({
|
|
32
|
+
name: z.string().min(2, "Name must be at least 2 characters").max(100),
|
|
33
|
+
email: z.string().email("Invalid email address"),
|
|
34
|
+
role: z.enum(["USER", "ADMIN"]).default("USER"),
|
|
35
|
+
bio: z.string().max(500).optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Client-Side Validation with react-hook-form
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
"use client";
|
|
45
|
+
import { useForm } from "react-hook-form";
|
|
46
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
47
|
+
import { createUserSchema, type CreateUserInput } from "@/schemas/user.schema";
|
|
48
|
+
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
|
|
49
|
+
import { Input } from "@/components/ui/input";
|
|
50
|
+
import { Button } from "@/components/ui/button";
|
|
51
|
+
|
|
52
|
+
export function CreateUserForm() {
|
|
53
|
+
const form = useForm<CreateUserInput>({
|
|
54
|
+
resolver: zodResolver(createUserSchema),
|
|
55
|
+
defaultValues: { name: "", email: "", role: "USER" },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Form {...form}>
|
|
60
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
61
|
+
<FormField
|
|
62
|
+
control={form.control}
|
|
63
|
+
name="name"
|
|
64
|
+
render={({ field }) => (
|
|
65
|
+
<FormItem>
|
|
66
|
+
<FormLabel>Name</FormLabel>
|
|
67
|
+
<FormControl><Input {...field} /></FormControl>
|
|
68
|
+
<FormMessage />
|
|
69
|
+
</FormItem>
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
{/* More fields... */}
|
|
73
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
74
|
+
Create
|
|
75
|
+
</Button>
|
|
76
|
+
</form>
|
|
77
|
+
</Form>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Server-Side Validation
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// src/actions/user.actions.ts
|
|
86
|
+
"use server";
|
|
87
|
+
import { createUserSchema } from "@/schemas/user.schema";
|
|
88
|
+
|
|
89
|
+
export async function createUser(input: unknown) {
|
|
90
|
+
const parsed = createUserSchema.safeParse(input);
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
return { success: false, error: parsed.error.errors[0].message };
|
|
93
|
+
}
|
|
94
|
+
// proceed with validated data...
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## I18n Error Messages with Zod
|
|
99
|
+
|
|
100
|
+
For internationalized error messages, use a Zod error map:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// src/lib/zod-i18n.ts
|
|
104
|
+
import { z } from "zod";
|
|
105
|
+
import { getTranslations } from "next-intl/server";
|
|
106
|
+
|
|
107
|
+
export async function createI18nSchema() {
|
|
108
|
+
const t = await getTranslations("validation");
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
createUser: z.object({
|
|
112
|
+
name: z.string().min(2, t("name.min", { min: 2 })),
|
|
113
|
+
email: z.string().email(t("email.invalid")),
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or use a custom error map:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// src/lib/zod-error-map.ts
|
|
123
|
+
import { type ZodErrorMap, ZodIssueCode } from "zod";
|
|
124
|
+
|
|
125
|
+
export function createZodErrorMap(t: (key: string, params?: Record<string, unknown>) => string): ZodErrorMap {
|
|
126
|
+
return (issue, ctx) => {
|
|
127
|
+
switch (issue.code) {
|
|
128
|
+
case ZodIssueCode.too_small:
|
|
129
|
+
return { message: t("too_small", { minimum: issue.minimum }) };
|
|
130
|
+
case ZodIssueCode.too_big:
|
|
131
|
+
return { message: t("too_big", { maximum: issue.maximum }) };
|
|
132
|
+
case ZodIssueCode.invalid_string:
|
|
133
|
+
if (issue.validation === "email") return { message: t("invalid_email") };
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
return { message: ctx.defaultError };
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Multi-Card Form Layout
|
|
142
|
+
|
|
143
|
+
For complex forms, split into logical sections using Cards:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
147
|
+
<Card>
|
|
148
|
+
<CardHeader>
|
|
149
|
+
<CardTitle>Personal Information</CardTitle>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent className="grid gap-4 md:grid-cols-2">
|
|
152
|
+
{/* Name, Email fields */}
|
|
153
|
+
</CardContent>
|
|
154
|
+
</Card>
|
|
155
|
+
|
|
156
|
+
<Card>
|
|
157
|
+
<CardHeader>
|
|
158
|
+
<CardTitle>Preferences</CardTitle>
|
|
159
|
+
</CardHeader>
|
|
160
|
+
<CardContent className="space-y-4">
|
|
161
|
+
{/* Role, Bio fields */}
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
|
|
165
|
+
<div className="flex justify-end">
|
|
166
|
+
<Button type="submit">Create User</Button>
|
|
167
|
+
</div>
|
|
168
|
+
</form>
|
|
169
|
+
```
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hero-copywriting
|
|
3
|
-
description: "Guidelines for writing high-converting hero sections. Use when creating landing pages, marketing
|
|
4
|
-
allowed-tools: "Read, Write, Edit"
|
|
3
|
+
description: "Guidelines for writing high-converting hero sections with headlines, CTAs, and social proof. Use when creating landing pages, writing marketing copy, designing hero sections, or building any page that needs a compelling above-the-fold area."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# Hero Section Copywriting
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Lead with the outcome, not the feature** — what does the user GET?
|
|
11
|
+
- **Headlines must be 8-12 words maximum** — short, punchy, outcome-focused.
|
|
12
|
+
- **CTA must use action verbs** — "Start", "Get", "Try", "Launch", "Build".
|
|
13
|
+
- **Always include social proof** — logos, metrics, ratings, or testimonials.
|
|
14
|
+
- **Be specific over generic** — "Save 5 hours a week" beats "Save time".
|
|
15
|
+
- **Speak to the reader directly** — use "you" and "your".
|
|
16
|
+
|
|
9
17
|
## Headline Rules
|
|
10
18
|
|
|
11
|
-
-
|
|
19
|
+
- Lead with the outcome, not the feature. What does the user GET?
|
|
12
20
|
- Bad: "AI-powered project management"
|
|
13
21
|
- Good: "Ship projects 3x faster with your AI co-pilot"
|
|
14
22
|
- Keep headlines to **8-12 words maximum**.
|
|
@@ -41,7 +49,7 @@ allowed-tools: "Read, Write, Edit"
|
|
|
41
49
|
|
|
42
50
|
## Visual Guidelines
|
|
43
51
|
|
|
44
|
-
- Hero should occupy the full viewport height on desktop (`min-h-
|
|
52
|
+
- Hero should occupy the full viewport height on desktop (`min-h-dvh` or `min-h-[80vh]`).
|
|
45
53
|
- Text should be left-aligned or center-aligned. Never justify.
|
|
46
54
|
- Maximum content width: `max-w-2xl` for centered, `max-w-xl` for left-aligned.
|
|
47
55
|
- Generous whitespace. Don't crowd the hero.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-patterns
|
|
3
|
+
description: "Internationalization with next-intl for RSC, Server Actions, and Zod messages. Use when adding multi-language support, translating user-facing strings, or implementing locale-aware validation."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# I18n Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **All user-facing strings must be translated** — no hardcoded text in components.
|
|
11
|
+
- **Use namespaces** — group translations by feature/page.
|
|
12
|
+
- **Server-side `getTranslations`** — use in RSC and Server Actions.
|
|
13
|
+
- **Client-side `useTranslations`** — use in Client Components only.
|
|
14
|
+
- **Validation messages are translated** — use i18n error map with Zod.
|
|
15
|
+
- **ICU message syntax** for plurals and interpolation: `"{count, plural, one {# item} other {# items}}"`.
|
|
16
|
+
|
|
17
|
+
## Setup with next-intl
|
|
18
|
+
|
|
19
|
+
### Configuration
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/i18n/config.ts
|
|
23
|
+
export const locales = ["en", "fr", "de"] as const;
|
|
24
|
+
export type Locale = (typeof locales)[number];
|
|
25
|
+
export const defaultLocale: Locale = "en";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Message Files
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
messages/
|
|
32
|
+
en.json
|
|
33
|
+
fr.json
|
|
34
|
+
de.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Structure messages by namespace:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"common": {
|
|
42
|
+
"save": "Save",
|
|
43
|
+
"cancel": "Cancel",
|
|
44
|
+
"delete": "Delete",
|
|
45
|
+
"loading": "Loading..."
|
|
46
|
+
},
|
|
47
|
+
"auth": {
|
|
48
|
+
"login": "Sign in",
|
|
49
|
+
"logout": "Sign out",
|
|
50
|
+
"register": "Create account"
|
|
51
|
+
},
|
|
52
|
+
"users": {
|
|
53
|
+
"title": "Users",
|
|
54
|
+
"create": "Create user",
|
|
55
|
+
"name": "Name",
|
|
56
|
+
"email": "Email"
|
|
57
|
+
},
|
|
58
|
+
"validation": {
|
|
59
|
+
"required": "This field is required",
|
|
60
|
+
"email.invalid": "Please enter a valid email",
|
|
61
|
+
"too_small": "Must be at least {minimum} characters",
|
|
62
|
+
"too_big": "Must be at most {maximum} characters"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Server Components (RSC)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { getTranslations } from "next-intl/server";
|
|
71
|
+
|
|
72
|
+
export default async function UsersPage() {
|
|
73
|
+
const t = await getTranslations("users");
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<h1>{t("title")}</h1>
|
|
78
|
+
<Button>{t("create")}</Button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Client Components
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
"use client";
|
|
88
|
+
import { useTranslations } from "next-intl";
|
|
89
|
+
|
|
90
|
+
export function UserForm() {
|
|
91
|
+
const t = useTranslations("users");
|
|
92
|
+
|
|
93
|
+
return <label>{t("name")}</label>;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Server Actions with I18n
|
|
98
|
+
|
|
99
|
+
Pass locale context to server actions for localized error messages:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// src/actions/user.actions.ts
|
|
103
|
+
"use server";
|
|
104
|
+
import { getLocale, getTranslations } from "next-intl/server";
|
|
105
|
+
|
|
106
|
+
export async function createUser(input: unknown) {
|
|
107
|
+
const t = await getTranslations("validation");
|
|
108
|
+
const locale = await getLocale();
|
|
109
|
+
|
|
110
|
+
const schema = z.object({
|
|
111
|
+
name: z.string().min(2, t("too_small", { minimum: 2 })),
|
|
112
|
+
email: z.string().email(t("email.invalid")),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const parsed = schema.safeParse(input);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
return { success: false, error: parsed.error.errors[0].message };
|
|
118
|
+
}
|
|
119
|
+
// ...
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Zod Validation with I18n
|
|
124
|
+
|
|
125
|
+
### Custom Error Map
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// src/lib/zod-i18n.ts
|
|
129
|
+
import { z } from "zod";
|
|
130
|
+
|
|
131
|
+
export function createI18nErrorMap(
|
|
132
|
+
t: (key: string, params?: Record<string, unknown>) => string
|
|
133
|
+
): z.ZodErrorMap {
|
|
134
|
+
return (issue, ctx) => {
|
|
135
|
+
switch (issue.code) {
|
|
136
|
+
case z.ZodIssueCode.too_small:
|
|
137
|
+
if (issue.type === "string") {
|
|
138
|
+
return { message: t("too_small", { minimum: issue.minimum }) };
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case z.ZodIssueCode.too_big:
|
|
142
|
+
if (issue.type === "string") {
|
|
143
|
+
return { message: t("too_big", { maximum: issue.maximum }) };
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case z.ZodIssueCode.invalid_string:
|
|
147
|
+
if (issue.validation === "email") {
|
|
148
|
+
return { message: t("email.invalid") };
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case z.ZodIssueCode.invalid_type:
|
|
152
|
+
if (issue.received === "undefined") {
|
|
153
|
+
return { message: t("required") };
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
return { message: ctx.defaultError };
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Date and Number Formatting
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { useFormatter } from "next-intl";
|
|
166
|
+
|
|
167
|
+
function PriceDisplay({ amount }: { amount: number }) {
|
|
168
|
+
const format = useFormatter();
|
|
169
|
+
return <span>{format.number(amount, { style: "currency", currency: "EUR" })}</span>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function DateDisplay({ date }: { date: Date }) {
|
|
173
|
+
const format = useFormatter();
|
|
174
|
+
return <span>{format.dateTime(date, { dateStyle: "medium" })}</span>;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: layered-architecture
|
|
3
|
+
description: "Enforces strict layered architecture: Presentation → Facade → Service → DAL → Persistence. Use when structuring a Next.js application, creating new features with separation of concerns, or refactoring code into layers."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Layered Architecture
|
|
7
|
+
|
|
8
|
+
Strict separation of concerns across five layers. Each layer only calls the layer directly below it.
|
|
9
|
+
|
|
10
|
+
## Critical Rules
|
|
11
|
+
|
|
12
|
+
- **Never skip layers** — presentation must not call services directly.
|
|
13
|
+
- **No business logic in facades** — they coordinate, not decide.
|
|
14
|
+
- **No auth checks in DAL** — DAL is purely data access.
|
|
15
|
+
- **No database calls in services** — services call DAL.
|
|
16
|
+
- **Functional style** — pure functions, no classes, prefer composition over inheritance.
|
|
17
|
+
- **Top-down design** — keep functions short, extract when >100 lines.
|
|
18
|
+
|
|
19
|
+
## Layer Overview
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Presentation (RSC / Client Components)
|
|
23
|
+
↓
|
|
24
|
+
Facade (entry point for business logic)
|
|
25
|
+
↓
|
|
26
|
+
Service (business rules, authorization)
|
|
27
|
+
↓
|
|
28
|
+
DAL — Data Access Layer (queries, data shaping)
|
|
29
|
+
↓
|
|
30
|
+
Persistence (Drizzle ORM / database)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Presentation Layer
|
|
34
|
+
|
|
35
|
+
- React Server Components and Client Components live here.
|
|
36
|
+
- Components call **facades only** — never services or DAL directly.
|
|
37
|
+
- RSC by default. Only add `"use client"` when strictly needed (hooks, events, browser APIs).
|
|
38
|
+
- Top-down design: if a component exceeds ~100 lines, extract sub-functions/sub-components.
|
|
39
|
+
- Functional over OOP — pure functions, composition, immutability.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// app/(app)/users/page.tsx — Presentation
|
|
43
|
+
import { getUserList } from "@/facades/user.facade";
|
|
44
|
+
|
|
45
|
+
export default async function UsersPage() {
|
|
46
|
+
const users = await getUserList();
|
|
47
|
+
return <UserTable users={users} />;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Facade Layer
|
|
52
|
+
|
|
53
|
+
- Entry points for business logic. One facade per domain entity.
|
|
54
|
+
- Located in `src/facades/` — files named `{entity}.facade.ts`.
|
|
55
|
+
- Facades orchestrate service calls and transform data for the presentation.
|
|
56
|
+
- Facades handle auth context extraction and pass it down.
|
|
57
|
+
- Never contain business logic — only coordination.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// src/facades/user.facade.ts
|
|
61
|
+
import { getCurrentUser } from "@/lib/auth";
|
|
62
|
+
import { listUsers, createUser } from "@/services/user.service";
|
|
63
|
+
|
|
64
|
+
export async function getUserList() {
|
|
65
|
+
const currentUser = await getCurrentUser();
|
|
66
|
+
return listUsers(currentUser);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Service Layer
|
|
71
|
+
|
|
72
|
+
- Contains all business rules and authorization checks.
|
|
73
|
+
- Located in `src/services/` — files named `{entity}.service.ts`.
|
|
74
|
+
- Services receive the auth context as parameter — never fetch it themselves.
|
|
75
|
+
- CASL authorization checks happen here.
|
|
76
|
+
- Services call DAL functions for data access.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/services/user.service.ts
|
|
80
|
+
import { ForbiddenError } from "@casl/ability";
|
|
81
|
+
import { defineAbilityFor } from "@/lib/casl";
|
|
82
|
+
import { findAllUsers } from "@/dal/user.dal";
|
|
83
|
+
|
|
84
|
+
export async function listUsers(currentUser: AuthUser) {
|
|
85
|
+
const ability = defineAbilityFor(currentUser);
|
|
86
|
+
ForbiddenError.from(ability).throwUnlessCan("read", "User");
|
|
87
|
+
return findAllUsers();
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Data Access Layer (DAL)
|
|
92
|
+
|
|
93
|
+
- Pure data access — no business logic, no authorization.
|
|
94
|
+
- Located in `src/dal/` — files named `{entity}.dal.ts`.
|
|
95
|
+
- DAL functions shape data: select specific columns, join relations, paginate.
|
|
96
|
+
- Always return typed results — never raw query results.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/dal/user.dal.ts
|
|
100
|
+
import { db } from "@/lib/db";
|
|
101
|
+
import { users } from "@/schema";
|
|
102
|
+
|
|
103
|
+
export async function findAllUsers() {
|
|
104
|
+
return db.select({
|
|
105
|
+
id: users.id,
|
|
106
|
+
name: users.name,
|
|
107
|
+
email: users.email,
|
|
108
|
+
role: users.role,
|
|
109
|
+
}).from(users);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Persistence Layer
|
|
114
|
+
|
|
115
|
+
- Drizzle ORM schema definitions and database client.
|
|
116
|
+
- Located in `src/schema/` for table definitions, `src/lib/db.ts` for the client.
|
|
117
|
+
- Schema-only — no query logic here.
|
|
118
|
+
|
|
119
|
+
## File Naming Convention
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/
|
|
123
|
+
facades/ # {entity}.facade.ts
|
|
124
|
+
services/ # {entity}.service.ts
|
|
125
|
+
dal/ # {entity}.dal.ts
|
|
126
|
+
schema/ # {entity}.ts (Drizzle table defs)
|
|
127
|
+
lib/db.ts # Database client
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- All new files use **kebab-case**.
|
|
131
|
+
- One file per entity per layer — avoid catch-all files.
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nextjs-conventions
|
|
3
|
-
description: "
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "Next.js 15+ / React 19 / TypeScript conventions for App Router, RSC, route groups, and file naming. Use when creating pages, components, layouts, or structuring a Next.js application."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# Next.js Conventions
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **RSC by default** — only add `"use client"` when strictly needed.
|
|
11
|
+
- **Functional over OOP** — pure functions, composition, immutability. Never class components.
|
|
12
|
+
- **kebab-case for all new files** — `user-profile.tsx`, `format-date.ts`.
|
|
13
|
+
- **Top-down design** — extract sub-functions/sub-components when >100 lines.
|
|
14
|
+
- **Never use `any`** — use `unknown` if the type is truly unknown.
|
|
15
|
+
|
|
9
16
|
## App Router
|
|
10
17
|
|
|
11
18
|
- Use the App Router (`src/app/`) exclusively. Never use the Pages Router.
|
|
@@ -13,14 +20,39 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
13
20
|
- Use `loading.tsx` for Suspense boundaries and `error.tsx` for error boundaries.
|
|
14
21
|
- Use `not-found.tsx` for 404 pages at the appropriate route level.
|
|
15
22
|
|
|
23
|
+
## Route Groups
|
|
24
|
+
|
|
25
|
+
Organize routes by access level using route groups:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/app/
|
|
29
|
+
(public)/ # No auth required — landing, login, register
|
|
30
|
+
login/
|
|
31
|
+
register/
|
|
32
|
+
(auth)/ # Auth required, any role — onboarding
|
|
33
|
+
onboarding/
|
|
34
|
+
(app)/ # Auth required, active user — main app
|
|
35
|
+
dashboard/
|
|
36
|
+
settings/
|
|
37
|
+
admin/ # Admin only — user management, app settings
|
|
38
|
+
users/
|
|
39
|
+
settings/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `(public)` routes are accessible without authentication.
|
|
43
|
+
- `(auth)` routes require a session but no specific role.
|
|
44
|
+
- `(app)` routes require an active, verified user.
|
|
45
|
+
- `admin` routes require admin role — not a route group so the URL reflects `/admin/`.
|
|
46
|
+
|
|
16
47
|
## Server vs Client Components
|
|
17
48
|
|
|
18
|
-
- **
|
|
49
|
+
- **RSC by default**. Only add `"use client"` when strictly needed:
|
|
19
50
|
- Event handlers (`onClick`, `onChange`, etc.)
|
|
20
51
|
- React hooks (`useState`, `useEffect`, `useRef`, etc.)
|
|
21
52
|
- Browser-only APIs (`window`, `localStorage`, etc.)
|
|
22
53
|
- Never import server-only modules in client components.
|
|
23
54
|
- Pass server data to client components via props, not by importing server functions.
|
|
55
|
+
- Wrap async data in `<Suspense>` boundaries for streaming and progressive rendering.
|
|
24
56
|
|
|
25
57
|
## Data Fetching
|
|
26
58
|
|
|
@@ -31,10 +63,10 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
31
63
|
|
|
32
64
|
## File Naming
|
|
33
65
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
66
|
+
- **All new files: `kebab-case`** (e.g., `user-profile.tsx`, `format-date.ts`).
|
|
67
|
+
- Types: define in `src/types/` with `.ts` extension.
|
|
68
|
+
- Server Actions: `src/actions/{entity}.actions.ts`.
|
|
69
|
+
- Schemas: `src/schemas/{entity}.schema.ts`.
|
|
38
70
|
|
|
39
71
|
## TypeScript
|
|
40
72
|
|
|
@@ -62,3 +94,10 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
62
94
|
- Use `next/link` for internal navigation. Never use `<a>` for internal links.
|
|
63
95
|
- Use `next/font` for font loading.
|
|
64
96
|
- Lazy load heavy client components with `dynamic()` from `next/dynamic`.
|
|
97
|
+
|
|
98
|
+
## Code Design
|
|
99
|
+
|
|
100
|
+
- **Functional over OOP** — pure functions, composition, immutability. Never use class components.
|
|
101
|
+
- **Top-down design** — if a function or component exceeds ~100 lines, extract sub-functions or sub-components.
|
|
102
|
+
- Prefer named exports over default exports (except for page/layout components).
|
|
103
|
+
- Group imports: React/Next.js first, then external libs, then internal modules.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: react-native-patterns
|
|
3
|
-
description: "React Native and Expo best practices for navigation, styling, performance, and native features. Use when building mobile apps with Expo
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "React Native and Expo best practices for navigation, styling, performance, and native features. Use when building mobile apps with Expo, implementing React Native navigation, styling with NativeWind, optimizing mobile performance, or adding push notifications."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# React Native & Expo Patterns
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Always try Expo Go first** — only use `npx expo run:ios/android` when custom native modules are required.
|
|
11
|
+
- **Functional components only** — never class components.
|
|
12
|
+
- **Use `expo-image` instead of React Native's `Image`** — better caching and performance.
|
|
13
|
+
- **Use `react-native-safe-area-context`** — never React Native's built-in `SafeAreaView`.
|
|
14
|
+
- **Use `FlashList` instead of `FlatList`** for large lists — significantly better performance.
|
|
15
|
+
- **Never block the JS thread** — target 60fps, offload heavy computation to native.
|
|
16
|
+
|
|
9
17
|
## Expo Router
|
|
10
18
|
|
|
11
19
|
- Use file-based routing in the `app/` directory exclusively.
|
|
@@ -23,12 +31,6 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
23
31
|
- Prefer `useWindowDimensions` over `Dimensions.get()` for responsive layouts.
|
|
24
32
|
- Use flexbox exclusively for layout — avoid absolute positioning except for overlays.
|
|
25
33
|
|
|
26
|
-
## Running the App
|
|
27
|
-
|
|
28
|
-
- **Always try Expo Go first** before creating custom builds (`npx expo start`).
|
|
29
|
-
- Only use `npx expo run:ios/android` when custom native modules are required.
|
|
30
|
-
- Expo Go supports: all `expo-*` packages, Expo Router, Reanimated, Gesture Handler.
|
|
31
|
-
|
|
32
34
|
## Components
|
|
33
35
|
|
|
34
36
|
- Use functional components only — never class components.
|