@folpe/loom 0.1.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.
Files changed (45) hide show
  1. package/README.md +82 -16
  2. package/data/agents/backend/AGENT.md +12 -0
  3. package/data/agents/database/AGENT.md +3 -0
  4. package/data/agents/frontend/AGENT.md +10 -0
  5. package/data/agents/marketing/AGENT.md +3 -0
  6. package/data/agents/orchestrator/AGENT.md +1 -15
  7. package/data/agents/security/AGENT.md +3 -0
  8. package/data/agents/tests/AGENT.md +2 -0
  9. package/data/agents/ux-ui/AGENT.md +5 -0
  10. package/data/presets/api-backend.yaml +37 -0
  11. package/data/presets/chrome-extension.yaml +36 -0
  12. package/data/presets/cli-tool.yaml +31 -0
  13. package/data/presets/e-commerce.yaml +49 -0
  14. package/data/presets/expo-mobile.yaml +41 -0
  15. package/data/presets/fullstack-auth.yaml +45 -0
  16. package/data/presets/landing-page.yaml +38 -0
  17. package/data/presets/mvp-lean.yaml +35 -0
  18. package/data/presets/saas-default.yaml +7 -11
  19. package/data/presets/saas-full.yaml +71 -0
  20. package/data/skills/api-design/SKILL.md +149 -0
  21. package/data/skills/auth-rbac/SKILL.md +179 -0
  22. package/data/skills/better-auth-patterns/SKILL.md +212 -0
  23. package/data/skills/chrome-extension-patterns/SKILL.md +105 -0
  24. package/data/skills/cli-development/SKILL.md +147 -0
  25. package/data/skills/drizzle-patterns/SKILL.md +166 -0
  26. package/data/skills/env-validation/SKILL.md +142 -0
  27. package/data/skills/form-validation/SKILL.md +169 -0
  28. package/data/skills/hero-copywriting/SKILL.md +12 -4
  29. package/data/skills/i18n-patterns/SKILL.md +176 -0
  30. package/data/skills/layered-architecture/SKILL.md +131 -0
  31. package/data/skills/nextjs-conventions/SKILL.md +46 -7
  32. package/data/skills/react-native-patterns/SKILL.md +87 -0
  33. package/data/skills/react-query-patterns/SKILL.md +193 -0
  34. package/data/skills/resend-email/SKILL.md +181 -0
  35. package/data/skills/seo-optimization/SKILL.md +106 -0
  36. package/data/skills/server-actions-patterns/SKILL.md +156 -0
  37. package/data/skills/shadcn-ui/SKILL.md +126 -0
  38. package/data/skills/stripe-integration/SKILL.md +96 -0
  39. package/data/skills/supabase-patterns/SKILL.md +110 -0
  40. package/data/skills/table-pagination/SKILL.md +224 -0
  41. package/data/skills/tailwind-patterns/SKILL.md +12 -2
  42. package/data/skills/testing-patterns/SKILL.md +203 -0
  43. package/data/skills/ui-ux-guidelines/SKILL.md +179 -0
  44. package/dist/index.js +254 -100
  45. package/package.json +2 -1
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: supabase-patterns
3
+ description: "Supabase patterns for auth, database, RLS, storage, and real-time. Use when working with Supabase, writing RLS policies, implementing auth flows, or managing file storage."
4
+ ---
5
+
6
+ # Supabase Patterns
7
+
8
+ ## Critical Rules
9
+
10
+ - **Enable RLS on every table — no exceptions**, even internal tables.
11
+ - **Always validate session server-side** — never trust client-side `getUser()` alone.
12
+ - **Use `@supabase/ssr`** — never `@supabase/auth-helpers-nextjs` (deprecated).
13
+ - **Handle errors explicitly** — never ignore the `error` return value.
14
+ - **Use `LIMIT`** on all queries — never fetch unbounded result sets.
15
+ - **RLS policies on storage buckets** — no exceptions.
16
+
17
+ ## Authentication
18
+
19
+ - Use `@supabase/ssr` for Next.js server-side auth.
20
+ - Create two client helpers:
21
+ - `createClient()` in `src/lib/supabase/client.ts` for Client Components
22
+ - `createServerClient()` in `src/lib/supabase/server.ts` for Server Components / Actions
23
+ - Always validate session server-side before granting access.
24
+ - Use middleware (`middleware.ts`) to refresh the session on every request:
25
+ ```ts
26
+ const { data: { user } } = await supabase.auth.getUser()
27
+ if (!user && protectedRoutes.includes(request.nextUrl.pathname)) {
28
+ return NextResponse.redirect(new URL('/login', request.url))
29
+ }
30
+ ```
31
+ - Support multiple auth strategies: email/password, OAuth (Google, GitHub), magic link.
32
+ - Store user metadata in a separate `profiles` table linked by `auth.users.id`.
33
+
34
+ ## Row Level Security (RLS)
35
+
36
+ - Write policies that reference `auth.uid()` directly:
37
+ ```sql
38
+ CREATE POLICY "Users read own data"
39
+ ON profiles FOR SELECT
40
+ USING (auth.uid() = user_id);
41
+ ```
42
+ - Avoid `security definer` functions unless absolutely necessary. Prefer RLS policies.
43
+ - Test policies explicitly: try accessing data as different users/roles.
44
+ - Use `auth.jwt() ->> 'role'` for role-based access if implementing RBAC.
45
+
46
+ ## Database Schema
47
+
48
+ - Use `uuid` for primary keys, generated by `gen_random_uuid()`.
49
+ - Add `created_at` and `updated_at` timestamps to every table:
50
+ ```sql
51
+ created_at timestamptz DEFAULT now() NOT NULL,
52
+ updated_at timestamptz DEFAULT now() NOT NULL
53
+ ```
54
+ - Create an `updated_at` trigger using `moddatetime` extension.
55
+ - Use foreign key constraints for referential integrity.
56
+ - Add indexes on columns used in WHERE clauses and JOIN conditions.
57
+
58
+ ## Query Patterns
59
+
60
+ - Use the Supabase client query builder for simple CRUD:
61
+ ```ts
62
+ const { data, error } = await supabase
63
+ .from('posts')
64
+ .select('*, author:profiles(name, avatar_url)')
65
+ .eq('published', true)
66
+ .order('created_at', { ascending: false })
67
+ .limit(20)
68
+ ```
69
+ - Handle errors explicitly — never ignore `error`:
70
+ ```ts
71
+ if (error) throw new Error(error.message)
72
+ ```
73
+ - Use `.single()` when expecting exactly one row. Use `.maybeSingle()` when the row may not exist.
74
+ - Use server-side Supabase client for mutations in Server Actions.
75
+
76
+ ## Real-time
77
+
78
+ - Use Supabase Realtime only for features that genuinely need live updates (chat, notifications, collaboration).
79
+ - Subscribe in Client Components and clean up on unmount:
80
+ ```ts
81
+ useEffect(() => {
82
+ const channel = supabase.channel('room').on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, handler).subscribe()
83
+ return () => { supabase.removeChannel(channel) }
84
+ }, [])
85
+ ```
86
+ - Prefer polling or revalidation for data that changes infrequently.
87
+
88
+ ## Storage
89
+
90
+ - Use Supabase Storage for file uploads (images, documents).
91
+ - Create separate buckets for public vs private files.
92
+ - Set RLS policies on storage buckets — **no exceptions**.
93
+ - Generate signed URLs for private files with short expiration times.
94
+ - File upload pattern:
95
+ ```ts
96
+ const { data, error } = await supabase.storage
97
+ .from("avatars")
98
+ .upload(`${userId}/${fileName}`, file, {
99
+ cacheControl: "3600",
100
+ upsert: true,
101
+ });
102
+ ```
103
+ - Get public URL: `supabase.storage.from("avatars").getPublicUrl(path)`.
104
+
105
+ ## Performance
106
+
107
+ - Use connection pooling (Supavisor) in production — never connect directly at scale.
108
+ - Add `LIMIT` to all queries. Never fetch unbounded result sets.
109
+ - Use `select('column1, column2')` instead of `select('*')` for large tables.
110
+ - Create partial indexes for frequently filtered subsets of data.
@@ -0,0 +1,224 @@
1
+ ---
2
+ name: table-pagination
3
+ description: "Data table patterns with server-side pagination, toolbar, and responsive columns. Use when building data tables, implementing search and pagination, or creating admin list views."
4
+ ---
5
+
6
+ # Table & Pagination Patterns
7
+
8
+ ## Critical Rules
9
+
10
+ - **Server-side pagination** — paginate in the database, not in the client.
11
+ - **URL-based state** — page, limit, search, sort in query params for shareable URLs.
12
+ - **Toolbar = search + counter + limit** — consistent UX across all tables.
13
+ - **Responsive columns** — hide columns with `hidden sm:table-cell` breakpoints.
14
+ - **Reset page on filter change** — always return to page 1 when search/filter changes.
15
+ - **Use `tabular-nums`** on numeric columns — for proper alignment.
16
+
17
+ ## Table Architecture
18
+
19
+ ```
20
+ DataTable (container)
21
+ ├── Toolbar (search + filters + counter + limit selector)
22
+ ├── Table (columns + rows)
23
+ └── Pagination (page navigation)
24
+ ```
25
+
26
+ ## Server-Side Pagination
27
+
28
+ ### URL-Based State
29
+
30
+ ```tsx
31
+ // app/users/page.tsx
32
+ import { Suspense } from "react";
33
+ import { getUsersPaginated } from "@/facades/user.facade";
34
+
35
+ interface Props {
36
+ searchParams: Promise<{
37
+ page?: string;
38
+ limit?: string;
39
+ search?: string;
40
+ sort?: string;
41
+ order?: string;
42
+ }>;
43
+ }
44
+
45
+ export default async function UsersPage({ searchParams }: Props) {
46
+ const params = await searchParams;
47
+ const page = Number(params.page) || 1;
48
+ const limit = Number(params.limit) || 20;
49
+
50
+ const { data, total } = await getUsersPaginated({
51
+ page,
52
+ limit,
53
+ search: params.search,
54
+ sort: params.sort,
55
+ order: params.order as "asc" | "desc",
56
+ });
57
+
58
+ return (
59
+ <DataTable
60
+ columns={columns}
61
+ data={data}
62
+ total={total}
63
+ page={page}
64
+ limit={limit}
65
+ />
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### Backend Pagination
71
+
72
+ ```ts
73
+ // src/dal/user.dal.ts
74
+ import { db } from "@/lib/db";
75
+ import { users } from "@/schema";
76
+ import { sql, ilike, desc, asc } from "drizzle-orm";
77
+
78
+ interface PaginationParams {
79
+ page: number;
80
+ limit: number;
81
+ search?: string;
82
+ sort?: string;
83
+ order?: "asc" | "desc";
84
+ }
85
+
86
+ export async function findUsersPaginated(params: PaginationParams) {
87
+ const { page, limit, search, sort = "createdAt", order = "desc" } = params;
88
+ const offset = (page - 1) * limit;
89
+
90
+ const where = search ? ilike(users.name, `%${search}%`) : undefined;
91
+ const orderBy = order === "asc" ? asc(users[sort]) : desc(users[sort]);
92
+
93
+ const [data, [{ count }]] = await Promise.all([
94
+ db.select().from(users).where(where).orderBy(orderBy).limit(limit).offset(offset),
95
+ db.select({ count: sql<number>`count(*)` }).from(users).where(where),
96
+ ]);
97
+
98
+ return { data, total: Number(count) };
99
+ }
100
+ ```
101
+
102
+ ## Table Toolbar
103
+
104
+ ```tsx
105
+ "use client";
106
+
107
+ export function DataTableToolbar({ table, total }: ToolbarProps) {
108
+ const searchParams = useSearchParams();
109
+ const router = useRouter();
110
+ const pathname = usePathname();
111
+
112
+ function updateParam(key: string, value: string) {
113
+ const params = new URLSearchParams(searchParams);
114
+ if (value) params.set(key, value);
115
+ else params.delete(key);
116
+ params.delete("page"); // reset to page 1 on filter change
117
+ router.push(`${pathname}?${params}`);
118
+ }
119
+
120
+ return (
121
+ <div className="flex items-center justify-between gap-2">
122
+ <div className="flex flex-1 items-center gap-2">
123
+ <Input
124
+ placeholder="Search..."
125
+ defaultValue={searchParams.get("search") ?? ""}
126
+ onChange={(e) => updateParam("search", e.target.value)}
127
+ className="h-8 w-[250px]"
128
+ />
129
+ <span className="text-sm text-muted-foreground">
130
+ {total} result{total !== 1 ? "s" : ""}
131
+ </span>
132
+ </div>
133
+ <div className="flex items-center gap-2">
134
+ <Select
135
+ defaultValue={searchParams.get("limit") ?? "20"}
136
+ onValueChange={(v) => updateParam("limit", v)}
137
+ >
138
+ <SelectTrigger className="h-8 w-[80px]">
139
+ <SelectValue />
140
+ </SelectTrigger>
141
+ <SelectContent>
142
+ {[10, 20, 50, 100].map((n) => (
143
+ <SelectItem key={n} value={String(n)}>{n}</SelectItem>
144
+ ))}
145
+ </SelectContent>
146
+ </Select>
147
+ </div>
148
+ </div>
149
+ );
150
+ }
151
+ ```
152
+
153
+ ## Responsive Columns
154
+
155
+ Hide columns by breakpoint to keep tables readable on mobile:
156
+
157
+ ```tsx
158
+ const columns: ColumnDef<User>[] = [
159
+ {
160
+ accessorKey: "name",
161
+ header: "Name",
162
+ // Always visible
163
+ },
164
+ {
165
+ accessorKey: "email",
166
+ header: "Email",
167
+ meta: { className: "hidden sm:table-cell" },
168
+ },
169
+ {
170
+ accessorKey: "role",
171
+ header: "Role",
172
+ meta: { className: "hidden md:table-cell" },
173
+ },
174
+ {
175
+ accessorKey: "createdAt",
176
+ header: "Created",
177
+ meta: { className: "hidden lg:table-cell" },
178
+ cell: ({ row }) => formatDate(row.getValue("createdAt")),
179
+ },
180
+ ];
181
+ ```
182
+
183
+ Apply the responsive class in the table cell:
184
+
185
+ ```tsx
186
+ <TableCell className={column.columnDef.meta?.className}>
187
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
188
+ </TableCell>
189
+ ```
190
+
191
+ ## Pagination Component
192
+
193
+ ```tsx
194
+ "use client";
195
+
196
+ export function DataTablePagination({ page, limit, total }: PaginationProps) {
197
+ const totalPages = Math.ceil(total / limit);
198
+ const searchParams = useSearchParams();
199
+ const router = useRouter();
200
+ const pathname = usePathname();
201
+
202
+ function goToPage(p: number) {
203
+ const params = new URLSearchParams(searchParams);
204
+ params.set("page", String(p));
205
+ router.push(`${pathname}?${params}`);
206
+ }
207
+
208
+ return (
209
+ <div className="flex items-center justify-between px-2">
210
+ <p className="text-sm text-muted-foreground">
211
+ Page {page} of {totalPages}
212
+ </p>
213
+ <div className="flex items-center gap-2">
214
+ <Button variant="outline" size="sm" onClick={() => goToPage(page - 1)} disabled={page <= 1}>
215
+ Previous
216
+ </Button>
217
+ <Button variant="outline" size="sm" onClick={() => goToPage(page + 1)} disabled={page >= totalPages}>
218
+ Next
219
+ </Button>
220
+ </div>
221
+ </div>
222
+ );
223
+ }
224
+ ```
@@ -1,11 +1,18 @@
1
1
  ---
2
2
  name: tailwind-patterns
3
- description: "Provides Tailwind CSS patterns, utility conventions, and component styling guidelines. Use when building UI with Tailwind CSS."
4
- allowed-tools: "Read, Write, Edit"
3
+ description: "Tailwind CSS utility conventions, responsive design, and component styling. Use when styling components, implementing responsive layouts, or applying consistent spacing and typography."
5
4
  ---
6
5
 
7
6
  # Tailwind CSS Patterns
8
7
 
8
+ ## Critical Rules
9
+
10
+ - **Utility-first** — use utility classes directly in JSX, avoid `@apply`.
11
+ - **Mobile-first** — base styles for mobile, then `sm:`, `md:`, `lg:`, `xl:`.
12
+ - **Semantic color tokens** — use `bg-background`, `text-foreground`, never hardcode hex.
13
+ - **Progressive padding** — `px-0 sm:px-2 md:px-4 lg:px-6` grows with viewport.
14
+ - **Use `cn()`** — from `@/lib/utils` for conditional and merged classes.
15
+
9
16
  ## Utility-First Approach
10
17
 
11
18
  - Always use utility classes directly in JSX. Avoid `@apply` except in global base styles.
@@ -30,6 +37,9 @@ allowed-tools: "Read, Write, Edit"
30
37
  - `lg:` (1024px) — laptops
31
38
  - `xl:` (1280px) — desktops
32
39
  - Grid responsive pattern: `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3`
40
+ - **Progressive padding**: `px-0 sm:px-2 md:px-4 lg:px-6` — spacing grows with viewport.
41
+ - **Cards border adaptatif**: `border-0 sm:border` — no border on mobile, border on larger screens.
42
+ - **Table columns by breakpoint**: hide non-essential columns on small screens with `hidden sm:table-cell`, `hidden md:table-cell`, `hidden lg:table-cell`.
33
43
 
34
44
  ## Typography
35
45
 
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: testing-patterns
3
+ description: "Vitest testing strategy with role-based patterns, seed data factories, and CI/CD integration. Use when writing tests, creating test data, or setting up continuous integration pipelines."
4
+ ---
5
+
6
+ # Testing Patterns
7
+
8
+ ## Critical Rules
9
+
10
+ - **Test every role** — PUBLIC, USER, ADMIN for every feature.
11
+ - **Test services, not routes** — service layer is the source of truth.
12
+ - **Use seed factories** — deterministic, reusable test data.
13
+ - **Clean up after each test** — truncate tables, no test pollution.
14
+ - **CI runs tests + build** — every PR must pass before merge.
15
+ - **No mocking the database** — test against a real PostgreSQL instance.
16
+
17
+ ## Role-Based Test Matrix
18
+
19
+ Every feature must be tested from 3 perspectives:
20
+
21
+ | Role | What to test |
22
+ |------|-------------|
23
+ | **PUBLIC** | Unauthenticated access — redirects, public pages, API 401s |
24
+ | **USER** | Standard user — CRUD own resources, forbidden on others |
25
+ | **ADMIN** | Admin user — manage all resources, admin-only features |
26
+
27
+ ```ts
28
+ describe("deletePost", () => {
29
+ it("should return 401 for public (unauthenticated)", async () => {
30
+ const result = await deletePost(null, postId);
31
+ expect(result.error).toBe("Unauthorized");
32
+ });
33
+
34
+ it("should allow USER to delete own post", async () => {
35
+ const result = await deletePost(userSession, ownPostId);
36
+ expect(result.success).toBe(true);
37
+ });
38
+
39
+ it("should forbid USER from deleting another's post", async () => {
40
+ const result = await deletePost(userSession, otherPostId);
41
+ expect(result.error).toBe("Forbidden");
42
+ });
43
+
44
+ it("should allow ADMIN to delete any post", async () => {
45
+ const result = await deletePost(adminSession, otherPostId);
46
+ expect(result.success).toBe(true);
47
+ });
48
+ });
49
+ ```
50
+
51
+ ## Vitest Configuration
52
+
53
+ ```ts
54
+ // vitest.config.ts
55
+ import { defineConfig } from "vitest/config";
56
+ import path from "path";
57
+
58
+ export default defineConfig({
59
+ test: {
60
+ globals: true,
61
+ environment: "node",
62
+ setupFiles: ["./tests/setup.ts"],
63
+ include: ["**/*.test.ts", "**/*.test.tsx"],
64
+ coverage: {
65
+ provider: "v8",
66
+ reporter: ["text", "lcov"],
67
+ exclude: ["node_modules", "tests/setup.ts"],
68
+ },
69
+ },
70
+ resolve: {
71
+ alias: {
72
+ "@": path.resolve(__dirname, "./src"),
73
+ },
74
+ },
75
+ });
76
+ ```
77
+
78
+ ## Seed Data
79
+
80
+ ### Seed Users
81
+
82
+ ```ts
83
+ // tests/seed/users.ts
84
+ export const SEED_USERS = {
85
+ public: null, // no session
86
+ user: {
87
+ id: "user-1",
88
+ email: "user@test.com",
89
+ name: "Test User",
90
+ role: "USER" as const,
91
+ },
92
+ admin: {
93
+ id: "admin-1",
94
+ email: "admin@test.com",
95
+ name: "Test Admin",
96
+ role: "ADMIN" as const,
97
+ },
98
+ } as const;
99
+ ```
100
+
101
+ ### Seed Data Factory
102
+
103
+ ```ts
104
+ // tests/seed/factories.ts
105
+ import { db } from "@/lib/db";
106
+ import { users, posts } from "@/schema";
107
+
108
+ export async function seedUser(overrides?: Partial<NewUser>) {
109
+ const [user] = await db.insert(users).values({
110
+ name: "Test User",
111
+ email: `test-${Date.now()}@test.com`,
112
+ role: "USER",
113
+ ...overrides,
114
+ }).returning();
115
+ return user;
116
+ }
117
+
118
+ export async function seedPost(authorId: string, overrides?: Partial<NewPost>) {
119
+ const [post] = await db.insert(posts).values({
120
+ title: "Test Post",
121
+ content: "Test content",
122
+ authorId,
123
+ published: true,
124
+ ...overrides,
125
+ }).returning();
126
+ return post;
127
+ }
128
+ ```
129
+
130
+ ## Test Setup
131
+
132
+ ```ts
133
+ // tests/setup.ts
134
+ import { beforeAll, afterAll, afterEach } from "vitest";
135
+ import { db } from "@/lib/db";
136
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
137
+
138
+ beforeAll(async () => {
139
+ await migrate(db, { migrationsFolder: "./drizzle" });
140
+ });
141
+
142
+ afterEach(async () => {
143
+ // Clean up test data — truncate in reverse FK order
144
+ await db.execute(sql`TRUNCATE posts, users CASCADE`);
145
+ });
146
+ ```
147
+
148
+ ## Service Layer Testing
149
+
150
+ ```ts
151
+ // src/services/__tests__/user.service.test.ts
152
+ import { describe, it, expect, beforeEach } from "vitest";
153
+ import { listUsers, deleteUser } from "@/services/user.service";
154
+ import { SEED_USERS } from "tests/seed/users";
155
+ import { seedUser } from "tests/seed/factories";
156
+
157
+ describe("user.service", () => {
158
+ let testUser: User;
159
+
160
+ beforeEach(async () => {
161
+ testUser = await seedUser();
162
+ });
163
+
164
+ describe("listUsers", () => {
165
+ it("should allow ADMIN to list all users", async () => {
166
+ const result = await listUsers(SEED_USERS.admin);
167
+ expect(result).toHaveLength(1);
168
+ });
169
+
170
+ it("should forbid USER from listing users", async () => {
171
+ await expect(listUsers(SEED_USERS.user)).rejects.toThrow("Forbidden");
172
+ });
173
+ });
174
+ });
175
+ ```
176
+
177
+ ## CI/CD Integration
178
+
179
+ ```yaml
180
+ # .github/workflows/ci.yml
181
+ name: CI
182
+ on: [push, pull_request]
183
+
184
+ jobs:
185
+ test:
186
+ runs-on: ubuntu-latest
187
+ services:
188
+ postgres:
189
+ image: postgres:16
190
+ env:
191
+ POSTGRES_DB: test
192
+ POSTGRES_USER: test
193
+ POSTGRES_PASSWORD: test
194
+ ports: ["5432:5432"]
195
+ steps:
196
+ - uses: actions/checkout@v4
197
+ - uses: pnpm/action-setup@v4
198
+ - uses: actions/setup-node@v4
199
+ with: { node-version: 20, cache: "pnpm" }
200
+ - run: pnpm install --frozen-lockfile
201
+ - run: pnpm test
202
+ - run: pnpm build
203
+ ```