@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.
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 -19
  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 +0 -3
  11. package/data/presets/chrome-extension.yaml +0 -3
  12. package/data/presets/cli-tool.yaml +0 -3
  13. package/data/presets/e-commerce.yaml +0 -3
  14. package/data/presets/expo-mobile.yaml +0 -3
  15. package/data/presets/fullstack-auth.yaml +0 -3
  16. package/data/presets/landing-page.yaml +0 -3
  17. package/data/presets/mvp-lean.yaml +0 -3
  18. package/data/presets/saas-default.yaml +3 -4
  19. package/data/presets/saas-full.yaml +71 -0
  20. package/data/skills/api-design/SKILL.md +43 -2
  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 +13 -6
  24. package/data/skills/cli-development/SKILL.md +11 -3
  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 +10 -8
  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 +10 -2
  36. package/data/skills/server-actions-patterns/SKILL.md +156 -0
  37. package/data/skills/shadcn-ui/SKILL.md +46 -12
  38. package/data/skills/stripe-integration/SKILL.md +11 -3
  39. package/data/skills/supabase-patterns/SKILL.md +23 -6
  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 +10 -5
  44. package/dist/index.js +254 -100
  45. package/package.json +2 -1
@@ -1,18 +1,26 @@
1
1
  ---
2
2
  name: supabase-patterns
3
- description: "Supabase best practices for auth, database, RLS, and real-time. Use when working with Supabase in any project. Inspired by supabase/agent-skills."
4
- allowed-tools: "Read, Write, Edit, Glob, Grep"
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."
5
4
  ---
6
5
 
7
6
  # Supabase Patterns
8
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
+
9
17
  ## Authentication
10
18
 
11
- - Use `@supabase/ssr` for Next.js server-side auth — never use `@supabase/auth-helpers-nextjs` (deprecated).
19
+ - Use `@supabase/ssr` for Next.js server-side auth.
12
20
  - Create two client helpers:
13
21
  - `createClient()` in `src/lib/supabase/client.ts` for Client Components
14
22
  - `createServerClient()` in `src/lib/supabase/server.ts` for Server Components / Actions
15
- - Always validate session server-side before granting access. Never trust `supabase.auth.getUser()` from the client alone.
23
+ - Always validate session server-side before granting access.
16
24
  - Use middleware (`middleware.ts`) to refresh the session on every request:
17
25
  ```ts
18
26
  const { data: { user } } = await supabase.auth.getUser()
@@ -25,7 +33,6 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
25
33
 
26
34
  ## Row Level Security (RLS)
27
35
 
28
- - **Enable RLS on every table** — no exceptions, even internal tables.
29
36
  - Write policies that reference `auth.uid()` directly:
30
37
  ```sql
31
38
  CREATE POLICY "Users read own data"
@@ -82,8 +89,18 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
82
89
 
83
90
  - Use Supabase Storage for file uploads (images, documents).
84
91
  - Create separate buckets for public vs private files.
85
- - Set RLS policies on storage buckets.
92
+ - Set RLS policies on storage buckets — **no exceptions**.
86
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)`.
87
104
 
88
105
  ## Performance
89
106
 
@@ -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
+ ```
@@ -1,19 +1,25 @@
1
1
  ---
2
2
  name: ui-ux-guidelines
3
- description: "Comprehensive UI/UX design intelligence: accessibility, interaction patterns, typography, color palettes, animation rules, and pre-delivery checklist. Use when building any user-facing interface. Inspired by nextlevelbuilder/ui-ux-pro-max-skill."
4
- allowed-tools: "Read, Write, Edit, Glob, Grep"
3
+ description: "UI/UX design rules for accessibility, interaction, typography, color, and animation. Use when building user-facing interfaces, reviewing designs, checking accessibility compliance, or choosing fonts and color palettes."
5
4
  ---
6
5
 
7
6
  # UI/UX Design Guidelines
8
7
 
9
8
  Comprehensive design guide covering accessibility, interaction, layout, typography, color, and animation — prioritized by impact.
10
9
 
10
+ ## Critical Rules
11
+
12
+ - **Color contrast**: minimum 4.5:1 for normal text, 3:1 for large text.
13
+ - **Touch targets**: minimum 44x44px on mobile.
14
+ - **No `h-screen`** — use `h-dvh` for correct mobile viewport.
15
+ - **No animation unless requested** — respect `prefers-reduced-motion`.
16
+ - **Empty states must have one clear next action** — never blank screens.
17
+ - **All icon-only buttons must have `aria-label`**.
18
+
11
19
  ## Priority 1 — Accessibility (CRITICAL)
12
20
 
13
- - **Color contrast**: minimum 4.5:1 ratio for normal text, 3:1 for large text.
14
21
  - **Color is not enough**: never convey information by color alone — add icons or text.
15
22
  - **Alt text**: descriptive alt for all meaningful images. Use `alt=""` only for decorative images.
16
- - **ARIA labels**: every icon-only button must have `aria-label`.
17
23
  - **Heading hierarchy**: sequential h1 → h2 → h3, one `<h1>` per page.
18
24
  - **Keyboard navigation**: tab order matches visual order, no keyboard traps.
19
25
  - **Focus states**: visible focus rings on all interactive elements — never `outline-none` without replacement.
@@ -24,7 +30,6 @@ Comprehensive design guide covering accessibility, interaction, layout, typograp
24
30
 
25
31
  ## Priority 2 — Touch & Interaction (CRITICAL)
26
32
 
27
- - **Touch targets**: minimum 44x44px on mobile — `min-h-[44px] min-w-[44px]`.
28
33
  - **Touch spacing**: minimum 8px gap between adjacent touch targets.
29
34
  - **Hover vs tap**: never rely on hover for primary interactions — use click/tap.
30
35
  - **Cursor pointer**: add `cursor-pointer` to all clickable elements.