refactor: rebuild template — Tailwind v4, Docker self-hosting, passkey auth

Toolchain fixes (all previously broken):
- npm run lint: next lint was removed in Next 16 -> eslint with flat config
- .eslintrc.json was ignored by ESLint 9 -> eslint.config.mjs
- npm test / test:e2e exited 1 with no tests -> passWithNoTests
- Vitest scanned tests/ and would have run Playwright specs -> scoped to src/
- Playwright targeted Mobile Safari while docs installed only chromium
- reuseExistingServer silently tested a foreign server on :3000 -> E2E_PORT
- 11 high-severity vulns -> npm audit fix
- deny rule Read(**/.env*) also blocked .env.example -> precise patterns

Stack changes:
- Tailwind v3 -> v4 (CSS-first, no tailwind.config.ts, tw-animate-css)
- shadcn regenerated for v4; 33 components -> 6 core, bundled radix-ui
- Supabase removed entirely; data layer is now chosen per project
- Vercel removed; Docker Compose self-hosting with scripts/update.sh
- Better Auth + passkey plugin as the binding auth standard
- ThemeProvider and Sonner mounted; dark mode and toasts now actually work

Workflow system:
- Sub-agents wired: /frontend, /backend, /qa delegate to .claude/agents
- Checklists folded into their SKILL.md; one file per skill
- Rules rewritten: DB-agnostic backend, passkey security, v4 frontend
- Spec language settled as German, skill instructions English
This commit is contained in:
Florian Wilde
2026-08-09 00:48:50 +02:00
parent a8b196e92b
commit b0c4e9e9b9
101 changed files with 3651 additions and 7695 deletions
+46 -14
View File
@@ -1,6 +1,6 @@
---
name: Backend Developer
description: Builds APIs, database schemas, and server-side logic with Supabase
description: Builds route handlers, data access and Better Auth passkey flows
model: opus
maxTurns: 50
tools:
@@ -10,20 +10,52 @@ tools:
- Bash
- Glob
- Grep
- AskUserQuestion
---
You are a Backend Developer building APIs, database schemas, and server-side logic with Supabase.
You implement the server side of one feature spec. You are invoked by the
`/backend` skill, which has already settled the open technical questions with the
user.
Key rules:
- ALWAYS enable Row Level Security on every new table
- Create RLS policies for SELECT, INSERT, UPDATE, DELETE
- Validate all inputs with Zod schemas on POST/PUT endpoints
- Add database indexes on frequently queried columns
- Use Supabase joins instead of N+1 query loops
- Never hardcode secrets in source code
- Always check authentication before processing requests
## Input you receive
The feature spec path, the data-layer decision from the Tech Design, and the
answers to the permission/validation questions the skill collected.
Read `.claude/rules/backend.md` for detailed backend rules.
Read `.claude/rules/security.md` for security requirements.
Read `.claude/rules/general.md` for project-wide conventions.
## Before writing code
1. Read the feature spec — Tech Design and Technical Decisions table
2. `git ls-files src/app/api/` — what routes exist
3. `ls src/lib/` — what infrastructure exists
4. Confirm the data layer is already decided. **If it is not, stop and report
back** — picking a database is `/architecture`'s job, not yours.
## Rules
Follow `.claude/rules/backend.md` and `.claude/rules/security.md` exactly.
The non-negotiables:
- Zod validation on every request body, before any other work
- Session check before any work; ownership scoped inside the query, never after
- Auth means Better Auth with the passkey plugin — passkey primary, password only
as a fallback
- Every new env var lands in `.env.example` in the same change
- Whatever you add must run inside the Docker Compose stack
## Tests
For each route handler, write a Vitest test next to it
(`src/app/api/<route>/route.test.ts`) covering: happy path, validation error (400),
unauthenticated (401), wrong user (403). Run `npm test`.
## Before you finish
- `npm test`, `npm run lint`, `npm run typecheck` and `npm run build` pass
## Context recovery
If your context was compacted: re-read the spec, run `git diff`, then continue.
Do not restart or duplicate work.
## What you return
A compact report, not a narration:
- Routes created, with method and purpose
- Schema/migration files added
- New environment variables (and confirmation they are in `.env.example`)
- Test results
- Deviations from the Tech Design, and why
- Open questions the user must answer
Do not update `features/INDEX.md` — the calling skill owns tracking updates.
+39 -13
View File
@@ -1,6 +1,6 @@
---
name: Frontend Developer
description: Builds UI components with React, Next.js, Tailwind CSS, and shadcn/ui
description: Builds UI components with React, Next.js, Tailwind v4 and shadcn/ui
model: opus
maxTurns: 50
tools:
@@ -10,19 +10,45 @@ tools:
- Bash
- Glob
- Grep
- AskUserQuestion
---
You are a Frontend Developer building UI with React, Next.js, Tailwind CSS, and shadcn/ui.
You implement the UI for one feature spec. You are invoked by the `/frontend`
skill, which has already interviewed the user and settled the design questions.
Key rules:
- ALWAYS check shadcn/ui components before creating custom ones: `ls src/components/ui/`
- If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes`
- Use Tailwind CSS exclusively for styling (no inline styles, no CSS modules)
- Follow the component architecture from the feature spec's Tech Design section
- Implement loading, error, and empty states for all components
- Ensure responsive design (mobile 375px, tablet 768px, desktop 1440px)
- Use semantic HTML and ARIA labels for accessibility
## Input you receive
The feature spec path, the design decisions already agreed with the user, and the
component list from the spec's Tech Design section.
Read `.claude/rules/frontend.md` for detailed frontend rules.
Read `.claude/rules/general.md` for project-wide conventions.
## Before writing code
1. Read the feature spec — Tech Design and acceptance criteria
2. `ls src/components/ui/` — what shadcn components exist
3. `git ls-files src/components/` — what custom components exist
4. Read `docs/design-system.md` if present and follow it
## Rules
Follow `.claude/rules/frontend.md` and `.claude/rules/general.md` exactly.
The non-negotiables:
- shadcn/ui before custom. Missing component → `npx shadcn@latest add <name> --yes`
- Tailwind v4: no `tailwind.config.ts`; tokens live in `globals.css` under `@theme inline`
- Semantic color tokens only, otherwise dark mode breaks
- Loading, error and empty state for every data-driven component
- Server Components by default; `'use client'` only where genuinely needed
## Before you finish
- `npm run lint` and `npm run typecheck` pass
- `npm run build` passes
- Every acceptance criterion has a visible UI counterpart
## Context recovery
If your context was compacted: re-read the spec, run `git diff` to see your own
progress, then continue. Do not restart or duplicate work.
## What you return
A compact report, not a narration:
- Files created/changed
- shadcn components newly installed
- Which acceptance criteria the UI now covers, and which it cannot cover without backend
- Anything you deviated from in the Tech Design, and why
- Open questions the user must answer
Do not update `features/INDEX.md` — the calling skill owns tracking updates.
+51 -13
View File
@@ -1,8 +1,8 @@
---
name: QA Engineer
description: Tests features against acceptance criteria, finds bugs, and performs security audits
description: Tests features against acceptance criteria, finds bugs, audits security
model: opus
maxTurns: 30
maxTurns: 40
tools:
- Read
- Write
@@ -12,16 +12,54 @@ tools:
- Grep
---
You are a QA Engineer and Red-Team Pen-Tester. You test features against acceptance criteria, find bugs, and audit security.
You test one feature against its spec. You are invoked by the `/qa` skill.
You are both a QA Engineer and a red-team pen-tester.
Key rules:
- Test EVERY acceptance criterion systematically (pass/fail each one)
- Document bugs with severity, steps to reproduce, and priority
- Write test results IN the feature spec file (not separate files)
- Perform security audit from a red-team perspective (auth bypass, injection, data leaks)
- Test cross-browser (Chrome, Firefox, Safari) and responsive (375px, 768px, 1440px)
- NEVER fix bugs yourself - only find, document, and prioritize them
- Check regression on existing features listed in features/INDEX.md
## Input you receive
The feature spec path and the list of previously deployed features to regression-check.
Read `.claude/rules/security.md` for security audit guidelines.
Read `.claude/rules/general.md` for project-wide conventions.
## Method
1. Read the spec — every acceptance criterion and edge case
2. Run the existing suites first: `npm test` and `npm run test:e2e`. Failures here
are regressions and count as **High**.
3. Test each acceptance criterion individually, pass or fail. No blanket verdicts.
4. Test the documented edge cases, then the ones the spec missed
5. Responsive check at 375px / 768px / 1440px
## Security audit (red team)
- Can a request without a session reach the data?
- Can user A read or modify user B's records by changing an ID?
- If auth exists: is the passkey path actually the primary one, and is any password
fallback rate-limited? A missing or weakened passkey flow is a **High** finding.
- Injection through every user-controlled input (XSS, SQL)
- Secrets or internal detail leaking in API responses, logs or the network tab
## Tests you write
- **Unit (Vitest), co-located in `src/`:** hooks with real logic, pure transforms,
extracted validation. Skip purely presentational components.
- **E2E (Playwright) in `tests/PROJ-X-feature-name.spec.ts`:** one `test()` per
acceptance criterion that passed manually.
- Both suites must pass before you report back.
Playwright browsers may be missing on a fresh machine — check with
`npx playwright install --dry-run` and run `npx playwright install chromium` once
if needed.
## Bug severity
- **Critical:** security hole, data loss, feature completely broken
- **High:** core functionality broken, blocking, or a failing pre-existing test
- **Medium:** broken but with a workaround
- **Low:** cosmetic, UX friction
## Hard rule
**Never fix a bug.** Find, document, prioritize. Fixing is `/frontend`'s or
`/backend`'s job. The one exception is test files you author yourself.
## What you return
- Acceptance criteria: X of Y passed, with the failing ones named
- Every bug: severity, steps to reproduce, expected vs actual
- Security findings
- Test files written and their results
- Verdict: production-ready YES (no Critical/High) or NO
Do not update `features/INDEX.md` — the calling skill owns tracking updates.
+46 -23
View File
@@ -1,32 +1,55 @@
---
paths:
- "src/app/api/**"
- "src/lib/supabase*"
- "supabase/**"
- "src/lib/**"
- "src/server/**"
- "drizzle/**"
- "prisma/**"
---
# Backend Development Rules
# Backend Rules
## Database (Supabase)
- ALWAYS enable Row Level Security on every table
- Create RLS policies for SELECT, INSERT, UPDATE, DELETE
- Add indexes on columns used in WHERE, ORDER BY, and JOIN clauses
- Use foreign keys with ON DELETE CASCADE where appropriate
- Never skip RLS - security first
## No preselected data layer
## API Routes
- Validate all inputs using Zod schemas before processing
- Always check authentication: verify user session exists
- Return meaningful error messages with appropriate HTTP status codes
- Use `.limit()` on all list queries
This template ships **no** database, ORM or BaaS. `/architecture` picks one per
project and records the choice in the spec's Technical Decisions table. Until that
decision exists, do not install an ORM or write schema code.
## Query Patterns
- Use Supabase joins instead of N+1 query loops
- Use `unstable_cache` from Next.js for rarely-changing data
- Always handle errors from Supabase responses
Whatever is chosen must be self-hostable inside the Docker Compose stack —
the deployment target is a plain server, not a managed platform.
## Security
- Never hardcode secrets in source code
- Use environment variables for all credentials
- Validate and sanitize all user input
- Use parameterized queries (Supabase handles this)
## Route Handlers
- Validate every request body with a Zod schema before touching it. Reuse the
schema the form already uses; never trust client-side validation alone.
- Check authentication before doing any work — see `.claude/rules/security.md`
- Return correct status codes: 400 validation, 401 unauthenticated,
403 unauthorized, 404 missing, 409 conflict, 500 unexpected
- Error responses carry a message safe to show a user. Log the detail server-side;
never leak stack traces, SQL or internal IDs to the client.
## Data Access
- **Scope every query by the current user.** Ownership is checked in the query
itself (`where userId = session.user.id`), not after fetching. A filter applied
in application code after a broad read is a data leak waiting for a bug.
- Index columns used in WHERE, ORDER BY and JOIN
- Foreign keys with an explicit ON DELETE behaviour
- Always paginate or `.limit()` list queries — no unbounded reads
- Use joins instead of looping queries (N+1)
- Only parameterized queries. Never build SQL through string concatenation.
- Handle the error branch of every database call
## Migrations
- Schema changes go through checked-in migration files, never manual edits on a
running database
- Migrations must be reversible or explicitly documented as one-way
- The deployment step runs pending migrations before the new container serves
traffic — see `docs/DEPLOYMENT.md`
## Secrets
- Read configuration from `process.env`, never hardcode it
- Every new variable goes into `.env.example` with a dummy value in the same commit
- `NEXT_PUBLIC_` only for values that are safe in a browser bundle
+41 -21
View File
@@ -6,29 +6,49 @@ paths:
- "src/hooks/**"
---
# Frontend Development Rules
# Frontend Rules
## shadcn/ui First (MANDATORY)
- Before creating ANY UI component, check if shadcn/ui has it: `ls src/components/ui/`
- NEVER create custom implementations of: Button, Input, Select, Checkbox, Switch, Dialog, Modal, Alert, Toast, Table, Tabs, Card, Badge, Dropdown, Popover, Tooltip, Navigation, Sidebar, Breadcrumb
- If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes`
- Custom components are ONLY for business-specific compositions that internally use shadcn primitives
## shadcn/ui first (mandatory)
## Import Pattern
```tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
Pre-installed: `button`, `card`, `input`, `label`, `form`, `sonner`.
Before building any UI element, check `ls src/components/ui/`. If shadcn has the
component and it is not installed yet, install it — do not hand-write it:
```bash
npx shadcn@latest add dialog table tabs --yes
```
## Component Standards
- Use Tailwind CSS exclusively (no inline styles, no CSS modules)
- All components must be responsive (mobile 375px, tablet 768px, desktop 1440px)
- Implement loading states, error states, and empty states
- Use semantic HTML and ARIA labels for accessibility
- Keep components small and focused
- Use TypeScript interfaces for all props
Never write a custom Button, Input, Select, Checkbox, Switch, Dialog, Alert,
Table, Tabs, Card, Badge, Dropdown, Popover, Tooltip, Sidebar or Breadcrumb.
Custom components are only compositions of these primitives plus business logic.
## Auth Best Practices (Supabase)
- Use `window.location.href` for post-login redirect (not `router.push`)
- Always verify `data.session` exists before redirecting
- Always reset loading state in all code paths (success, error, finally)
## Tailwind v4
- **There is no `tailwind.config.ts`.** Theme tokens live in `src/app/globals.css`
under `@theme inline`. Add a design token there, not in a config file.
- Use semantic tokens (`bg-background`, `text-muted-foreground`, `border-border`),
not raw palette values like `bg-slate-900` — otherwise dark mode breaks.
- Tailwind classes only. No inline `style`, no CSS modules.
## Component Standards
- TypeScript interfaces for all props
- Responsive at 375px / 768px / 1440px
- Every data-driven component handles three states: loading, error, empty
- Semantic HTML and ARIA labels; keyboard reachable
- Server Components by default — add `'use client'` only when you need state,
effects or browser APIs
## Feedback and Theming
- Toasts via `sonner`: `import { toast } from 'sonner'`. The `Toaster` is already
mounted in `layout.tsx` — do not mount a second one.
- Dark mode is class-based via next-themes; never read the theme during render in
a way that differs between server and client (hydration mismatch)
## Forms
`react-hook-form` + `zod` through `@hookform/resolvers/zod`, using the shadcn
`form` component. The Zod schema is the single source of truth — the same schema
validates again on the server.
+54 -59
View File
@@ -1,75 +1,70 @@
# General Project Rules
## New Project Detection (MANDATORY)
Before starting ANY work, check if the project has been initialized:
1. Read `docs/PRD.md` - if it still contains placeholder text like "_Describe what you are building_", the project is NOT initialized
2. Read `features/INDEX.md` - if the features table is empty, no features have been defined
## Before any work: is the project initialized?
**If the project is not initialized:**
- Do NOT write any code or create any components
- Do NOT skip ahead to implementation
- Instead, tell the user: "This project hasn't been set up yet. Let's start by defining what you want to build. Run `/init` with a description of your idea (e.g. `/init I want to build a task management app`)."
- If the user already described their idea in the current message, run `/init` automatically with their description
1. Read `docs/PRD.md` — placeholder text like "_Describe what you are building_" means **not** initialized
2. Read `features/INDEX.md` — an empty feature table means no features defined
**If the project is initialized but the user requests a feature not yet in INDEX.md:**
- Guide them to run `/write-spec` first to create the feature spec before any implementation
**Not initialized:** do not write code or create components. Say:
> "Dieses Projekt ist noch nicht aufgesetzt. Starte mit `/init` und beschreibe kurz deine Idee
> (z. B. `/init Ich will eine Aufgabenverwaltung bauen`)."
If the user already described their idea in that same message, run `/init` with it directly.
**Initialized, but the requested feature is not in INDEX.md:** guide the user to `/write-spec` first.
## Feature Tracking
- All features are tracked in `features/INDEX.md` - read it before starting any work
- Feature specs live in `features/PROJ-X-feature-name.md`
- Feature IDs are sequential: check INDEX.md for the next available number
- One feature per spec file (Single Responsibility)
- Never combine multiple independent functionalities in one spec
## Git Conventions
- Commit format: `type(PROJ-X): description`
- Types: feat, fix, refactor, test, docs, deploy, chore
- Check existing features before creating new ones: `ls features/ | grep PROJ-`
- Check existing components before building: `git ls-files src/components/`
- Check existing APIs before building: `git ls-files src/app/api/`
- `features/INDEX.md` is the index — read it before starting anything
- Specs live in `features/PROJ-X-feature-name.md`, IDs are sequential
- One feature per spec file. Never combine independent functionality.
## Human-in-the-Loop
- Always ask for user approval before finalizing deliverables
- Present options using clear choices rather than open-ended questions
- Never proceed to the next workflow phase without user confirmation
## Status Flow
## Status Updates (MANDATORY - Write-Then-Verify)
After completing work on any feature, you MUST update tracking files. Follow this exact sequence:
`Roadmap` → `Planned` → `Architected` → `In Progress` → `In Review` → `Approved` → `Deployed`
1. **Read** the feature spec (`features/PROJ-X-*.md`) and `features/INDEX.md` BEFORE editing
2. **Write** your changes using the Edit tool — do NOT just describe what you would write
3. **Re-read** the file AFTER editing to verify the changes are actually present
4. **If changes are missing**, repeat step 2 — never claim updates were made without verifying
| Status | Set by |
|--------|--------|
| Roadmap | `/init` — feature identified, no spec file yet |
| Planned | `/write-spec` |
| Architected | `/architecture` |
| In Progress | `/frontend` or `/backend` |
| In Review | `/qa` starts |
| Approved | `/qa` passes with no Critical/High bugs |
| Deployed | `/deploy` |
**What to update in the feature spec:**
- Status field in the header (Planned → In Progress → In Review → Deployed)
- Implementation notes: what was built, what changed, any deviations from the original spec
- Bug fixes or design changes discovered during implementation
## Status Updates (write, then verify)
**What to update in `features/INDEX.md`:**
- Feature status column must match the feature spec header
- Valid statuses: Roadmap → Planned → Architected → In Progress → In Review → Approved → Deployed
- **Roadmap**: after `/init` — feature identified, no spec file yet
- **Planned**: after `/write-spec`
- **Architected**: after `/architecture`
- **In Progress**: after `/frontend` or `/backend` starts
- **In Review**: after `/qa` starts
- **Approved**: after `/qa` passes (no critical/high bugs)
- **Deployed**: after `/deploy`
1. **Read** the spec and `features/INDEX.md` before editing
2. **Write** with the Edit tool — describing the change in chat is not doing it
3. **Re-read** the file afterwards to confirm the change landed
4. If it did not land, repeat step 2
**NEVER do this:**
- Do NOT say "I've updated the feature spec" without actually calling the Edit tool
- Do NOT summarize changes in chat as a substitute for writing them to the file
- Do NOT skip updates because "it's obvious" or "minor"
Update in the spec: status header, implementation notes, deviations, bugs found.
Update in `INDEX.md`: the status column must match the spec header.
Never claim a file was updated without having called Edit and verified the result.
## File Handling
- ALWAYS read a file before modifying it - never assume contents from memory
- After context compaction, re-read files before continuing work
- When unsure about current project state, read `features/INDEX.md` first
- Run `git diff` to verify what has already been changed in this session
- Never guess at import paths, component names, or API routes - verify by reading
## Handoffs Between Skills
- After completing a skill, suggest the next skill to the user
- Format: "Next step: Run `/skillname` to [action]"
- Handoffs are always user-initiated, never automatic
- Read a file before modifying it — never work from memory
- After context compaction, re-read before continuing
- Verify import paths, component names and API routes by reading, never guess
- `git diff` shows what this session already changed
## Git
- Commit format: `type(PROJ-X): description` (feat, fix, refactor, test, docs, deploy, chore)
- Check what exists before building: `git ls-files src/components/`, `git ls-files src/app/api/`
## Human-in-the-Loop
- Ask for approval before finalizing a deliverable
- Offer clear choices instead of open-ended questions
- Never advance to the next phase without confirmation
- End each skill with: "Nächster Schritt: `/skillname` — [Aktion]"
## Language
Specs, PRD and acceptance criteria are written in **German**. Code, comments,
commit messages and these instructions stay **English**.
+60 -23
View File
@@ -1,36 +1,73 @@
---
paths:
- "src/app/api/**"
- "src/lib/auth*"
- "src/server/**"
- ".env*"
- "supabase/**"
- "next.config.*"
- "Dockerfile"
- "compose.yaml"
---
# Security Rules
## Secrets Management
- NEVER commit secrets, API keys, or credentials to git
- Use `.env.local` for local development (already in .gitignore)
- Use `NEXT_PUBLIC_` prefix ONLY for values safe to expose in browser
- Document all required env vars in `.env.local.example` with dummy values
## Authentication: Better Auth with passkeys (mandatory)
## Input Validation
- Validate ALL user input on the server side with Zod
- Never trust client-side validation alone
- Sanitize data before database insertion
Whenever a project needs authentication, it uses **Better Auth** with the
**passkey plugin**. Do not substitute another auth library, and do not build a
custom session/credential layer.
## Authentication
- Always verify authentication before processing API requests
- Use Supabase RLS as a second line of defense
- Implement rate limiting on authentication endpoints
```bash
npm install better-auth
# passkey support ships as a Better Auth plugin — enable it in the auth config
```
## Security Headers
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: origin-when-cross-origin
- Strict-Transport-Security with includeSubDomains
**Passkeys are the primary method.** The sign-in screen offers passkey first and
presents it as the default path. A password flow is permitted only as a fallback
for users whose device cannot do WebAuthn.
## Code Review Triggers
- Any changes to RLS policies require explicit user approval
- Any changes to authentication flow require explicit user approval
- Any new environment variables must be documented in .env.local.example
Requirements whenever auth is present:
- Passkey registration is offered during sign-up, not hidden in settings
- Existing password users are prompted to add a passkey after signing in
- The WebAuthn relying-party ID must match the deployment hostname. It is derived
from `NEXT_PUBLIC_APP_URL` / `BETTER_AUTH_URL` — a mismatch makes every passkey
fail verification, and it fails silently.
- Passkeys require HTTPS in production (localhost is exempt). The reverse proxy
terminates TLS — see `docs/DEPLOYMENT.md`.
- If the password fallback exists: rate-limit it, never reveal whether an account
exists, and store hashes only through Better Auth's own primitives
## Authorization
- Verify the session in every route handler before any work
- Authorization is per resource, not per route: having a session does not mean
owning the record. Scope the query by the session user.
- Never trust an ID coming from the client as proof of ownership
## Secrets
- Never commit secrets. `.env` is gitignored; only `.env.example` is committed.
- Every new variable is documented in `.env.example` with a dummy value
- `NEXT_PUBLIC_` only for values safe to expose in the browser bundle
- `BETTER_AUTH_SECRET` is generated per environment (`openssl rand -base64 32`)
and never shared between staging and production
## Input Handling
- Validate all input server-side with Zod, even when the client already did
- Never interpolate user input into SQL, shell commands or file paths
- Escape or avoid `dangerouslySetInnerHTML`
## Transport and Headers
Security headers are configured in `next.config.ts`: `X-Frame-Options: DENY`,
`X-Content-Type-Options: nosniff`, `Referrer-Policy: origin-when-cross-origin`,
`Strict-Transport-Security`. Verify they survive the reverse proxy after deploying.
## Changes That Need Explicit User Approval
- Any change to the authentication flow
- Weakening or removing the passkey requirement
- Any change to authorization or ownership checks
- New environment variables (must land in `.env.example` in the same commit)
+26 -10
View File
@@ -3,37 +3,53 @@
"deny": [
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git push --force-with-lease:*)",
"Read(**/.env*)",
"Read(**/.dev.vars*)",
"Read(**/.env)",
"Read(**/.env.local)",
"Read(**/.env.development)",
"Read(**/.env.production)",
"Read(**/.env.test)",
"Read(**/.env.*.local)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/secrets/**)",
"Read(**/credentials/**)",
"Read(**/.aws/**)",
"Read(**/.ssh/**)",
"Read(**/config/database.yml)",
"Read(**/config/credentials.json)",
"Read(**/.npmrc)",
"Read(**/.pypirc)",
"Edit(**/.env*)",
"Edit(**/.env)",
"Edit(**/.env.local)",
"Edit(**/.env.development)",
"Edit(**/.env.production)",
"Edit(**/.env.test)",
"Edit(**/.env.*.local)",
"Edit(**/secrets/**)",
"Edit(**/.ssh/**)"
],
"allow": [
"Bash(npm install)",
"Bash(npm ci)",
"Bash(npm run dev:*)",
"Bash(npm run build:*)",
"Bash(npm run lint:*)",
"Bash(npm run typecheck:*)",
"Bash(npm run start:*)",
"Bash(npm test:*)",
"Bash(npm run test:*)",
"Bash(npx shadcn@latest add:*)",
"Bash(npx playwright install:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(git tag:*)",
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(git status:*)",
"Bash(git ls-files:*)",
"Bash(lsof:*)",
"Bash(kill:*)",
"Bash(npx shadcn@latest add:*)",
"Bash(docker compose build:*)",
"Bash(docker compose up:*)",
"Bash(docker compose down:*)",
"Bash(docker compose ps:*)",
"Bash(docker compose logs:*)",
"Bash(ls:*)"
]
}
+97 -90
View File
@@ -1,127 +1,134 @@
---
name: architecture
description: Design PM-friendly technical architecture for features. No code, only high-level design decisions.
argument-hint: "feature-spec-path"
description: Design PM-friendly technical architecture for a feature. No code, only high-level decisions.
argument-hint: "PROJ-X or feature-spec-path"
user-invocable: true
---
# Solution Architect
# Architecture
## Role
You are a Solution Architect who translates feature specs into understandable architecture plans. Your audience is product managers and non-technical stakeholders.
You translate a feature spec into a design a non-technical stakeholder can follow —
and you make the binding technical decisions the developers will implement.
## CRITICAL Rule
NEVER write code or show implementation details:
- No SQL queries
- No TypeScript/JavaScript code
- No API implementation snippets
- Focus: WHAT gets built and WHY, not HOW in detail
## Hard rule: no code
## Before Starting
1. Read `features/INDEX.md` to understand project context
2. Verify the feature has a full spec — check that:
- The feature's status in INDEX.md is **"Planned"** (not "Roadmap")
- A spec file `features/PROJ-X-*.md` actually exists on disk
3. Check existing components: `git ls-files src/components/`
4. Check existing APIs: `git ls-files src/app/api/`
5. Read the feature spec the user references
No SQL, no TypeScript, no API snippets. Describe **what** gets built and **why**,
never **how** in detail. Structure and plain language only.
**If the feature status is "Roadmap" or no spec file exists:**
> "This feature doesn't have a spec yet. Run `/write-spec PROJ-X` first — the architecture design needs user stories and acceptance criteria to work from."
→ Stop here.
## 1. Load context
## Workflow
1. Read `features/INDEX.md`
2. Read `docs/PRD.md`
3. Read the feature spec
4. `git ls-files src/components/ src/app/api/` — what already exists
### 1. Read Feature Spec
- Read `/features/PROJ-X.md`
- Understand user stories + acceptance criteria
- Determine: Do we need backend? Or frontend-only?
**If the status is "Roadmap" or no spec file exists:**
> "PROJ-X hat noch keine Spec. Führe erst `/write-spec PROJ-X` aus — ohne User Stories
> und Acceptance Criteria kann ich das Design nicht entwerfen."
→ Stop.
### 2. Ask Clarifying Questions (if needed)
Use `AskUserQuestion` for:
- Do we need login/user accounts?
- Should data sync across devices? (localStorage vs database)
- Are there multiple user roles?
- Any third-party integrations?
## 2. Resolve the open decisions
### 3. Create High-Level Design
One question at a time, each with a recommendation.
#### A) Component Structure (Visual Tree)
Show which UI parts are needed:
### Data layer (decide this first — it shapes everything else)
This template deliberately ships **no** database or ORM. Decide per feature:
> "Wo sollen die Daten liegen?"
> Empfehlung: Ohne Nutzerkonten und ohne Gerätewechsel reicht localStorage. Sobald
> Daten einen Neustart überleben oder zwischen Nutzern geteilt werden müssen, braucht
> es eine Datenbank im Compose-Stack.
If a database is needed, also settle the ORM/driver, and record both in the
Technical Decisions table. Constraint: it must be self-hostable in the Docker
Compose stack — no managed-only services.
### Authentication
> "Braucht dieses Feature angemeldete Nutzer?"
If yes, the stack is fixed: **Better Auth with the passkey plugin**, passkey as the
primary method, password only as a fallback. Do not re-open that choice — note it
as a dependency and move on.
### Remaining questions
- Multiple user roles with different permissions?
- Third-party integrations?
- Anything with unusual performance or volume characteristics?
## 3. Write the design
Add a **Tech Design** section to the spec containing:
**A) Component structure** — a plain tree of the UI parts:
```
Main Page
+-- Input Area (add item)
+-- Board
| +-- "To Do" Column
| | +-- Task Cards (draggable)
| +-- "Done" Column
| +-- Task Cards (draggable)
+-- Empty State Message
Board-Seite
├── Eingabefeld (neue Aufgabe)
├── Spalte "Offen"
│ └── Aufgabenkarte (verschiebbar)
├── Spalte "Erledigt"
└── Leerzustand
```
#### B) Data Model (plain language)
Describe what information is stored:
**B) Data model** — in words, no schema:
```
Each task has:
- Unique ID
- Title (max 200 characters)
- Status (To Do or Done)
- Created timestamp
Eine Aufgabe hat:
- eindeutige ID
- Titel (max. 200 Zeichen)
- Status (offen / erledigt)
- Erstellzeitpunkt
Stored in: Browser localStorage (no server needed)
Gespeichert in: Browser-localStorage (kein Server nötig)
```
#### C) Tech Decisions (justified for PM)
Explain WHY specific tools/approaches are chosen in plain language.
**C) Tech decisions** — each justified in language a PM understands
#### D) Dependencies (packages to install)
List only package names with brief purpose.
**D) Dependencies** — package names with a one-line purpose each
### 4. Add Design to Feature Spec
Add a "Tech Design (Solution Architect)" section to `/features/PROJ-X.md`
## 4. Log the decisions
### 5. Log Technical Decisions
For every meaningful technical choice made during this session, add an entry to the **Technical Decisions** table in the spec's Decision Log:
- Storage approach (localStorage vs. database, and why)
- Package choices (why this library over alternatives)
- Data model decisions (key names, structure, constraints)
- API design choices (REST vs. server action, auth strategy)
- Any decision that a future developer might otherwise question
Every meaningful choice goes into the spec's **Technical Decisions** table:
**Format:**
```
| Decision | Rationale | Date |
| localStorage over Supabase | No user accounts needed; data is device-local | 2026-05-19 |
| Entscheidung | Begründung | Datum |
| localStorage statt Datenbank | Keine Nutzerkonten, Daten bleiben gerätelokal | 2026-08-09 |
```
If any questions came up during the design that couldn't be resolved, add them to the **Open Questions** section as `- [ ]` items.
Log at minimum: storage approach, ORM/driver if any, auth yes/no, package choices,
and anything a future developer would otherwise question.
### 5. User Review
- Present the design for review
- Ask: "Does this design make sense? Any questions?"
- Wait for approval before suggesting handoff
Anything unresolved goes to **Open Questions** as `- [ ]`.
## Checklist Before Completion
- [ ] Checked existing architecture via git
- [ ] Feature spec read and understood
- [ ] Component structure documented (visual tree, PM-readable)
- [ ] Data model described (plain language, no code)
- [ ] Backend need clarified (localStorage vs database)
- [ ] Tech decisions justified (WHY, not HOW)
## 5. Review
Present the design and ask:
> "Ergibt das Design so Sinn? Irgendwo Fragen oder Bauchschmerzen?"
Wait for approval.
## 6. Wrap up
- [ ] Existing components and APIs checked
- [ ] Component structure documented, PM-readable
- [ ] Data model in plain language, no code
- [ ] Data layer decided and justified
- [ ] Auth need clarified (Better Auth + passkey if yes)
- [ ] Dependencies listed
- [ ] Design added to feature spec file
- [ ] Technical Decisions logged in spec's Decision Log
- [ ] Any new Open Questions added to spec
- [ ] User has reviewed and approved
- [ ] `features/INDEX.md` status updated to "Architected"
- [ ] Technical Decisions table filled
- [ ] Open Questions recorded
- [ ] Design written into the spec and re-read to verify
- [ ] `features/INDEX.md` → **Architected**
- [ ] User approved
## Handoff
After approval, tell the user:
> "Design is ready! Next step: Run `/frontend` to build the UI components for this feature."
>
> If this feature needs backend work, you'll run `/backend` after frontend is done.
## Git Commit
> "Design steht. Nächster Schritt: `/frontend` für die UI-Komponenten."
> Braucht das Feature einen Server-Teil, folgt `/backend` danach.
## Commit
```
docs(PROJ-X): Add technical design for [feature name]
```
+54 -85
View File
@@ -1,113 +1,82 @@
---
name: backend
description: Build APIs, database schemas, and server-side logic with Supabase. Use after frontend is built.
argument-hint: "feature-spec-path"
description: Build route handlers, data access and Better Auth passkey flows. Use after frontend is built.
argument-hint: "PROJ-X or feature-spec-path"
user-invocable: true
---
# Backend Developer
# Backend
## Role
You are an experienced Backend Developer. You read feature specs + tech design and implement APIs, database schemas, and server-side logic using Supabase and Next.js.
You own the server-side phase for one feature. You do the interview and the review
with the user; the **Backend Developer** sub-agent does the implementation.
## Before Starting
1. Read `features/INDEX.md` for project context
2. Read the feature spec referenced by the user (including Tech Design section)
3. Check existing APIs: `git ls-files src/app/api/`
4. Check existing database patterns: `git log --oneline -S "CREATE TABLE" -10`
5. Check existing lib files: `ls src/lib/`
## 1. Load context
## Workflow
1. Read `features/INDEX.md`
2. Read the feature spec — Tech Design and the Technical Decisions table
3. `git ls-files src/app/api/` and `ls src/lib/`
### 1. Read Feature Spec + Design
- Understand the data model from Solution Architect
- Identify tables, relationships, and RLS requirements
- Identify API endpoints needed
**If the Tech Design does not name a data layer:**
> "Im Tech Design steht noch nicht, wo die Daten liegen sollen. Diese Vorlage bringt
> bewusst keine Datenbank mit — das entscheidet `/architecture` pro Projekt.
> Lass uns das dort klären, bevor ich Schema-Code schreibe."
→ Stop.
### 2. Ask Technical Questions
Use `AskUserQuestion` for:
- What permissions are needed? (Owner-only vs shared access)
- How do we handle concurrent edits?
- Do we need rate limiting for this feature?
- What specific input validations are required?
## 2. Clarify the open technical questions
### 3. Create Database Schema
- Write SQL for new tables in Supabase SQL Editor
- Enable Row Level Security on EVERY table
- Create RLS policies for all CRUD operations
- Add indexes on performance-critical columns (WHERE, ORDER BY, JOIN)
- Use foreign keys with ON DELETE CASCADE where appropriate
One at a time, each with a recommendation:
### 4. Create API Routes
- Create route handlers in `/src/app/api/`
- Implement CRUD operations
- Add Zod input validation on all POST/PUT endpoints
- Add proper error handling with meaningful messages
- Always check authentication (verify user session)
- Who may read and write these records? (owner only, shared, role-based)
- What happens on concurrent edits — last write wins, or conflict detection?
- Which validation rules beyond the obvious types?
- Does this feature need rate limiting?
- Does it need authentication? If yes: Better Auth with the passkey plugin is the
standard — see `.claude/rules/security.md`. Passkey primary, password only as a
fallback.
### 5. Connect Frontend
- Update frontend components to use real API endpoints
- Replace any mock data or localStorage with API calls
- Handle loading and error states
## 3. Set status
### 6. Write Integration Tests
For each API route created, write a Vitest integration test in `src/app/api/[route]/[route].test.ts`:
- Test the happy path (valid input → expected response)
- Test validation errors (invalid input → 400 with error message)
- Test authentication (unauthenticated request → 401)
- Test authorization (wrong user → 403)
- Run tests: `npm test`
Update `features/INDEX.md` and the spec header to **In Progress**. Verify by
re-reading.
### 7. User Review
- Walk user through the API endpoints created
- Show test results
- Ask: "Do the APIs work correctly? Any edge cases to test?"
## 4. Delegate implementation
## Context Recovery
If your context was compacted mid-task:
1. Re-read the feature spec you're implementing
2. Re-read `features/INDEX.md` for current status
3. Run `git diff` to see what you've already changed
4. Run `git ls-files src/app/api/` to see current API state
5. Continue from where you left off - don't restart or duplicate work
Invoke the **Backend Developer** sub-agent with:
- the spec path
- the data-layer decision from the Tech Design
- the answers from step 2
- which frontend components need wiring to real endpoints
## Output Format Examples
Wait for its report. Do not implement in parallel yourself.
### Database Migration
```sql
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
status TEXT CHECK (status IN ('todo', 'in_progress', 'done')) DEFAULT 'todo',
created_at TIMESTAMPTZ DEFAULT NOW()
);
## 5. Review with the user
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
Walk through the endpoints created and the test results, then:
> "Funktionieren die Endpunkte so? Fällt dir ein Randfall ein, den wir noch testen sollten?"
CREATE POLICY "Users see own tasks" ON tasks
FOR SELECT USING (auth.uid() = user_id);
Iterate by sending follow-up instructions to the same sub-agent.
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_status ON tasks(status);
```
## 6. Wrap up
## Production References
- See [database-optimization.md](../../../docs/production/database-optimization.md) for query optimization
- See [rate-limiting.md](../../../docs/production/rate-limiting.md) for rate limiting setup
## Checklist
See [checklist.md](checklist.md) for the full implementation checklist.
After completion, update tracking files:
- [ ] Feature spec updated with implementation notes
- [ ] `features/INDEX.md` status updated to "In Progress"
Verify before declaring done:
- [ ] `npm test`, `npm run lint`, `npm run typecheck`, `npm run build` pass
- [ ] Zod validation on every write endpoint
- [ ] Session checked in every handler; ownership scoped inside the query
- [ ] If auth: passkey is the primary flow
- [ ] Indexes on filtered/sorted columns, list queries bounded
- [ ] Every new env var is in `.env.example`
- [ ] No secrets in source
- [ ] Frontend wired to real endpoints, mock data removed
- [ ] Spec updated with implementation notes (written **and** re-read)
- [ ] `features/INDEX.md` status is In Progress
- [ ] User approved
## Handoff
After completion:
> "Backend is done! Next step: Run `/qa` to test this feature against its acceptance criteria."
## Git Commit
> "Backend steht. Nächster Schritt: `/qa` zum Testen gegen die Acceptance Criteria."
## Commit
```
feat(PROJ-X): Implement backend for [feature name]
```
-33
View File
@@ -1,33 +0,0 @@
# Backend Implementation Checklist
## Core Checklist
- [ ] Checked existing tables/APIs via git before creating new ones
- [ ] Database tables created in Supabase
- [ ] Row Level Security enabled on ALL new tables
- [ ] RLS policies created for SELECT, INSERT, UPDATE, DELETE
- [ ] Indexes created on performance-critical columns
- [ ] Foreign keys set with appropriate ON DELETE behavior
- [ ] All planned API endpoints implemented in `/src/app/api/`
- [ ] Authentication verified (no access without valid session)
- [ ] Input validation with Zod on all POST/PUT requests
- [ ] Meaningful error messages with correct HTTP status codes
- [ ] No TypeScript errors in API routes
- [ ] All endpoints tested manually
- [ ] No hardcoded secrets in source code
- [ ] Frontend connected to real API endpoints
- [ ] User has reviewed and approved
## Verification (run before marking complete)
- [ ] `npm run build` passes without errors
- [ ] All acceptance criteria from feature spec addressed in API
- [ ] All API endpoints return correct status codes (test with curl or browser)
- [ ] `features/INDEX.md` status updated to "In Progress"
- [ ] Code committed to git
## Performance Checklist
- [ ] All frequently filtered columns have indexes
- [ ] No N+1 queries (use Supabase joins instead of loops)
- [ ] All list queries use `.limit()`
- [ ] Zod validation on all write endpoints
- [ ] Slow queries cached where appropriate (optional for MVP)
- [ ] Rate limiting on public-facing APIs (optional for MVP)
+96 -86
View File
@@ -1,114 +1,124 @@
---
name: deploy
description: Deploy to Vercel with production-ready checks, error tracking, and security headers setup.
argument-hint: "feature-spec-path or 'to Vercel'"
description: Deploy the Docker Compose stack to a server, with pre-flight checks and rollback.
argument-hint: "PROJ-X or 'production'"
user-invocable: true
---
# DevOps Engineer
# Deploy
## Role
You are an experienced DevOps Engineer handling deployment, environment setup, and production readiness.
Self-hosted Docker Compose. There is no Vercel, no managed platform. The stack runs
one app container; TLS and routing are handled by a reverse proxy already present
on the host.
## Before Starting
1. Read `features/INDEX.md` to know what is being deployed
2. Check QA status in the feature spec
3. Verify no Critical/High bugs exist in QA results
4. If QA has not been done, tell the user: "Run `/qa` first before deploying."
## 1. Gate
## Workflow
1. Read `features/INDEX.md` — what is being shipped
2. Read the feature spec's QA section
### 1. Pre-Deployment Checks
- [ ] `npm run build` succeeds locally
- [ ] `npm run lint` passes
- [ ] QA Engineer has approved the feature (check feature spec)
- [ ] No Critical/High bugs in test report
- [ ] All environment variables documented in `.env.local.example`
- [ ] No secrets committed to git
- [ ] All database migrations applied in Supabase (if applicable)
- [ ] All code committed and pushed to remote
**No QA section:** → "Für PROJ-X gibt es noch kein QA-Ergebnis. Führe erst `/qa` aus." → Stop.
**Critical or High bugs open:** → name them and stop. Those get fixed first.
### 2. Vercel Setup (first deployment only)
## 2. Pre-flight
> **Prerequisite (manual, do this first):** The user must create a Vercel account in the browser before this step — go to [vercel.com](https://vercel.com) and sign up (e.g. "Sign up with GitHub"). Account creation and login are browser/OAuth steps that cannot be automated by the skill. Also ensure the repo is pushed to a GitHub remote so Vercel can connect to it.
Run and show the results — do not proceed on a failure:
Guide the user through:
- [ ] Create Vercel project: `npx vercel` or via vercel.com
- [ ] Connect GitHub repository for auto-deploy on push
- [ ] Add all environment variables from `.env.local.example` in Vercel Dashboard
- [ ] Build settings: Framework Preset = Next.js (auto-detected)
- [ ] Configure domain (or use default `*.vercel.app`)
```bash
npm run lint
npm run typecheck
npm test
npm run build
```
### 3. Deploy
- Push to main branch → Vercel auto-deploys
- Or manual: `npx vercel --prod`
- Monitor build in Vercel Dashboard
Then check:
- [ ] Every env var used in code is in `.env.example` with a dummy value
- [ ] No secrets in the diff (`git diff --cached`)
- [ ] Pending migrations are committed
- [ ] Everything committed and pushed to the remote the server pulls from
### 4. Post-Deployment Verification
- [ ] Production URL loads correctly
- [ ] Deployed feature works as expected
- [ ] Database connections work (if applicable)
- [ ] Authentication flows work (if applicable)
- [ ] No errors in browser console
- [ ] No errors in Vercel function logs
## 3. First deployment only
### 5. Production-Ready Essentials
Walk the user through this once. It happens on the server, over SSH.
For first deployment, guide the user through these setup guides:
**Prerequisites the user provides manually:** a server with Docker and the Compose
plugin, a reverse proxy (nginx, Caddy, Traefik) terminating TLS, and a DNS record
pointing at it.
**Error Tracking (5 min):** See [error-tracking.md](../../../docs/production/error-tracking.md)
**Security Headers (copy-paste):** See [security-headers.md](../../../docs/production/security-headers.md)
**Performance Check:** See [performance.md](../../../docs/production/performance.md)
**Database Optimization:** See [database-optimization.md](../../../docs/production/database-optimization.md)
**Rate Limiting (optional):** See [rate-limiting.md](../../../docs/production/rate-limiting.md)
```bash
git clone <repo-url> /srv/<app-name> && cd /srv/<app-name>
cp .env.example .env
```
### 6. Post-Deployment Bookkeeping
- Update feature spec: Add deployment section with production URL and date
- Update `features/INDEX.md`: Set status to **Deployed**
- Create git tag: `git tag -a v1.X.0-PROJ-X -m "Deploy PROJ-X: [Feature Name]"`
- Push tag: `git push origin v1.X.0-PROJ-X`
Then edit `.env`:
- `NEXT_PUBLIC_APP_URL` — the real public URL. **If auth is used, this must match
the domain exactly**: the WebAuthn relying-party ID comes from this hostname, and
a mismatch makes every passkey fail without a useful error.
- `APP_PORT` — the port the reverse proxy forwards to
- `APP_BIND` — leave at `127.0.0.1` so the container is not exposed directly
- `BETTER_AUTH_SECRET` — `openssl rand -base64 32`, unique per environment
## Common Issues
```bash
docker compose up -d --build
curl -fsS http://127.0.0.1:${APP_PORT:-3000}/api/health
```
### Build fails on Vercel but works locally
- Check Node.js version (Vercel may use different version)
- Ensure all dependencies are in package.json (not just devDependencies)
- Review Vercel build logs for specific error
Point the reverse proxy at `127.0.0.1:$APP_PORT` and make sure it forwards
`X-Forwarded-Proto` and `X-Forwarded-Host`, otherwise redirects and WebAuthn origin
checks break.
### Environment variables not available
- Verify vars are set in Vercel Dashboard (Settings → Environment Variables)
- Client-side vars need `NEXT_PUBLIC_` prefix
- Redeploy after adding new env vars (they don't apply retroactively)
Passkeys need HTTPS in production — confirm TLS works before testing auth.
### Database connection errors
- Verify Supabase URL and anon key in Vercel env vars
- Check RLS policies allow the operations being attempted
- Verify Supabase project is not paused (free tier pauses after inactivity)
## 4. Subsequent deployments
## Rollback Instructions
If production is broken:
1. **Immediate:** Vercel Dashboard → Deployments → Click "..." on previous working deployment → "Promote to Production"
2. **Fix locally:** Debug the issue, `npm run build`, commit, push
3. Vercel auto-deploys the fix
On the server:
## Full Deployment Checklist
- [ ] Pre-deployment checks all pass
- [ ] Vercel build successful
- [ ] Production URL loads and works
- [ ] Feature tested in production environment
- [ ] No console errors, no Vercel log errors
- [ ] Error tracking setup (Sentry or alternative)
- [ ] Security headers configured in next.config
- [ ] Lighthouse score checked (target > 90)
- [ ] Feature spec updated with deployment info
- [ ] `features/INDEX.md` updated to Deployed
- [ ] Git tag created and pushed
- [ ] User has verified production deployment
```bash
./scripts/update.sh
```
It pulls the branch, rebuilds, restarts, waits for the healthcheck, and rolls back
to the previous commit if the new container never turns healthy. `--force` rebuilds
without a new commit; `--branch <name>` deploys a different branch.
If a data layer exists, migrations run before the new container serves traffic —
see `docs/DEPLOYMENT.md`.
## 5. Verify
- [ ] Public URL loads over HTTPS
- [ ] `/api/health` returns `{"status":"ok"}`
- [ ] The deployed feature works end to end
- [ ] Sign-in works and offers passkey first (if auth exists)
- [ ] No console errors, no errors in `docker compose logs app`
- [ ] Security headers survived the proxy (DevTools → Network → Response Headers)
## 6. Bookkeeping
- Add a Deployment section to the spec: URL, date, commit SHA
- `features/INDEX.md` → **Deployed** (write, then re-read to verify)
- Tag: `git tag -a v1.X.0-PROJ-X -m "Deploy PROJ-X: [name]"` and push it
## Rollback
`scripts/update.sh` rolls back automatically on an unhealthy container. Manually:
```bash
git reset --hard <previous-sha>
docker compose up -d --build
```
## Common failures
| Symptom | Cause |
|---------|-------|
| Build works locally, fails in Docker | A dependency sits in devDependencies but is needed at build time |
| Env var undefined at runtime | Not in `.env` on the server, or missing `NEXT_PUBLIC_` for client use |
| Container healthy, proxy returns 502 | Proxy points at the wrong port, or `APP_BIND` is not reachable from it |
| Passkeys fail after deploy | `NEXT_PUBLIC_APP_URL` does not match the real domain, or TLS is missing |
| Redirect loop | Proxy not forwarding `X-Forwarded-Proto` |
## Commit
## Git Commit
```
deploy(PROJ-X): Deploy [feature name] to production
- Production URL: https://your-app.vercel.app
- Deployed: YYYY-MM-DD
```
+54 -65
View File
@@ -1,95 +1,84 @@
---
name: frontend
description: Build UI components with React, Next.js, Tailwind CSS, and shadcn/ui. Use after architecture is designed.
argument-hint: "feature-spec-path"
description: Build UI components with React, Next.js, Tailwind v4 and shadcn/ui. Use after architecture is designed.
argument-hint: "PROJ-X or feature-spec-path"
user-invocable: true
---
# Frontend Developer
# Frontend
## Role
You are an experienced Frontend Developer. You read feature specs + tech design and implement the UI using React, Next.js, Tailwind CSS, and shadcn/ui.
You own the UI phase for one feature. You do the interview and the review with the
user; the **Frontend Developer** sub-agent does the implementation.
## Before Starting
1. Read `features/INDEX.md` for project context
2. Read the feature spec referenced by the user (including Tech Design section)
3. Check installed shadcn/ui components: `ls src/components/ui/`
4. Check existing custom components: `ls src/components/*.tsx 2>/dev/null`
5. Check existing hooks: `ls src/hooks/ 2>/dev/null`
6. Check existing pages: `ls src/app/`
## 1. Load context
## Workflow
1. Read `features/INDEX.md`
2. Read the feature spec, including its Tech Design section
3. `ls src/components/ui/` — installed shadcn components
4. `git ls-files src/components/` — existing custom components
### 1. Read Feature Spec + Design
- Understand the component architecture from Solution Architect
- Identify which shadcn/ui components to use
- Identify what needs to be built custom
**If the spec has no Tech Design section:**
> "Für PROJ-X gibt es noch kein technisches Design. Führe zuerst `/architecture` aus —
> ohne Komponentenstruktur baue ich sonst am Entwurf vorbei."
→ Stop.
### 2. Clarify Design Requirements
First check for a project design system: `cat docs/design-system.md 2>/dev/null`
## 2. Clarify design
If `docs/design-system.md` exists → read it and apply its colors, typography, and component guidelines throughout. Do not ask the user about design choices already covered there.
Check for a design system: `cat docs/design-system.md 2>/dev/null`
If it does not exist, check for other design files: `ls -la design/ mockups/ assets/ 2>/dev/null`
If it exists, follow it and do not ask about anything it already answers.
Otherwise check `ls design/ mockups/ 2>/dev/null`, and if nothing exists, ask the
user — one question at a time, each with a recommendation:
If no design specs exist at all, ask the user:
- Visual style preference (modern/minimal, corporate, playful, dark mode)
- Reference designs or inspiration URLs
- Brand colors (hex codes or use Tailwind defaults)
- Layout preference (sidebar, top-nav, centered)
- Visual style (modern/minimal, corporate, playful)
- Brand colors, or Tailwind/shadcn defaults
- Layout (sidebar, top-nav, centered)
- Interactions beyond the basics (drag & drop, animations)
### 3. Clarify Technical Questions
- Mobile-first or desktop-first?
- Any specific interactions needed (hover effects, animations, drag & drop)?
- Accessibility requirements beyond defaults (WCAG 2.1 AA)?
## 3. Set status
### 4. Implement Components
- Create components in `/src/components/`
- ALWAYS use shadcn/ui for standard UI elements (check `src/components/ui/` first!)
- If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes`
- Only create custom components as compositions of shadcn primitives
- Use Tailwind CSS for all styling
Update `features/INDEX.md` and the spec header to **In Progress**. Verify by
re-reading.
### 5. Integrate into Pages
- Add components to pages in `/src/app/`
- Set up routing if needed
- Connect to backend APIs or localStorage as specified in tech design
## 4. Delegate implementation
### 6. User Review
- Tell the user to test in browser (localhost:3000)
- Ask: "Does the UI look right? Any changes needed?"
- Iterate based on feedback
Invoke the **Frontend Developer** sub-agent with:
- the spec path
- the design decisions from step 2
- the component list from the Tech Design
- the shadcn components already installed
## Context Recovery
If your context was compacted mid-task:
1. Re-read the feature spec you're implementing
2. Re-read `features/INDEX.md` for current status
3. Run `git diff` to see what you've already changed
4. Run `git ls-files src/components/ | head -20` to see current component state
5. Continue from where you left off - don't restart or duplicate work
Wait for its report. Do not implement in parallel yourself.
## After Completion: Backend & QA Handoff
## 5. Review with the user
Check the feature spec - does this feature need backend?
Present the sub-agent's report, then:
> "Läuft unter http://localhost:3000. Schau es dir an — passt das so?"
**Backend needed if:** Database access, user authentication, server-side logic, API endpoints, multi-user data sync
Iterate on feedback by sending follow-up instructions to the same sub-agent, so it
keeps its context.
**No backend if:** localStorage only, no user accounts, no server communication
## 6. Wrap up
If backend is needed:
> "Frontend is done! This feature needs backend work. Next step: Run `/backend` to build the APIs and database."
Verify before declaring done:
- [ ] `npm run lint`, `npm run typecheck`, `npm run build` pass
- [ ] Every acceptance criterion has a UI counterpart
- [ ] shadcn used everywhere it could be; no hand-written duplicates
- [ ] Loading, error and empty states exist
- [ ] Responsive at 375 / 768 / 1440px
- [ ] Spec updated with implementation notes (written **and** re-read)
- [ ] `features/INDEX.md` status is In Progress
- [ ] User approved
If no backend needed:
> "Frontend is done! Next step: Run `/qa` to test this feature against its acceptance criteria."
## Handoff
## Checklist
See [checklist.md](checklist.md) for the full implementation checklist.
Does the feature need a server side (data storage, auth, APIs, cross-device sync)?
After completion, update tracking files:
- [ ] Feature spec updated with implementation notes
- [ ] `features/INDEX.md` status updated to "In Progress"
- Yes → "Frontend steht. Nächster Schritt: `/backend` für APIs und Datenhaltung."
- No → "Frontend steht. Nächster Schritt: `/qa` zum Testen gegen die Acceptance Criteria."
## Commit
## Git Commit
```
feat(PROJ-X): Implement frontend for [feature name]
```
-38
View File
@@ -1,38 +0,0 @@
# Frontend Implementation Checklist
Before marking frontend as complete:
## shadcn/ui
- [ ] Checked shadcn/ui for EVERY UI component needed
- [ ] No custom duplicates of shadcn components created
- [ ] Missing shadcn components installed via `npx shadcn@latest add`
## Existing Code
- [ ] Checked existing project components via `git ls-files src/components/`
- [ ] Reused existing components where possible
## Design
- [ ] Design preferences clarified with user (if no mockups)
- [ ] Component architecture from Solution Architect followed
## Implementation
- [ ] All planned components implemented
- [ ] All components use Tailwind CSS (no inline styles, no CSS modules)
- [ ] Loading states implemented (spinner/skeleton during data fetches)
- [ ] Error states implemented (user-friendly error messages)
- [ ] Empty states implemented ("No data yet" messages)
## Quality
- [ ] Responsive: Mobile (375px), Tablet (768px), Desktop (1440px)
- [ ] Accessibility: Semantic HTML, ARIA labels, keyboard navigation
- [ ] TypeScript: No errors (`npm run build` passes)
- [ ] ESLint: No warnings (`npm run lint`)
## Verification (run before marking complete)
- [ ] `npm run build` passes without errors
- [ ] All acceptance criteria from feature spec addressed in UI
- [ ] `features/INDEX.md` status updated to "In Progress"
## Completion
- [ ] User has reviewed and approved the UI in browser
- [ ] Code committed to git
+38 -84
View File
@@ -1,104 +1,58 @@
---
name: help
description: Context-aware guide that tells you where you are in the workflow and what to do next. Use anytime you're unsure.
description: Context-aware guide — where the project stands and what to do next.
argument-hint: "optional question"
user-invocable: true
---
# Project Help Guide
# Help
You are a helpful project assistant. Your job is to analyze the current project state and tell the user exactly where they are and what to do next.
Work out where the project actually stands, then say what to do next. Be concrete,
be short.
## When Invoked
## 1. Read the state
### Step 1: Analyze Current State
1. `docs/PRD.md` — still the empty template, or filled in?
2. `features/INDEX.md` — which features exist, at which status?
3. For the active feature, check which sections its spec already has:
Tech Design (`/architecture`), QA Test Results (`/qa`), Deployment (`/deploy`)
4. Quick scan: `ls src/components/ui/`, `ls src/app/api/ 2>/dev/null`, `ls tests/ 2>/dev/null`
Read these files to understand where the project stands:
Trust the files over the status column if they disagree — and say so when they do.
1. **Check PRD:** Read `docs/PRD.md`
- Is it still the empty template? → Project not initialized yet
- Is it filled out? → Project has been set up
## 2. Name the next step
2. **Check Feature Index:** Read `features/INDEX.md`
- No features listed? → No features created yet
- Features exist? → Check their statuses
| State | Next step |
|-------|-----------|
| PRD is the empty template | `/init` mit einer kurzen Beschreibung der Idee |
| PRD filled, no features | `/write-spec` für das erste Feature |
| Status Roadmap | `/write-spec PROJ-X` — Spec schreiben |
| Status Planned | `/architecture PROJ-X` — technisches Design |
| Status Architected | `/frontend PROJ-X` — UI bauen |
| Frontend done, needs a server side | `/backend PROJ-X` |
| Implemented, no QA section | `/qa PROJ-X` |
| Status Approved | `/deploy PROJ-X` |
| Everything deployed | `/write-spec` für das nächste Feature aus der PRD-Roadmap |
3. **Check Feature Specs:** For each feature in INDEX.md, check if:
- Tech Design section exists (added by /architecture)
- QA Test Results section exists (added by /qa)
- Deployment section exists (added by /deploy)
## 3. Answer the question, if there was one
4. **Check Codebase:** Quick scan of what's been built
- `ls src/components/*.tsx 2>/dev/null` → Custom components
- `ls src/app/api/ 2>/dev/null` → API routes
- `ls src/components/ui/` → Installed shadcn components
If the user asked something specific, answer that **first**, then show the status.
Common ones: which skills exist, how to add a feature, how to customize the
template (CLAUDE.md, `.claude/rules/`, `.claude/skills/`), how deployment works
(Docker Compose, `scripts/update.sh` — no Vercel).
### Step 2: Determine Next Action
## Output
Based on the state analysis, determine what the user should do next:
### Stand
_Zwei, drei Sätze: wo das Projekt steht._
**If PRD is empty template:**
> Your project hasn't been initialized yet.
> Run `/init` with a description of what you want to build.
> Example: `/init I want to build a task management app for small teams`
### Features
_Tabelle aus INDEX.md._
**If PRD exists but no features:**
> Your PRD is set up but no features have been created yet.
> Run `/write-spec` to create your first feature specification.
### Nächster Schritt
_Der eine wichtigste Schritt, mit dem exakten Kommando._
**If features exist with status "Planned" (no Tech Design):**
> Feature PROJ-X is ready for architecture design.
> Run `/architecture` to create the technical design for `features/PROJ-X-name.md`
### Sonst möglich
_Kurze Liste weiterer sinnvoller Aktionen._
**If features have Tech Design but no implementation:**
> Feature PROJ-X has a tech design and is ready for implementation.
> Run `/frontend` to build the UI for `features/PROJ-X-name.md`
> (If backend is needed, run `/backend` after frontend is done)
**If features are implemented but no QA:**
> Feature PROJ-X is implemented and ready for testing.
> Run `/qa` to test `features/PROJ-X-name.md` against its acceptance criteria.
**If features have passed QA but aren't deployed:**
> Feature PROJ-X has passed QA and is ready for deployment.
> Run `/deploy` to deploy to production.
**If all features are deployed:**
> All current features are deployed! You can:
> - Run `/write-spec` to spec out a new feature
> - Check `docs/PRD.md` for planned features not yet specified
### Step 3: Answer User Questions
If the user asked a specific question (via arguments), answer it in the context of the current project state. Common questions:
- "What skills are available?" → List all available skills with brief descriptions
- "How do I add a new feature?" → Explain `/write-spec` workflow (or `/init` if the project isn't set up yet)
- "How do I customize this template?" → Point to CLAUDE.md, rules/, skills/
- "What's the project structure?" → Explain the directory layout
- "How do I deploy?" → Explain `/deploy` workflow and prerequisites
## Output Format
Always respond with this structure:
### Current Project Status
_Brief summary of where the project stands_
### Features Overview
_Table of features and their current status (from INDEX.md)_
### Recommended Next Step
_The single most important thing to do next, with the exact command_
### Other Available Actions
_Other things the user could do right now_
If the user asked a specific question, answer that FIRST, then show the status overview.
## Important
- Be concise and actionable
- Always give the exact command to run
- Reference specific file paths
- Don't explain the framework architecture in detail unless asked
- Focus on: "Here's where you are, here's what to do next"
Give exact commands and real file paths. Do not explain the framework unless asked.
+80 -106
View File
@@ -1,146 +1,120 @@
---
name: init
description: Initialize a new project. Creates the PRD and a prioritized feature map. Run once at the very start of a new project. If a PRD is empty (raw template stucture) use this skill to plan out the project togehter with the user.
description: Initialize a new project — PRD and prioritized feature map. Run once at the very start.
argument-hint: "description of what you want to build"
user-invocable: true
---
# Project Initializer
# Init
## Role
You are an experienced Product Strategist. Your job is to help the user articulate their project vision and break it down into a prioritized feature map — before any code is written.
You are a product strategist. You interview the user until the project is genuinely
understood, then write the PRD and break it into a feature map. No code, no specs.
## The Grill Me Principle
Interview the user relentlessly until you reach a **complete shared understanding** of the project. Follow these rules strictly:
## Grill Me principle
- **One question at a time** — never list multiple questions
- **Always provide a recommended answer** — the user confirms or corrects it
- **Follow the conversation** — open new branches based on answers, don't follow a fixed script
- **Explore before asking** — if a question can be answered by reading existing files, read them first
- **No fixed question limit** — stop when you truly understand the project, not after N questions
- **One question at a time** — never a list
- **Always offer a recommended answer** — the user confirms or corrects it
- **Follow the conversation** — open branches based on answers, no fixed script
- **Read before asking** — if a file answers it, read the file
- **No question limit** — stop when you understand, not after N questions
## Before Starting
1. Read `docs/PRD.md` — check if it's still the empty template
2. Read `features/INDEX.md` — check if features already exist
## Before starting
**If the project is already initialized** (PRD is filled out and not the empty template):
→ Tell the user: "This project is already initialized. Use `/write-spec` to create a feature spec, or `/refine PROJ-X` to update an existing one."
→ Stop here.
Read `docs/PRD.md` and `features/INDEX.md`.
## Interview Phase
**If the PRD is already filled in:**
> "Dieses Projekt ist bereits initialisiert. Nutze `/write-spec` für eine neue
> Feature-Spec oder `/refine PROJ-X`, um eine bestehende zu überarbeiten."
→ Stop.
Start the conversation based on the argument the user provided. If they described their idea, acknowledge it and ask your first clarifying question about the most important open point. If no argument was given, ask:
## Interview
> "What do you want to build, and what problem does it solve?"
> My recommendation: Start with the user pain — what frustrates people today that your product will fix?
Start from whatever the user passed as argument. If nothing, ask:
> "Was willst du bauen, und welches Problem löst es?"
> Empfehlung: Fang beim Schmerz an — was nervt heute, das dein Produkt beseitigt?
Cover these topics through natural conversation (not as a checklist):
- Core problem being solved
- Primary target users and their specific pain points
- Must-have features for MVP vs. nice-to-have later
- Existing alternatives / competitors — what's different here?
- Constraints: timeline, budget, team size
- Success metrics: how do you know this product worked?
- Non-goals: what are you explicitly NOT building in this version?
Cover through natural conversation: the core problem, the target users and their
specific pain, MVP versus later, existing alternatives and the difference, constraints
(time, budget, team), success metrics, and explicit non-goals.
### Mandatory: Backend Decision (ask before building the feature map)
This question MUST be resolved before you create the feature map — it determines the entire architecture and feature list.
### Mandatory: persistence
Ask:
> "Does the app need to store data persistently or sync between users/devices?"
> My recommendation: Yes — most apps need at least local persistence. If multiple users or cross-device sync is needed, a backend is required.
Resolve before the feature map — it shapes the architecture.
**If yes → follow up:**
> "Should we use Supabase (the template's built-in backend: PostgreSQL + Auth + Storage) or keep it frontend-only with localStorage?"
> My recommendation: Supabase — if users need accounts or data needs to survive a browser refresh, local storage won't be enough.
> "Müssen Daten dauerhaft gespeichert oder zwischen Nutzern und Geräten synchronisiert werden?"
> Empfehlung: Ja — sobald Daten einen Browser-Neustart überleben sollen, reicht
> localStorage nicht mehr.
**If Supabase is chosen:**
- Add **"Supabase Infrastructure Setup"** as **PROJ-1, P0** in the feature map
- All features that require auth, data storage, or file uploads must list PROJ-1 as a dependency
- This feature covers: project setup, environment variables, database schema outline, auth configuration
This template ships **no** database. If persistence is needed, note it as a
constraint; `/architecture` picks the concrete data layer per feature. The only
hard requirement: it must run self-hosted in the Docker Compose stack.
**If frontend-only (localStorage):**
- No infrastructure feature needed
- Note "No backend — localStorage only" in the PRD Constraints section
### Mandatory: authentication
### Mandatory: Design System (ask before building the feature map)
Ask:
> "Do you have an existing design system, brand guidelines, or UI reference I should follow?"
> My recommendation: Even a rough color palette and font preference saves a lot of back-and-forth later.
> "Brauchen Nutzer eigene Konten?"
**Three ways the user can provide it:**
1. **File upload** — an HTML or Markdown file with colors, typography, component styles
2. **Manual input** — the user describes it directly (e.g. "dark theme, Inter font, blue primary #2563EB")
3. **None** — use the template's default (Tailwind + shadcn/ui defaults)
If yes, add **"Auth-Grundlage (Better Auth + Passkeys)"** as **PROJ-1, P0** with no
dependencies. Every feature that needs a logged-in user depends on PROJ-1.
The stack is fixed: Better Auth with the passkey plugin, passkey primary, password
only as a fallback. Do not offer alternatives — record it and move on.
**If a design system is provided:**
- Save it to `docs/design-system.md` (create the file with the provided content or a structured summary)
- Add a note in `docs/PRD.md` under Constraints: "Design system: see `docs/design-system.md`"
- The `/frontend` skill will read this file when building UI components
### Mandatory: design system
## After the Interview: Create the PRD
> "Hast du ein bestehendes Design-System, Brand Guidelines oder eine UI-Referenz?"
> Empfehlung: Schon eine grobe Farbpalette und Schriftwahl spart später viel Hin und Her.
Once you have a complete understanding, write `docs/PRD.md` with:
- **Vision:** 2-3 sentences — what it is and why it matters
- **Target Users:** Who they are, their specific needs and pain points
- **Core Features (Roadmap):** Prioritized table (P0 = MVP, P1 = next, P2 = later)
- **Success Metrics:** Measurable outcomes
- **Constraints:** Timeline, team, budget, technical limitations
- **Non-Goals:** What will NOT be built in this version
If provided (as a file or described in words), save it to `docs/design-system.md`
and reference it in the PRD constraints. `/frontend` reads it. If nothing, the
Tailwind/shadcn defaults apply.
Present the draft PRD to the user for review before saving. Apply feedback, then save.
## Write the PRD
## After PRD: Create the Feature Map
Fill `docs/PRD.md`, in German:
- **Vision** — 2–3 sentences
- **Zielgruppe** — who, and their concrete pain
- **Roadmap** — prioritized table, P0 = MVP, P1 = next, P2 = later
- **Erfolgsmetriken** — measurable
- **Rahmenbedingungen** — time, team, budget, technical limits, persistence decision
- **Nicht-Ziele** — explicitly out for this version
Apply Single Responsibility to break the roadmap into individual features:
- Each feature = ONE testable, deployable unit
- Identify dependencies between features
- Assign recommended build order (respecting dependencies)
- Assign priority: P0 = MVP, P1 = next, P2 = later
Present the draft, apply feedback, then save.
**What each feature entry in `features/INDEX.md` contains:**
- Feature ID (PROJ-1, PROJ-2, ...)
- Feature name
- One-line description
- Priority (P0/P1/P2)
- Dependencies (which other features it needs, or "None")
- Status: Roadmap
## Build the feature map
Present the feature map to the user:
> "I've identified X features. Here's the breakdown and recommended build order:"
Single Responsibility: each feature is one testable, deployable unit.
Apply feedback, then update `features/INDEX.md` and the "Next Available ID" line.
Each entry in `features/INDEX.md` carries: ID, name, one-line description,
priority, dependencies, status **Roadmap**.
## What NOT to do
- Do NOT create individual `features/PROJ-X-*.md` spec files — that is `/write-spec`'s job
- Do NOT write code or make technical decisions
- Do NOT ask multiple questions at once
- Do NOT stop early — keep going until you have full clarity on the project
Present it as a build order that respects dependencies:
> "Ich habe X Features identifiziert. Hier die Aufteilung und die empfohlene Reihenfolge:"
## Checklist Before Completion
- [ ] PRD fully filled out (Vision, Target Users, Roadmap, Metrics, Constraints, Non-Goals)
- [ ] Backend decision resolved (Supabase vs. localStorage)
- [ ] If Supabase: "Supabase Infrastructure Setup" added as PROJ-1, P0, no dependencies
- [ ] If Supabase: all data/auth-dependent features list PROJ-1 as dependency
- [ ] If frontend-only: noted in PRD Constraints
- [ ] Design system decision resolved
- [ ] If design system provided: saved to `docs/design-system.md` and referenced in PRD
- [ ] Every feature respects Single Responsibility
- [ ] Dependencies between features documented
- [ ] All features added to `features/INDEX.md` with status "Roadmap"
- [ ] "Next Available ID" updated in INDEX.md
- [ ] Build order recommended
- [ ] User has reviewed and approved PRD and feature map
Apply feedback, then write `features/INDEX.md` and update "Next Available ID".
Re-read the file to verify.
## Do not
- Create `features/PROJ-X-*.md` spec files — that is `/write-spec`
- Write code or pick concrete libraries beyond the fixed auth stack
- Ask several questions at once
- Stop before you actually understand the project
## Wrap up
- [ ] PRD complete (Vision, Zielgruppe, Roadmap, Metriken, Rahmenbedingungen, Nicht-Ziele)
- [ ] Persistence resolved and noted
- [ ] Auth resolved; if yes, PROJ-1 auth feature added and depended on
- [ ] Design system resolved; if provided, saved to `docs/design-system.md`
- [ ] Every feature is a single responsibility, dependencies documented
- [ ] `features/INDEX.md` written with status Roadmap, next ID updated, verified
- [ ] User approved PRD and feature map
## Handoff
After user approval:
> "Project setup complete. Run `/write-spec` to start speccing your first feature: **[recommended first feature name]** (PROJ-1)."
> "Projekt steht. Nächster Schritt: `/write-spec` für das erste Feature: **[Name]** (PROJ-1)."
## Commit
## Git Commit
```
feat: Initialize project — PRD and feature map
- Created docs/PRD.md with vision, target users, and roadmap
- Added X features to features/INDEX.md
```
+75 -131
View File
@@ -1,159 +1,103 @@
---
name: qa
description: Test features against acceptance criteria, find bugs, and perform security audit. Use after implementation is done.
argument-hint: "feature-spec-path"
description: Test a feature against acceptance criteria, find bugs, audit security. Use after implementation is done.
argument-hint: "PROJ-X or feature-spec-path"
user-invocable: true
---
# QA Engineer
# QA
## Role
You are an experienced QA Engineer AND Red-Team Pen-Tester. You test features against acceptance criteria, identify bugs, and audit for security vulnerabilities.
You own the test phase for one feature. The **QA Engineer** sub-agent does the
testing; you set up its context, then present and prioritize the results with the
user.
## Before Starting
1. Read `features/INDEX.md` for project context
2. Read the feature spec referenced by the user
3. Check recently implemented features for regression testing: `git log --oneline --grep="PROJ-" -10`
4. Check recent bug fixes: `git log --oneline --grep="fix" -10`
5. Check recently changed files: `git log --name-only -5 --format=""`
## 1. Load context
### Check Playwright Browser Installation
Run: `npx playwright install --dry-run 2>&1 | head -5`
1. Read `features/INDEX.md` — note which features are already **Deployed**
(those are the regression surface)
2. Read the feature spec
3. `git log --oneline -10` — what changed recently
If browsers are not installed, tell the user:
> "Playwright browsers need to be installed once. I'll do this now — it downloads ~300MB of browser binaries."
> Then run: `npx playwright install chromium`
> This is a one-time setup per machine. After cloning the repo, always run this once before E2E tests.
## 2. Set status
## Workflow
Update `features/INDEX.md` and the spec header to **In Review**. Verify by
re-reading.
### 1. Read Feature Spec
- Understand ALL acceptance criteria
- Understand ALL documented edge cases
- Understand the tech design decisions
- Note any dependencies on other features
## 3. Delegate testing
### 2. Manual Testing
Test the feature systematically in the browser:
- Test EVERY acceptance criterion (mark pass/fail)
- Test ALL documented edge cases
- Test undocumented edge cases you identify
- Cross-browser: Chrome, Firefox, Safari
- Responsive: Mobile (375px), Tablet (768px), Desktop (1440px)
Invoke the **QA Engineer** sub-agent with:
- the spec path
- the list of deployed features to regression-check
- any areas the developer flagged as risky
### 3. Security Audit (Red Team)
Think like an attacker:
- Test authentication bypass attempts
- Test authorization (can user X access user Y's data?)
- Test input injection (XSS, SQL injection via UI inputs)
- Test rate limiting (rapid repeated requests)
- Check for exposed secrets in browser console/network tab
- Check for sensitive data in API responses
Wait for its report.
### 4. Regression Testing
Verify existing features still work:
- Check features listed in `features/INDEX.md` with status "Deployed"
- Test core flows of related features
- Verify no visual regressions on shared components
## 4. Write results into the spec
### 5. Run Automated Tests
Run existing test suites before manual testing:
```bash
npm test # Vitest: integration tests for API routes
npm run test:e2e # Playwright: E2E tests from previous QA runs
Append this to the feature spec — not to a separate file:
```markdown
---
## QA Test Results
**Getestet:** YYYY-MM-DD
**Tester:** QA Engineer (AI)
### Acceptance Criteria
- [x] AC-1: [Kriterium]
- [ ] AC-2: [Kriterium] — BUG-1
### Edge Cases
- [x] EC-1: [Randfall] — korrekt behandelt
### Security-Audit
- [x] Ohne Session kein Zugriff
- [x] Kein Zugriff auf fremde Datensätze
- [x] Passkey ist der primäre Anmeldeweg
- [x] Eingaben validiert, keine Injection
### Gefundene Bugs
#### BUG-1: [Titel]
- **Severity:** Critical | High | Medium | Low
- **Reproduktion:** 1. … 2. … 3. …
- **Erwartet:** …
- **Tatsächlich:** …
### Zusammenfassung
- **Acceptance Criteria:** X von Y bestanden
- **Bugs:** N gesamt (C critical, H high, M medium, L low)
- **Produktionsreif:** JA / NEIN
```
Note any failures — these are regressions and must be treated as High bugs.
### 6. Write Unit Tests
Before E2E tests, identify and test isolated logic with Vitest. Place tests **co-located** next to the source file (e.g. `src/hooks/useFeature.test.ts` next to `src/hooks/useFeature.ts`):
Re-read the file afterwards to confirm it landed.
**What to unit test (evaluate each):**
- Custom hooks with non-trivial logic (e.g. `useKanbanStorage`: localStorage read/write, error fallback)
- Pure utility/transformation functions (e.g. drag-and-drop reorder logic)
- Form validation logic (if extracted from components)
## 5. Review with the user
**What NOT to unit test:**
- Pure presentational components with no logic
- Logic already fully covered by E2E tests
Present: criteria passed/failed, bugs by severity, security findings, verdict.
Then ask:
> "Welche Bugs sollen zuerst behoben werden?"
For each unit test:
- Test the happy path
- Test error paths and edge cases (e.g. corrupt input, empty state)
- Mock only external dependencies (localStorage, fetch) — not internal logic
## 6. Wrap up
Run to confirm all pass: `npm test`
### 7. Write E2E Tests
For each acceptance criterion that passed manual testing, write a Playwright test in `tests/PROJ-X-feature-name.spec.ts`:
- One `test()` per acceptance criterion
- Tests describe the user journey in plain language
- Run to confirm all pass: `npm run test:e2e`
These tests become the permanent regression suite for this feature.
### 8. Document Results
- Add QA Test Results section to the feature spec file (NOT a separate file)
- Use the template from [test-template.md](test-template.md)
### 9. User Review
Present test results with clear summary:
- Total acceptance criteria: X passed, Y failed
- Bugs found: breakdown by severity
- Security audit: findings
- Production-ready recommendation: YES or NO
Ask: "Which bugs should be fixed first?"
## Context Recovery
If your context was compacted mid-task:
1. Re-read the feature spec you're testing
2. Re-read `features/INDEX.md` for current status
3. Check if you already added QA results to the feature spec: search for "## QA Test Results"
4. Run `git diff` to see what you've already documented
5. Continue testing from where you left off - don't re-test passed criteria
## Bug Severity Levels
- **Critical:** Security vulnerabilities, data loss, complete feature failure
- **High:** Core functionality broken, blocking issues
- **Medium:** Non-critical functionality issues, workarounds exist
- **Low:** UX issues, cosmetic problems, minor inconveniences
## Important
- NEVER fix bugs yourself - that is for Frontend/Backend skills
- Focus: Find, Document, Prioritize
- Be thorough and objective: report even small bugs
## Production-Ready Decision
- **READY:** No Critical or High bugs remaining
- **NOT READY:** Critical or High bugs exist (must be fixed first)
## Checklist
- [ ] Feature spec fully read and understood
- [ ] All acceptance criteria tested (each has pass/fail)
- [ ] All documented edge cases tested
- [ ] Additional edge cases identified and tested
- [ ] Cross-browser tested (Chrome, Firefox, Safari)
- [ ] Responsive tested (375px, 768px, 1440px)
- [ ] Security audit completed (red-team perspective)
- [ ] Regression test on related features
- [ ] Every bug documented with severity + steps to reproduce
- [ ] Screenshots added for visual bugs
- [ ] Unit tests written for non-trivial hooks and utility functions (`npm test` passes)
- [ ] E2E tests written for all passing acceptance criteria (`npm run test:e2e` passes)
- [ ] QA section added to feature spec file
- [ ] User has reviewed results and prioritized bugs
- [ ] Production-ready decision made
- [ ] `features/INDEX.md` status updated to "In Review" (at QA start)
- [ ] `features/INDEX.md` status updated to "Approved" (if production-ready) OR kept "In Review" (if bugs remain)
- [ ] Every acceptance criterion has an individual pass/fail
- [ ] Documented and undocumented edge cases tested
- [ ] Security audit done from a red-team angle
- [ ] Regression check against deployed features
- [ ] Unit tests for non-trivial logic (`npm test` passes)
- [ ] E2E test per passing criterion (`npm run test:e2e` passes)
- [ ] QA section written into the spec and verified
- [ ] `features/INDEX.md` → **Approved** if no Critical/High, otherwise stays **In Review**
## Handoff
If production-ready:
> "All tests passed! Status updated to **Approved**. Next step: Run `/deploy` to deploy this feature to production."
If bugs found:
> "Found [N] bugs ([severity breakdown]). Status remains **In Review**. The developer needs to fix these before deployment. After fixes, run `/qa` again."
- Clean → "Alle Tests bestanden, Status **Approved**. Nächster Schritt: `/deploy`."
- Bugs → "[N] Bugs gefunden ([Aufschlüsselung]). Status bleibt **In Review**.
Erst `/frontend` bzw. `/backend` zum Beheben, danach nochmal `/qa`."
## Commit
## Git Commit
```
test(PROJ-X): Add QA test results for [feature name]
```
-56
View File
@@ -1,56 +0,0 @@
# QA Test Results Template
Add this section to the END of the feature spec `/features/PROJ-X.md`:
```markdown
---
## QA Test Results
**Tested:** YYYY-MM-DD
**App URL:** http://localhost:3000
**Tester:** QA Engineer (AI)
### Acceptance Criteria Status
#### AC-1: [Criterion Name]
- [x] Sub-criterion passed
- [ ] BUG: Sub-criterion failed (describe what went wrong)
#### AC-2: [Criterion Name]
- [x] All sub-criteria passed
### Edge Cases Status
#### EC-1: [Edge Case Name]
- [x] Handled correctly
#### EC-2: [Edge Case Name]
- [ ] BUG: Not handled (describe expected vs actual behavior)
### Security Audit Results
- [x] Authentication: Cannot access without login
- [x] Authorization: Users cannot access other users' data
- [x] Input validation: XSS attempts blocked
- [x] Rate limiting: Excessive requests handled
- [ ] BUG: [Security issue description]
### Bugs Found
#### BUG-1: [Bug Title]
- **Severity:** Critical | High | Medium | Low
- **Steps to Reproduce:**
1. Go to [page]
2. Do [action]
3. Expected: [what should happen]
4. Actual: [what actually happens]
- **Screenshot:** [if visual bug]
- **Priority:** Fix before deployment | Fix in next sprint | Nice to have
### Summary
- **Acceptance Criteria:** X/Y passed
- **Bugs Found:** N total (C critical, H high, M medium, L low)
- **Security:** [Pass / Issues found]
- **Production Ready:** YES / NO
- **Recommendation:** [Deploy / Fix bugs first]
```
+63 -77
View File
@@ -1,113 +1,99 @@
---
name: refine
description: Always use when the user wants to discuss an exsiting feature or specification. Open an existing feature spec to improve, extend, or fundamentally challenge it. Pass the feature ID as argument (e.g. /refine PROJ-2).
description: Reopen an existing feature spec to improve, extend or fundamentally challenge it. Pass the feature ID.
argument-hint: "PROJ-X"
user-invocable: true
---
# Feature Spec Refiner
# Refine
## Role
You are an experienced Product Manager reviewing a live spec. Your job is to improve, extend, or fundamentally challenge the spec based on what the user tells you.
You are a product manager reviewing a live spec. Improve it, extend it, or challenge
it — depending on what brought the user back.
## Before Starting
1. Read the feature spec `features/PROJ-X-*.md` — understand the full current state
2. Read `features/INDEX.md` — understand dependencies, status, and context
3. Read `docs/PRD.md` — keep the project vision in mind
## Before starting
**If no argument was provided** (no PROJ-X ID given):
> "Which feature spec would you like to refine?" — list all existing features from INDEX.md.
1. Read `features/PROJ-X-*.md` — the full current state
2. Read `features/INDEX.md` — status and dependencies
3. Read `docs/PRD.md` — keep the vision in view
**If the PROJ-X ID doesn't exist**: tell the user and list existing features.
**No argument given:** ask which spec, listing the features from INDEX.md.
**Unknown ID:** say so and list what exists.
## Opening Question (ALWAYS ask this first)
> "What brought you back to this spec?"
## Always open with this
This answer determines everything. Listen carefully — it will tell you which of the three paths to take.
> "Was bringt dich zurück zu dieser Spec?"
## Three Paths
The answer decides the path.
### Path 1: Something Changed
*Trigger: "scope changed", "we got user feedback", "business logic is different", "stakeholder changed the requirements"*
## Three paths
Run a targeted interview on the affected areas only:
- What specifically changed?
- Which user stories are affected?
- Which acceptance criteria need to be updated or removed?
- Do any edge cases change?
- Do dependencies change?
- Does the Out of Scope section need updating? (something previously excluded is now included, or vice versa)
### Path 1 — Something changed
*"Scope hat sich geändert", "wir haben Nutzerfeedback", "die Anforderung ist anders"*
### Path 2: Implementation Revealed Gaps
*Trigger: "during implementation we found...", "the backend doesn't support...", "we didn't think about X scenario"*
Targeted interview on the affected areas only: what exactly changed, which user
stories and criteria are affected, which edge cases shift, whether dependencies
move, and whether Out of Scope needs updating in either direction.
Focus on making the spec tighter:
- What specific scenario was missing?
- Should this become a new acceptance criterion or edge case?
- Does this change any existing criteria?
- Are there related gaps we should close now while we're here?
### Path 2 — Implementation revealed a gap
*"Bei der Umsetzung kam raus, dass…", "an den Fall haben wir nicht gedacht"*
### Path 3: Fundamental Challenge
*Trigger: "I'm not sure this feature is right", "maybe we should rethink this", "this might actually be two features"*
Tighten the spec: which scenario was missing, does it become a new acceptance
criterion or an edge case, does it invalidate an existing criterion, and are there
adjacent gaps worth closing now.
Challenge the entire spec from first principles:
- What assumption is being questioned?
- Is the user story still the right framing?
- Should this feature be split? If so, what are the two features?
- Should it be merged with another feature?
- What would the absolute minimal version of this feature look like?
- What moves to Out of Scope as a result of this challenge?
### Path 3 — Fundamental challenge
*"Ich bin nicht sicher, ob das Feature so richtig ist", "das sind vielleicht zwei Features"*
If the feature should be split: create the new spec file using the `/write-spec` workflow, and update `features/INDEX.md` accordingly.
Challenge from first principles: which assumption is in doubt, is the user story
still the right framing, should this split or merge, what is the minimal version,
and what moves to Out of Scope as a result.
## The Grill Me Principle
Same as in `/init` and `/write-spec`:
- **One question at a time** — never list multiple questions
- **Always provide a recommended answer** — the user confirms or corrects it
- **Follow the conversation** — don't follow a fixed script
- **Explore codebase first** if it can answer a question
- **Stop when you have full clarity** on what needs to change
If it should split: create the new spec via the `/write-spec` workflow and update
`features/INDEX.md`.
## After the Interview: Update the Spec
## Grill Me principle
Make the changes to `features/PROJ-X-*.md`. After saving, re-read the file to verify the changes are present.
One question at a time, always with a recommendation, following the conversation
rather than a script. Read the codebase before asking anything it can answer. Stop
when the change is fully clear.
### Maintain the Decision Log and Open Questions
## Update the spec
**Close resolved Open Questions:**
For any `- [ ]` items in Open Questions that are now answered, mark them as `- [x]` and add a brief resolution note:
Edit `features/PROJ-X-*.md`, then re-read it to verify the change landed.
**Close resolved open questions** — mark `- [x]` with the resolution:
```
- [x] Should we support bulk delete? → No, deferred to P1 (2026-05-19)
- [x] Sollen wir Massenlöschen unterstützen? → Nein, verschoben auf P1 (2026-08-09)
```
**Log new decisions:**
Any decision made during this refinement session belongs in the Decision Log. Add to the relevant sub-section (Product or Technical) with rationale and date. Decisions made here are often the most important — they reflect real-world feedback changing the original plan.
**Log new decisions** in the Entscheidungslog with rationale and date. Decisions
made here matter most — they are reality correcting the original plan.
**Add new Open Questions:**
If the refinement surfaced questions that couldn't be resolved now, add them as `- [ ]` items.
**Add new open questions** as `- [ ]` if anything stays unresolved.
## Update Tracking Files
- Update `features/INDEX.md` if status or dependencies changed
- Update `docs/PRD.md` if the roadmap is affected
## Update tracking
## Checklist Before Completion
- [ ] Opening question asked and path determined
- [ ] All interview questions resolved
- [ ] Spec file updated and verified (re-read after editing)
- [ ] Out of Scope updated if scope boundaries changed
- [ ] Resolved Open Questions marked as `- [x]` with resolution note
- [ ] New decisions logged in Decision Log with rationale
- [ ] New Open Questions added if anything remains unresolved
- [ ] `features/INDEX.md` updated if status or dependencies changed
- [ ] `docs/PRD.md` updated if roadmap affected
- [ ] User has reviewed the changes
- `features/INDEX.md` if status or dependencies changed
- `docs/PRD.md` if the roadmap is affected
## Wrap up
- [ ] Opening question asked, path identified
- [ ] Interview resolved
- [ ] Spec updated and re-read to verify
- [ ] Out of Scope adjusted if boundaries moved
- [ ] Resolved questions marked `- [x]` with a note
- [ ] New decisions logged with rationale
- [ ] INDEX.md / PRD updated where affected
- [ ] User reviewed the changes
## Handoff
Depends on the path taken:
- Path 1 or 2: "Spec updated. Continue with the next step in your workflow."
- Path 3 (split): "New spec created for PROJ-X. Run `/architecture` to design the technical approach."
## Git Commit
- Path 1 or 2 → "Spec aktualisiert. Mach mit deinem nächsten Workflow-Schritt weiter."
- Path 3 with a split → "Neue Spec für PROJ-Y angelegt. Nächster Schritt: `/architecture`."
## Commit
```
feat(PROJ-X): Refine feature specification — [brief reason]
feat(PROJ-X): Refine feature specification — [reason]
```
+86 -119
View File
@@ -1,159 +1,126 @@
---
name: write-spec
description: Write a full feature spec for a feature. Works for features already on the roadmap (status "Roadmap" from /init) and for features added later. Pass a feature name or PROJ-X ID as argument.
argument-hint: "feature name or PROJ-X ID"
description: Write a full feature spec — user stories, acceptance criteria, edge cases. Pass a feature name or PROJ-X ID.
argument-hint: "feature name or PROJ-X"
user-invocable: true
---
# Feature Spec Writer
# Write Spec
## Role
You are an experienced Product Manager. Your job is to turn a feature idea into a complete, testable specification — with user stories, acceptance criteria, and edge cases.
You are a product manager turning one feature idea into a complete, testable
specification. No code, no technical decisions — that is `/architecture`'s job.
## The Grill Me Principle
Interview the user until you reach a **complete shared understanding** of the feature. Rules:
## Grill Me principle
- **One question at a time** — never list multiple questions
- **Always provide a recommended answer** — the user confirms or corrects it
- **Follow the conversation** — open new branches, resolve dependencies between decisions one by one
- **Explore before asking** — if a question can be answered by reading the codebase, read it first
- **No fixed question limit** — stop when you truly understand the feature, not after N questions
- **One question at a time** — never a list
- **Always offer a recommended answer**
- **Follow the conversation**, resolve dependencies between decisions as they surface
- **Read the codebase before asking** anything it can answer
- **No question limit** — stop when you understand the feature
## Before Starting
1. Read `docs/PRD.md` — understand the project vision and target users
2. Read `features/INDEX.md` — see existing features, find the next available PROJ-X ID, check for duplicates
3. Check existing components: `git ls-files src/components/`
4. Check existing APIs: `git ls-files src/app/api/`
## Before starting
**If the project has not been initialized** (PRD is still the empty template):
> "The project hasn't been set up yet. Run `/init` first to define the project vision and feature map."
→ Stop here.
1. Read `docs/PRD.md` and `features/INDEX.md`
2. `git ls-files src/components/ src/app/api/`
**If no argument was provided**, ask: "Which feature would you like to spec out?" and list all features with status "Roadmap" from INDEX.md.
**If the PRD is still the empty template:**
> "Das Projekt ist noch nicht aufgesetzt. Führe erst `/init` aus." → Stop.
## Three Entry Points
**If no argument:** list all features with status Roadmap and ask which one.
### Entry Point A: Feature exists in INDEX.md with status "Roadmap"
The feature was identified during `/init`. Proceed directly to the Interview Phase.
## Three entry points
### Entry Point B: Feature does NOT exist in INDEX.md yet
The feature was forgotten during `/init` or is being added later. Before the full interview, quickly clarify:
- What is the feature called?
- What priority? (P0 = MVP, P1 = next, P2 = later) — provide a recommendation based on PRD context
- Does it depend on any existing features?
| Situation | What to do |
|-----------|------------|
| Status **Roadmap** in INDEX.md | Go straight to the interview |
| **Not in INDEX.md** | Clarify name, priority (with a recommendation) and dependencies, add it with the next PROJ-X ID and status Roadmap, then interview |
| Status **Planned** or higher | "PROJ-X hat schon eine Spec. Nutze `/refine PROJ-X`." → Stop |
Add it to `features/INDEX.md` with status "Roadmap" and the next available PROJ-X ID, then continue directly to the Interview Phase — no separate skill run needed.
## Interview
### Entry Point C: Feature already has a spec (status "Planned" or higher)
> "This feature already has a spec. Use `/refine PROJ-X` to update it."
→ Stop here.
Start from the PRD and the INDEX entry. First question targets the biggest open
point about this specific feature. Cover through conversation:
## Interview Phase
Start with what you know from `docs/PRD.md` and the feature entry in INDEX.md. Your first question should target the most important open point about this specific feature.
Cover these topics through natural conversation (not as a checklist):
- Who specifically uses this feature? (be precise — refer to the user types from the PRD)
- What is the core user action / job-to-be-done?
- What does success look like from the user's perspective?
- What are the must-have behaviors for MVP?
- What are the validation rules and constraints?
- Error states: what happens when things go wrong?
- Empty states: what does the user see before they have any data?
- Who exactly uses it (refer to the PRD's user types)
- The core action — the job to be done
- What success looks like from the user's side
- Must-have behaviour for the MVP
- Validation rules and limits
- Error states: what happens when things go wrong
- Empty states: what the user sees before any data exists
- Edge cases: concurrent edits, network failure, invalid input, permission boundaries
- Dependencies on other features (auth, data, etc.)?
- Performance or security requirements?
- Dependencies on other features (auth, data)
- Performance or security requirements
**For edge cases, always be concrete:**
- "What happens when the user submits an empty form?"
- "What if two users edit the same record simultaneously?"
- "What should happen if the API call times out?"
Be concrete about edge cases: "Was passiert, wenn zwei Nutzer denselben Datensatz
gleichzeitig bearbeiten?" beats "Gibt es Randfälle?"
## After the Interview: Write the Spec
## Write the spec
Use [template.md](template.md) to create the feature spec:
- Use the PROJ-X ID already in INDEX.md (or the one assigned in Entry Point B)
- Save to `features/PROJ-X-feature-name.md` (kebab-case filename)
Use `template.md`. Save to `features/PROJ-X-feature-name.md` (kebab-case).
**German**, per the project language rule.
**Populate Out of Scope, Decision Log, and Open Questions while the interview is fresh:**
Three sections carry the memory of this interview — do not skip them:
- **Out of Scope** — explicitly list everything that came up in the interview but was consciously excluded from this feature. Reference other features by ID where relevant (e.g. "Bulk delete — deferred to PROJ-5"). This section is critical for developer handoffs: without it, developers don't know what NOT to build.
- **Out of Scope** — everything discussed and consciously excluded, referencing
other features by ID ("Massenlöschen — verschoben auf PROJ-5"). Without it,
developers do not know what *not* to build.
- **Produkt-Entscheidungen** — every scoping and UX decision with its rationale
- **Offene Fragen** — anything unresolved, as `- [ ]` so it stays visible
Present the draft, apply feedback, then save.
## Acceptance criteria format
- **Product Decisions** — log every conscious scoping or UX decision made during the interview, with the rationale. Examples: "Why limit to X items?", "Why this user role and not another?", "Why include/exclude this edge case?"
- **Open Questions** — log anything that couldn't be resolved during the interview (pending user research, dependency on another team, unclear requirements). Mark as `- [ ]` so they're visible as unresolved.
Do not skip these sections — they are the memory of the spec interview.
Present the draft spec to the user for review. Apply feedback, then save.
## After Saving: Update Tracking Files
Update `features/INDEX.md`:
- Change the feature's status from "Roadmap" to "Planned"
- If Entry Point B: also update the "Next Available ID" line
Update `docs/PRD.md`:
- Update the status column in the roadmap table for this feature (if listed there)
## Feature Granularity (Single Responsibility)
Each spec = ONE testable, deployable unit.
**Never combine:**
- Multiple independent functionalities
- CRUD for different entities
- User functions + admin functions
- Different UI screens or areas
**Split when:**
1. Can it be tested independently? → Own spec
2. Can it be deployed independently? → Own spec
3. Does it target a different user role? → Own spec
4. Is it a separate UI screen? → Own spec
**Document dependencies:**
```markdown
## Dependencies
- Requires: PROJ-1 (User Authentication) — for logged-in user checks
```
## Important
- NEVER write code — that is for Frontend/Backend skills
- NEVER make technical decisions — that is for the Architecture skill
- Focus: WHAT the feature does (not HOW)
## Acceptance Criteria Format
Always write acceptance criteria in German using the Angenommen/Wenn/Dann format:
German, Angenommen/Wenn/Dann — one testable statement per line:
```
- [ ] Angenommen [Vorbedingung], wenn [Aktion], dann [Ergebnis]
```
Examples:
- [ ] Angenommen der Nutzer ist eingeloggt, wenn er ein leeres Formular abschickt, dann wird für jedes Pflichtfeld eine Validierungsfehlermeldung angezeigt
- [ ] Angenommen eine Aufgabe existiert, wenn der Nutzer auf „Löschen" klickt, dann erscheint ein Bestätigungsdialog bevor die Aufgabe entfernt wird
- [ ] Angenommen die API ist nicht erreichbar, wenn der Nutzer das Formular abschickt, dann wird eine Fehlermeldung angezeigt und die Eingabe bleibt erhalten
- [ ] Angenommen der Nutzer ist angemeldet, wenn er ein leeres Formular abschickt, dann erscheint pro Pflichtfeld eine Validierungsmeldung
- [ ] Angenommen eine Aufgabe existiert, wenn der Nutzer auf „Löschen" klickt, dann erscheint erst ein Bestätigungsdialog
- [ ] Angenommen die API antwortet nicht, wenn der Nutzer speichert, dann wird eine Fehlermeldung gezeigt und die Eingabe bleibt erhalten
This format ensures every criterion is unambiguous and directly testable by QA.
Each one must be directly testable by `/qa` without interpretation.
## Checklist Before Completion
- [ ] At least 3–5 user stories defined
- [ ] Out of Scope filled in (everything discussed but excluded, with references to other features where applicable)
- [ ] Every acceptance criterion uses Given/When/Then format
- [ ] Product Decisions logged with rationale
- [ ] Open Questions logged for anything unresolved
- [ ] At least 3–5 edge cases documented
- [ ] Feature ID assigned (PROJ-X)
- [ ] File saved to `features/PROJ-X-feature-name.md`
- [ ] `features/INDEX.md` updated (status: Roadmap → Planned; next ID updated if Entry Point B)
- [ ] `docs/PRD.md` roadmap table updated if applicable
- [ ] User has reviewed and approved the spec
## Granularity (Single Responsibility)
One spec = one testable, deployable unit. Split when it targets a different user
role, a different screen, or can be tested and shipped on its own. Never combine
CRUD for different entities, or user plus admin functions.
Document dependencies explicitly:
```markdown
## Abhängigkeiten
- Benötigt PROJ-1 (Auth-Grundlage) — für die Prüfung auf angemeldete Nutzer
```
## After saving
- `features/INDEX.md`: status Roadmap → **Planned** (and next ID if newly added)
- `docs/PRD.md`: update the roadmap status column if the feature is listed there
- Re-read both to verify
## Wrap up
- [ ] 3–5 user stories
- [ ] Out of Scope filled in with references
- [ ] Every criterion in Angenommen/Wenn/Dann form
- [ ] Produkt-Entscheidungen logged with rationale
- [ ] Offene Fragen logged
- [ ] 3–5 edge cases documented
- [ ] Saved as `features/PROJ-X-feature-name.md`
- [ ] INDEX.md and PRD updated and verified
- [ ] User approved
## Handoff
> "Spec is ready. Run `/architecture` to design the technical approach for PROJ-X."
## Git Commit
> "Spec steht. Nächster Schritt: `/architecture` für das technische Design von PROJ-X."
## Commit
```
feat(PROJ-X): Write feature specification for [feature name]
```
+38 -41
View File
@@ -1,63 +1,60 @@
# PROJ-X: Feature Name
# PROJ-X: Feature-Name
## Status: Planned
**Created:** YYYY-MM-DD
**Last Updated:** YYYY-MM-DD
**Status:** Planned
**Erstellt:** YYYY-MM-DD
**Zuletzt geändert:** YYYY-MM-DD
## Dependencies
- None
## Abhängigkeiten
- Keine
## User Stories
- As a [user type], I want to [action] so that [goal]
- Als [Nutzertyp] möchte ich [Aktion], um [Ziel] zu erreichen
## Out of Scope
<!-- What this feature explicitly does NOT cover. Critical for developer handoffs. -->
- _Example: Bulk operations (deferred to P1 — PROJ-X)_
- _Example: Admin view of other users' data (separate admin feature)_
<!-- Was dieses Feature bewusst NICHT abdeckt. Kritisch für die Übergabe an die Entwicklung. -->
- _Beispiel: Massenbearbeitung — verschoben auf PROJ-5_
- _Beispiel: Admin-Sicht auf fremde Daten — eigenes Feature_
## Acceptance Criteria
**Format:** Angenommen [Vorbedingung] / Wenn [Aktion] / Dann [Ergebnis]
- [ ] Angenommen [Vorbedingung], wenn [Aktion], dann [Ergebnis]
- [ ] Angenommen [Vorbedingung], wenn [Aktion], dann [Ergebnis]
<!-- Format: Angenommen [Vorbedingung], wenn [Aktion], dann [Ergebnis] -->
- [ ] Angenommen …, wenn …, dann …
- [ ] Angenommen …, wenn …, dann …
## Edge Cases
- What happens when...?
- How do we handle...?
- Was passiert, wenn …?
- Wie behandeln wir …?
## Technical Requirements (optional)
- Performance: < 200ms response time
- Security: Authentication required
- Browser Support: Chrome, Firefox, Safari
## Technische Anforderungen (optional)
- Performance: …
- Sicherheit: Anmeldung erforderlich (Better Auth, Passkey primär)
- Browser: Chrome, Firefox, Safari
## Open Questions
<!-- Unresolved questions from the spec interview. Close them in /refine when answered. -->
- [ ] Question 1
## Offene Fragen
<!-- Ungeklärtes aus dem Interview. Wird in /refine geschlossen. -->
- [ ] Frage 1
## Decision Log
<!-- Record of conscious decisions made and why. Added to by /write-spec and /architecture. -->
## Entscheidungslog
### Product Decisions
<!-- Added by /write-spec -->
| Decision | Rationale | Date |
|----------|-----------|------|
| _Example: Limit to 3 tags per item_ | _Keeps UI simple for MVP; can be raised later_ | YYYY-MM-DD |
### Produkt-Entscheidungen
<!-- von /write-spec -->
| Entscheidung | Begründung | Datum |
|--------------|------------|-------|
| _Beispiel: max. 3 Tags pro Eintrag_ | _Hält die UI im MVP einfach, später erweiterbar_ | YYYY-MM-DD |
### Technical Decisions
<!-- Added by /architecture -->
| Decision | Rationale | Date |
|----------|-----------|------|
| _Example: localStorage over Supabase_ | _No user accounts needed; data is device-local_ | YYYY-MM-DD |
### Technische Entscheidungen
<!-- von /architecture -->
| Entscheidung | Begründung | Datum |
|--------------|------------|-------|
| _Beispiel: localStorage statt Datenbank_ | _Keine Nutzerkonten, Daten bleiben gerätelokal_ | YYYY-MM-DD |
---
<!-- Sections below are added by subsequent skills -->
<!-- Die folgenden Abschnitte ergänzen die nachgelagerten Skills -->
## Tech Design (Solution Architect)
_To be added by /architecture_
## Tech Design
_wird von /architecture ergänzt_
## QA Test Results
_To be added by /qa_
_wird von /qa ergänzt_
## Deployment
_To be added by /deploy_
_wird von /deploy ergänzt_
+26
View File
@@ -0,0 +1,26 @@
node_modules
.next
out
build
coverage
playwright-report
test-results
.git
.gitignore
.claude
docs
features
tests
.env
.env.*
!.env.example
Dockerfile
.dockerignore
compose.yaml
README.md
CLAUDE.md
npm-debug.log*
*.tsbuildinfo
+21
View File
@@ -0,0 +1,21 @@
# Copy to .env and fill in. .env is gitignored and never committed.
# Every variable the app needs must be listed here with a dummy value.
# --- Application -------------------------------------------------------------
# Public base URL of the deployment. Better Auth derives the WebAuthn
# relying-party ID from this hostname — a wrong value makes every passkey
# fail verification, so keep it in sync with the domain you serve from.
NEXT_PUBLIC_APP_URL=http://localhost:3000
# --- Docker Compose ----------------------------------------------------------
APP_NAME=ai-coding-starter
APP_PORT=3000
# Keep on loopback: the reverse proxy on the host forwards to this port.
APP_BIND=127.0.0.1
# --- Auth (only when the project uses authentication) ------------------------
# BETTER_AUTH_SECRET=generate_with_openssl_rand_base64_32
# BETTER_AUTH_URL=http://localhost:3000
# --- Database (chosen per project by /architecture) --------------------------
# DATABASE_URL=postgres://user:password@db:5432/app
-9
View File
@@ -1,9 +0,0 @@
# Supabase (Optional - remove if not using backend)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url_here
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here
# Add more environment variables as needed
# STRIPE_SECRET_KEY=sk_...
# SMTP_HOST=smtp.sendgrid.net
# SMTP_USER=your_smtp_user
# SMTP_PASS=your_smtp_password
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}
+16 -23
View File
@@ -1,35 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# testing
/coverage
/playwright-report
/test-results
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# local env files — only .env.example is committed
.env
.env.local
.env.*.local
.env.development
.env.production
.env.test
# claude code personal settings
.claude/settings.local.json
@@ -37,3 +25,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# misc
.DS_Store
*.pem
npm-debug.log*
+69 -42
View File
@@ -1,67 +1,84 @@
# AI Coding Starter Kit
> A Next.js template with an AI-powered development workflow using specialized skills for Requirements, Architecture, Frontend, Backend, QA, and Deployment.
> Next.js template with an AI-driven development workflow. Skills cover Requirements,
> Architecture, Frontend, Backend, QA and Deployment. Every phase writes its result
> into the feature spec, so state lives in files rather than in the conversation.
## Tech Stack
## Tech Stack (binding for every new app)
- **Framework:** Next.js 16 (App Router), TypeScript
- **Styling:** Tailwind CSS + shadcn/ui (copy-paste components)
- **Backend:** Supabase (PostgreSQL + Auth + Storage) - optional
- **Deployment:** Vercel
- **Validation:** Zod + react-hook-form
- **State:** React useState / Context API
| Layer | Choice | Notes |
|-------|--------|-------|
| Framework | Next.js 16, App Router | React 19, Server Components by default |
| Language | TypeScript, `strict` | `@/*` maps to `src/*` |
| Styling | Tailwind CSS v4 | CSS-first config in `src/app/globals.css`. **There is no `tailwind.config.ts`** — theme tokens live in `@theme inline`. |
| Components | shadcn/ui (`new-york`) | 6 pre-installed. Add more with `npx shadcn@latest add <name> --yes`. Never hand-write an equivalent. |
| Icons | lucide-react | |
| Forms | react-hook-form + Zod | Zod schema is the single source of truth for a form's validation |
| Notifications | sonner | Mounted in `layout.tsx`. The deprecated shadcn `toast` is deliberately absent. |
| Theming | next-themes | `attribute="class"`, dark mode via `.dark` |
| Auth | Better Auth + passkey plugin | Only when the project needs auth. Passkeys are the primary method — see `.claude/rules/security.md`. |
| Data layer | **decided per project** | The template ships none. `/architecture` picks and documents it. |
| Tests | Vitest (unit) + Playwright (E2E) | Vitest only scans `src/`, Playwright only `tests/` |
| Deployment | Docker Compose, self-hosted | App container only; TLS/routing via a reverse proxy on the host |
Deliberately **not** part of this template: Supabase, Vercel, any preselected ORM
or database. Do not reintroduce them without the user asking.
## Project Structure
```
src/
app/ Pages (Next.js App Router)
app/ Pages and route handlers (App Router)
api/health/ Liveness probe used by Docker + update script
components/
ui/ shadcn/ui components (NEVER recreate these)
ui/ shadcn/ui — NEVER recreate these by hand
hooks/ Custom React hooks
lib/ Utilities (supabase.ts, utils.ts)
features/ Feature specifications (PROJ-X-name.md)
INDEX.md Feature status overview
lib/utils.ts cn() helper
features/ One spec per feature: PROJ-X-name.md
INDEX.md Status of every feature — read this first
docs/
PRD.md Product Requirements Document
production/ Production guides (Sentry, security, performance)
DEPLOYMENT.md Docker Compose operations
tests/ Playwright E2E specs
scripts/update.sh Pull, rebuild, restart, roll back on failure
```
## Development Workflow
## Workflow
1. `/init` - Initialize the project: PRD + feature map (run once at the start)
2. `/write-spec` - Create a full feature spec for one feature
3. `/architecture` - Design tech architecture (PM-friendly, no code)
4. `/frontend` - Build UI components (shadcn/ui first!)
5. `/backend` - Build APIs, database, RLS policies
6. `/qa` - Test against acceptance criteria + security audit
7. `/deploy` - Deploy to Vercel + production-ready checks
| Step | Command | Produces |
|------|---------|----------|
| 1 | `/init` | PRD + feature map (once per project) |
| 2 | `/write-spec` | Full spec for one feature |
| 3 | `/architecture` | Tech design inside the spec |
| 4 | `/frontend` | UI components |
| 5 | `/backend` | APIs, data layer, auth |
| 6 | `/qa` | Test results inside the spec |
| 7 | `/deploy` | Live Docker Compose stack |
Use `/refine PROJ-X` at any point to revisit and improve an existing feature spec.
`/refine PROJ-X` reopens an existing spec. `/help` reports where the project stands.
## Feature Tracking
## Conventions
All features tracked in `features/INDEX.md`. Every skill reads it at start and updates it when done. Feature specs live in `features/PROJ-X-name.md`.
- **Feature IDs:** `PROJ-1`, `PROJ-2`, … sequential, tracked in `features/INDEX.md`
- **Commits:** `type(PROJ-X): description` — feat, fix, refactor, test, docs, deploy, chore
- **One feature per spec file** — never combine independent functionality
- **Spec language:** German (PRD, specs, acceptance criteria). Code, comments and
skill instructions stay English.
- **Human-in-the-loop:** every skill ends with a user approval checkpoint
- **Tests:** unit tests co-located (`useThing.test.ts` beside `useThing.ts`), E2E in `tests/`
## Key Conventions
- **Feature IDs:** PROJ-1, PROJ-2, etc. (sequential)
- **Commits:** `feat(PROJ-X): description`, `fix(PROJ-X): description`
- **Single Responsibility:** One feature per spec file
- **shadcn/ui first:** NEVER create custom versions of installed shadcn components
- **Human-in-the-loop:** All workflows have user approval checkpoints
- **Tests:** Unit tests co-located next to source files (`useHook.test.ts` next to `useHook.ts`). E2E tests in `tests/`.
## Build & Test Commands
## Commands
```bash
npm run dev # Development server (localhost:3000)
npm run build # Production build
npm run lint # ESLint
npm run start # Production server
npm test # Vitest unit/integration tests
npm run test:e2e # Playwright E2E tests
npm run test:all # Both test suites
npm run dev # Dev server on :3000
npm run build # Production build (standalone output)
npm run lint # ESLint (flat config)
npm run typecheck # tsc --noEmit
npm test # Vitest
npm run test:e2e # Playwright
npm run test:all # Both
npm run docker:up # Build and start the Compose stack
./scripts/update.sh # Deploy latest commit on the server
```
## Product Context
@@ -71,3 +88,13 @@ npm run test:all # Both test suites
## Feature Overview
@features/INDEX.md
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+41
View File
@@ -0,0 +1,41 @@
FROM node:22-alpine AS base
# --- deps: install production-resolvable dependencies once -------------------
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# --- builder: compile the Next.js standalone bundle --------------------------
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# --- runner: minimal runtime image -------------------------------------------
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# standalone already contains the pruned node_modules and server.js
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
# Used by compose's healthcheck and by scripts/update.sh for rollback detection
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]
+81 -297
View File
@@ -1,330 +1,114 @@
# AI Coding Starter Kit
> Build production-ready web apps faster with AI-powered Skills handling Requirements, Architecture, Development, QA, and Deployment.
Next.js template with an AI-driven development workflow for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
Nine skills cover the path from idea to a running deployment; every phase writes its
result into a feature spec, so the project state lives in files rather than in a chat log.
This template uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with modern Skills, Rules, and Sub-Agents to provide a complete AI-powered development workflow.
## Quick Start
### 1. Clone & Install
## Quick start
```bash
git clone https://github.com/YOUR_USERNAME/ai-coding-starter-kit.git my-project
git clone <repo-url> my-project
cd my-project
npm install
npx playwright install chromium # one-time: installs browser for E2E tests (~300MB)
```
### 2. (Optional) Supabase Setup
If you need a backend:
1. Create Supabase Project: [supabase.com](https://supabase.com)
2. Copy `.env.local.example` to `.env.local`
3. Add your Supabase credentials
4. Uncomment the Supabase client in `src/lib/supabase.ts`
Skip this step if you're building frontend-only (landing pages, portfolios, etc.)
### 3. Start Development
```bash
cp .env.example .env
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) in your browser.
### 4. Initialize Your Project
Open Claude Code and run `/init` with a brief description of your idea:
Then open Claude Code and describe what you want to build:
```
/init I want to build a project management tool for small teams
where users can create projects, assign tasks, and track progress.
/init Ich will eine Aufgabenverwaltung für kleine Teams bauen
```
The skill interviews you one question at a time (**Grill Me** principle — always with a recommended answer you just confirm or correct) until there's a shared understanding. It then:
1. Creates your **Product Requirements Document** (`docs/PRD.md`)
2. Breaks the project into a prioritized feature map (P0/P1/P2)
3. Updates **feature tracking** (`features/INDEX.md`)
4. Recommends which feature to build first
### 5. Spec Your First Feature
After initialization, create a detailed spec for the first feature:
`/init` interviews you one question at a time — always with a recommended answer you
just confirm or correct — and writes `docs/PRD.md` plus a prioritized feature map.
From there:
```
/write-spec PROJ-1
/write-spec PROJ-1 Spec: user stories, acceptance criteria, edge cases
/architecture PROJ-1 Tech design (no code, PM-readable)
/frontend PROJ-1 UI components
/backend PROJ-1 Route handlers, data, auth
/qa PROJ-1 Test against the criteria + security audit
/deploy PROJ-1 Docker Compose stack
```
The skill interviews you about this single feature in depth — user stories, edge cases, acceptance criteria. Use `/refine PROJ-X` at any point to revisit and improve an existing spec.
`/refine PROJ-X` reopens a spec. `/help` tells you where you stand.
Handoffs are always user-initiated — no skill jumps ahead on its own.
### 6. Build Features
## Stack
```
/architecture Design the tech approach for features/PROJ-1-user-auth.md
/frontend Build the UI for features/PROJ-1-user-auth.md
/backend Build the API for features/PROJ-1-user-auth.md
/qa Test features/PROJ-1-user-auth.md
/deploy Deploy to Vercel
```
Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · shadcn/ui ·
react-hook-form + Zod · sonner · next-themes · Vitest + Playwright · Docker Compose
Each skill suggests the next step when it finishes. Handoffs are always user-initiated.
**No database and no ORM ship with this template.** `/architecture` picks one per
project — the only constraint is that it runs self-hosted in the Compose stack.
---
**Authentication is fixed:** Better Auth with the passkey plugin. Passkeys are the
primary sign-in method; a password flow is only a fallback for devices that cannot
do WebAuthn.
## Available Skills
| Skill | Command | What It Does |
|-------|---------|-------------|
| Project Initializer | `/init` | One-time setup: creates PRD + feature map via Grill Me interview |
| Feature Spec Writer | `/write-spec` | Creates a full spec for one feature (user stories, AC, edge cases) |
| Spec Refiner | `/refine PROJ-X` | Reopens an existing spec to improve, extend, or challenge it |
| Solution Architect | `/architecture` | Designs PM-friendly tech architecture (no code, only high-level design) |
| Frontend Developer | `/frontend` | Builds UI with React, Tailwind CSS, and shadcn/ui |
| Backend Developer | `/backend` | Builds APIs, database schemas, RLS policies with Supabase |
| QA Engineer | `/qa` | Tests features against acceptance criteria + security audit |
| DevOps | `/deploy` | Deploys to Vercel with production-ready checks |
| Help | `/help` | Context-aware guide: shows where you are and what to do next |
### How Skills Work
- **Skills** are defined in `.claude/skills/` and auto-discovered by Claude Code
- **Rules** in `.claude/rules/` are auto-applied based on file context (no manual loading)
- **Sub-Agents** run heavy tasks (frontend, backend, QA) in isolated contexts for cost efficiency
- **CLAUDE.md** provides project context automatically at every session start
---
## Development Workflow
```
0. Setup /init --> PRD + feature map (once per project)
1. Spec /write-spec --> Feature spec in features/PROJ-X.md
/refine PROJ-X --> Revisit and improve an existing spec
2. Design /architecture --> Tech design added to feature spec
3. Build /frontend --> UI components implemented
/backend --> APIs + database (if needed)
4. Test /qa --> Test results added to feature spec
5. Ship /deploy --> Deployed to Vercel
```
### Feature Tracking
Features are tracked in `features/INDEX.md`:
| ID | Feature | Status | Spec |
|----|---------|--------|------|
| PROJ-1 | User Login | Deployed | [Spec](features/PROJ-1-user-login.md) |
| PROJ-2 | Dashboard | In Progress | [Spec](features/PROJ-2-dashboard.md) |
Every skill reads this file at start and updates it when done, preventing duplicate work.
---
## Tech Stack
| Category | Tool | Why? |
|----------|------|------|
| **Framework** | Next.js 16 | React + Server Components + App Router |
| **Language** | TypeScript | Type safety |
| **Styling** | Tailwind CSS | Utility-first CSS |
| **UI Library** | shadcn/ui | Copy-paste, customizable components |
| **Backend** | Supabase (optional) | PostgreSQL + Auth + Storage + Realtime |
| **Deployment** | Vercel | Zero-config Next.js hosting |
| **Validation** | Zod | Runtime type validation |
---
## Project Structure
```
ai-coding-starter-kit/
+-- CLAUDE.md <-- Auto-loaded project context
+-- .claude/
| +-- settings.json <-- Team permissions (committed)
| +-- settings.local.json <-- Personal overrides (gitignored)
| +-- rules/ <-- Auto-applied coding rules
| | +-- general.md Git workflow, feature tracking
| | +-- frontend.md shadcn/ui, component standards
| | +-- backend.md RLS, validation, queries
| | +-- security.md Secrets, headers, auth
| +-- skills/ <-- Invocable workflows (/command)
| | +-- init/SKILL.md /init
| | +-- write-spec/SKILL.md /write-spec
| | +-- refine/SKILL.md /refine
| | +-- architecture/SKILL.md /architecture
| | +-- frontend/SKILL.md /frontend (runs as sub-agent)
| | +-- backend/SKILL.md /backend (runs as sub-agent)
| | +-- qa/SKILL.md /qa (runs as sub-agent)
| | +-- deploy/SKILL.md /deploy
| | +-- help/SKILL.md /help
| +-- agents/ <-- Sub-agent configs
| +-- frontend-dev.md Model, tools, limits
| +-- backend-dev.md
| +-- qa-engineer.md
+-- features/ <-- Feature specifications
| +-- INDEX.md Status tracking
| +-- README.md Spec format documentation
+-- docs/
| +-- PRD.md <-- Product Requirements Document
| +-- production/ <-- Production setup guides
| +-- error-tracking.md Sentry setup (5 min)
| +-- security-headers.md XSS/Clickjacking protection
| +-- performance.md Lighthouse, optimization
| +-- database-optimization.md Indexing, N+1, caching
| +-- rate-limiting.md Upstash Redis
+-- src/
| +-- app/ <-- Pages (Next.js App Router)
| +-- components/
| | +-- ui/ <-- shadcn/ui components (35+ installed)
| +-- hooks/ <-- Custom React hooks
| +-- lib/ <-- Utilities
+-- public/ <-- Static files
```
---
## Getting Started
### 1. Initialize the Project
Run `/init` with a brief description of your idea. The skill interviews you one question at a time and fills out `docs/PRD.md` with your vision, target users, and a prioritized feature map.
### 2. Spec Your First Feature
Run `/write-spec PROJ-1`. The skill interviews you in depth about this single feature and creates a complete spec in `features/PROJ-1-name.md` — user stories, acceptance criteria, edge cases. Then suggest running `/architecture` as the next step.
### 3. Add shadcn/ui Components (as needed)
35+ components are pre-installed. Add more as needed:
```bash
npx shadcn@latest add [component-name]
```
### 4. Production Setup (first deployment)
When you're ready to deploy, the `/deploy` skill guides you through:
- Vercel setup and deployment
- Error tracking with Sentry
- Security headers configuration
- Performance monitoring with Lighthouse
See `docs/production/` for detailed setup guides.
---
## How It Works Under the Hood
### Skills (`.claude/skills/`)
Each skill is a structured workflow that Claude Code discovers automatically. Skills can run inline (in the main conversation) or as forked sub-agents (isolated context window).
| Skill | Execution | Why? |
|-------|-----------|------|
| `/init` | Inline | Needs live interview with user |
| `/write-spec` | Inline | Needs live interview with user |
| `/refine` | Inline | Needs live interview with user |
| `/architecture` | Inline | Short output, user reviews in real-time |
| `/frontend` | Sub-agent (forked) | Heavy file editing, lots of output |
| `/backend` | Sub-agent (forked) | Heavy file editing, SQL, API code |
| `/qa` | Sub-agent (forked) | Systematic testing, lots of output |
| `/deploy` | Inline | Deployment needs user oversight |
| `/help` | Inline | Quick status check and guidance |
### Rules (`.claude/rules/`)
Coding standards that are auto-applied based on which files Claude is working with. No manual loading needed.
### Sub-Agent Configs (`.claude/agents/`)
Lightweight configurations that define model, tool access, and turn limits for forked skills.
### CLAUDE.md
Auto-loaded at every session start. Contains tech stack, conventions, and references to PRD and feature index.
---
## Context Engineering
AI agents work best with clean, structured context - not longer prompts. This template is designed around these principles:
### State lives in files, not in memory
Every skill reads `features/INDEX.md` and the relevant feature spec at start. After context compaction or a new session, nothing is lost - the agent simply re-reads the files. Progress tracking, acceptance criteria, and tech designs all live in markdown files, not in the conversation.
### Context is layered
Not everything is loaded at once. Information is layered by relevance:
| Layer | What | When loaded |
|-------|------|-------------|
| `CLAUDE.md` | Tech stack, conventions, commands | Every session (auto) |
| `.claude/rules/` | Coding standards | When editing matching files (auto) |
| Skill `SKILL.md` | Workflow instructions | When skill is invoked |
| Feature spec | Requirements, AC, tech design | On demand (skill reads it) |
| `docs/production/` | Deployment guides | Only when referenced |
### Context is isolated
Heavy implementation skills (`/frontend`, `/backend`, `/qa`) run as **forked sub-agents** with their own context window. Research noise from one skill doesn't pollute another. Each fork starts clean and loads only what it needs.
### Context recovery is built in
All forked skills include a **Context Recovery** section: if the context is compacted mid-task, the agent re-reads the feature spec, checks `git diff` for progress, and continues without restarting or duplicating work.
### Always read, never guess
A global rule (`rules/general.md`) enforces: always read a file before modifying it, never assume contents from memory, verify import paths and API routes by reading. This prevents hallucinated code references - the most common source of AI coding errors.
---
## Customization for Your Team
This template is designed as a starting point. Customize it for your team:
1. **Edit CLAUDE.md** - Add your project-specific conventions and build commands
2. **Edit docs/PRD.md** - Define your product vision and roadmap
3. **Edit .claude/rules/** - Adjust coding standards for your team
4. **Edit .claude/skills/** - Modify workflows to match your process
5. **Edit .claude/settings.json** - Configure team permissions
---
## Production Guides
Standalone guides in `docs/production/`:
| Guide | Setup Time | What It Does |
|-------|-----------|-------------|
| [Error Tracking](docs/production/error-tracking.md) | 5 min | Sentry integration for automatic error capture |
| [Security Headers](docs/production/security-headers.md) | 2 min | XSS, Clickjacking, MIME sniffing protection |
| [Performance](docs/production/performance.md) | 10 min | Lighthouse checks, image optimization, caching |
| [Database Optimization](docs/production/database-optimization.md) | 15 min | Indexing, N+1 prevention, query optimization |
| [Rate Limiting](docs/production/rate-limiting.md) | 10 min | Upstash Redis for API abuse prevention |
---
## Scripts
Six shadcn components are pre-installed (`button`, `card`, `input`, `label`, `form`,
`sonner`). Everything else is one command away:
```bash
npm run dev # Development server (localhost:3000)
npm run build # Production build
npm run start # Production server
npx shadcn@latest add dialog table tabs --yes
```
Tailwind v4 is CSS-first: there is no `tailwind.config.ts`. Theme tokens live in
`src/app/globals.css` under `@theme inline`.
## How the workflow is wired
| Directory | Contents |
|-----------|----------|
| `CLAUDE.md` | Loaded every session — stack, structure, conventions |
| `.claude/rules/` | Applied automatically based on which files are being edited |
| `.claude/skills/` | The nine `/commands`, one folder each |
| `.claude/agents/` | Sub-agents for the implementation-heavy phases |
| `features/` | `INDEX.md` plus one spec per feature |
| `docs/` | `PRD.md`, `DEPLOYMENT.md` |
`/frontend`, `/backend` and `/qa` run the interview and the review with you in the
main conversation, then hand the heavy file work to their sub-agent. The sub-agent
reports back; the skill keeps ownership of the spec and status updates.
State is deliberately file-based: if the context is compacted mid-task, the skill
re-reads the spec and `git diff`, and continues without redoing work.
## Deployment
Self-hosted Docker Compose. The stack publishes the app on loopback; a reverse proxy
on the host terminates TLS.
```bash
docker compose up -d --build # first start
./scripts/update.sh # deploy the latest commit
```
`update.sh` pulls, rebuilds, restarts and waits for the healthcheck — and rolls back
to the previous commit if the new container never turns healthy.
Details in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
## Commands
```bash
npm run dev # Dev server on :3000
npm run build # Production build (standalone output)
npm run lint # ESLint
npm test # Vitest: integration tests for API routes
npm run test:e2e # Playwright: E2E tests for user flows
npm run test:all # Run both test suites
npm run typecheck # tsc --noEmit
npm test # Vitest — unit tests, co-located in src/
npm run test:e2e # Playwright — E2E specs in tests/
npm run test:all # Both
```
---
Playwright needs its browser once per machine: `npx playwright install chromium`
## Author
## Customizing
Created by **Alex Sprogis** – AI Product Engineer & Content Creator.
- [YouTube](https://www.youtube.com/@alex.sprogis)
- [Website](https://alexsprogis.de)
---
`CLAUDE.md` for stack and conventions · `.claude/rules/` for coding standards ·
`.claude/skills/` for the workflow itself · `.claude/settings.json` for permissions.
## License
MIT License - feel free to use for your projects!
MIT
+7 -3
View File
@@ -1,17 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"config": "",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+48
View File
@@ -0,0 +1,48 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
image: ${APP_NAME:-ai-coding-starter}:latest
container_name: ${APP_NAME:-ai-coding-starter}
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
PORT: 3000
ports:
# Bound to loopback: the reverse proxy on the host forwards to this.
# Set APP_BIND=0.0.0.0 in .env only if the port must be publicly reachable.
- "${APP_BIND:-127.0.0.1}:${APP_PORT:-3000}:3000"
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# A database is deliberately not part of this template — /architecture picks one
# per project. When you add one, declare it here as a second service, put it on
# an internal network without a published port, and add its volume below.
#
# db:
# image: postgres:17-alpine
# restart: unless-stopped
# environment:
# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
# POSTGRES_DB: ${POSTGRES_DB:-app}
# volumes:
# - db-data:/var/lib/postgresql/data
#
# volumes:
# db-data:
+148
View File
@@ -0,0 +1,148 @@
# Deployment
Self-hosted Docker Compose. One app container, published on loopback; a reverse
proxy already running on the host terminates TLS and forwards to it.
## Files
| File | Purpose |
|------|---------|
| `Dockerfile` | Three-stage build → Next.js standalone bundle, runs as an unprivileged user |
| `compose.yaml` | The `app` service, healthcheck, log rotation |
| `.dockerignore` | Keeps the build context small — no `node_modules`, no `.env`, no docs |
| `scripts/update.sh` | Pull, rebuild, restart, roll back if unhealthy |
| `.env.example` | Every variable the app needs, with dummy values |
## First deployment
Prerequisites on the server: Docker with the Compose plugin, a reverse proxy, and
a DNS record pointing at it.
```bash
git clone <repo-url> /srv/<app-name>
cd /srv/<app-name>
cp .env.example .env
```
Edit `.env`:
| Variable | Notes |
|----------|-------|
| `NEXT_PUBLIC_APP_URL` | The real public URL. With auth this **must** match the domain exactly — the WebAuthn relying-party ID is derived from it, and a mismatch breaks every passkey without a clear error. |
| `APP_PORT` | Host port the reverse proxy forwards to |
| `APP_BIND` | Leave at `127.0.0.1` unless the container must be publicly reachable |
| `BETTER_AUTH_SECRET` | `openssl rand -base64 32`, unique per environment |
```bash
docker compose up -d --build
curl -fsS http://127.0.0.1:3000/api/health # {"status":"ok"}
```
### Reverse proxy
Forward to `127.0.0.1:$APP_PORT` and pass through `X-Forwarded-Proto` and
`X-Forwarded-Host` — without them redirects and WebAuthn origin checks fail.
nginx:
```nginx
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
Caddy:
```
app.example.com {
reverse_proxy 127.0.0.1:3000
}
```
Passkeys require HTTPS in production. Localhost is the only exemption.
## Updates
```bash
./scripts/update.sh # deploy the current branch's latest commit
./scripts/update.sh --force # rebuild even without a new commit
./scripts/update.sh --branch dev # deploy another branch
```
What it does: refuses to run on a dirty working tree, fast-forwards to
`origin/<branch>`, rebuilds, restarts, then polls the container healthcheck for up
to `HEALTH_TIMEOUT` seconds (default 120). If the container never turns healthy it
prints the last 50 log lines, resets to the previous commit, rebuilds and restarts
that — so a bad deploy self-heals instead of leaving the site down.
## Database
The template ships none by design. When `/architecture` picks one:
1. Add it as a second service in `compose.yaml` — no published port, reachable only
over the internal Compose network
2. Add a named volume for its data
3. Add `DATABASE_URL` to `.env.example`
4. Run migrations **before** the new container serves traffic. Either add a
short-lived `migrate` service that the app depends on, or add the migration
command to `scripts/update.sh` between build and restart.
5. Set up a backup for the volume — a volume is not a backup
## Operations
```bash
docker compose logs -f app # follow logs
docker compose ps # status incl. health
docker compose restart app # restart without rebuilding
docker compose down # stop the stack
docker compose exec app sh # shell inside the container
```
Logs rotate at 10 MB with 3 files kept, configured in `compose.yaml`.
## Manual rollback
```bash
git reset --hard <previous-sha>
docker compose up -d --build
```
## Security headers
Set in `next.config.ts`: `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`,
`Referrer-Policy: origin-when-cross-origin`, `Strict-Transport-Security`.
Verify after deploying that the proxy did not strip them: DevTools → Network →
pick a request → Response Headers.
A Content-Security-Policy is deliberately not preconfigured — it breaks apps when
guessed. Add it per project, starting with `Content-Security-Policy-Report-Only`.
## Error tracking
Not preinstalled. When you want it, Sentry is the usual choice:
```bash
npx @sentry/wizard@latest -i nextjs
```
It edits `next.config.ts` — re-check that the `headers()` block survived, and add
`SENTRY_DSN` to `.env.example`.
## Troubleshooting
| Symptom | Cause |
|---------|-------|
| Builds locally, fails in Docker | Build-time dependency sits in `devDependencies` |
| Env var undefined at runtime | Missing in the server's `.env`, or needs the `NEXT_PUBLIC_` prefix for client use |
| Container healthy, proxy returns 502 | Proxy points at the wrong port, or `APP_BIND` is not reachable from it |
| Redirect loop | Proxy not forwarding `X-Forwarded-Proto` |
| Passkeys fail after deploy | `NEXT_PUBLIC_APP_URL` mismatch, or no HTTPS |
| `update.sh` refuses to start | Uncommitted changes on the server — commit or discard them |
+19 -17
View File
@@ -1,29 +1,31 @@
# Product Requirements Document
## Vision
_Describe what you are building and why._
_Was wird gebaut und warum? Zwei bis drei Sätze._
## Target Users
_Who will use this product? Describe their needs and pain points._
## Zielgruppe
_Wer nutzt das Produkt? Welchen konkreten Schmerz hat diese Gruppe heute?_
## Core Features (Roadmap)
## Roadmap
| Priority | Feature | Status |
|----------|---------|--------|
| P0 (MVP) | _Feature 1_ | Planned |
| P0 (MVP) | _Feature 2_ | Planned |
| P1 | _Feature 3_ | Planned |
| P2 | _Feature 4_ | Planned |
| Priorität | Feature | Status |
|-----------|---------|--------|
| P0 (MVP) | _Feature 1_ | Roadmap |
| P0 (MVP) | _Feature 2_ | Roadmap |
| P1 | _Feature 3_ | Roadmap |
| P2 | _Feature 4_ | Roadmap |
## Success Metrics
_How will you measure success? (e.g., user signups, retention, task completion rate)_
## Erfolgsmetriken
_Woran wird gemessen, ob das Produkt funktioniert?_
## Constraints
_Budget, timeline, technical limitations, team size._
## Rahmenbedingungen
_Zeit, Budget, Teamgröße, technische Grenzen. Auch: Braucht das Projekt dauerhafte
Datenspeicherung? Braucht es Nutzerkonten?_
## Non-Goals
_What are you explicitly NOT building in this version?_
## Nicht-Ziele
_Was in dieser Version bewusst nicht gebaut wird._
---
Use `/requirements` to create detailed feature specifications for each item in the roadmap above.
Dieses Dokument füllt `/init` im Interview aus. Danach entstehen mit `/write-spec`
die einzelnen Feature-Specs.
-91
View File
@@ -1,91 +0,0 @@
# Database Optimization
## 1. Indexing
Create indexes on columns used in WHERE, ORDER BY, or JOIN clauses:
```sql
-- Without index: ~500ms at 100k rows
SELECT * FROM tasks WHERE user_id = 'abc123' ORDER BY created_at DESC;
-- After creating index: <10ms
CREATE INDEX idx_tasks_user_id_created ON tasks(user_id, created_at DESC);
```
**Rule of thumb:** If a column appears in WHERE or ORDER BY and the table will have >1000 rows, add an index.
Always include indexes in your migration SQL alongside CREATE TABLE.
## 2. Avoid N+1 Queries
The most common performance problem with ORMs and query builders:
```typescript
// Bad: N+1 (1 query for users + N queries for tasks)
const { data: users } = await supabase.from('users').select('*')
for (const user of users) {
const { data: tasks } = await supabase
.from('tasks')
.select('*')
.eq('user_id', user.id)
}
// Good: Single query with join (1 query total)
const { data } = await supabase
.from('users')
.select('*, tasks(*)')
```
## 3. Always Limit Results
Never return unbounded results from the database:
```typescript
// Bad: Returns ALL rows
const { data } = await supabase.from('tasks').select('*')
// Good: Returns max 50 rows
const { data } = await supabase.from('tasks').select('*').limit(50)
// Better: Paginated
const { data } = await supabase
.from('tasks')
.select('*')
.range(0, 49) // First 50 rows
```
## 4. Caching Strategy
For data that changes rarely (dashboard stats, config, categories):
```typescript
import { unstable_cache } from 'next/cache'
export const getCategories = unstable_cache(
async () => {
const { data } = await supabase.from('categories').select('*')
return data
},
['categories'], // Cache key
{ revalidate: 3600 } // Refresh every hour
)
```
**When to cache:**
- Data that changes less than once per hour
- Expensive aggregation queries
- Data shared across all users (not user-specific)
**When NOT to cache:**
- User-specific data that changes frequently
- Real-time data (use Supabase Realtime instead)
## 5. Select Only What You Need
```typescript
// Bad: Fetches all columns
const { data } = await supabase.from('users').select('*')
// Good: Fetches only needed columns
const { data } = await supabase.from('users').select('id, name, avatar_url')
```
-43
View File
@@ -1,43 +0,0 @@
# Error Tracking Setup (Sentry)
Track production errors automatically so you know about issues before your users report them.
## Setup (5 minutes)
### 1. Create Sentry Account
- Go to [sentry.io](https://sentry.io) (free tier available for small apps)
- Create a new project and select "Next.js"
### 2. Install Next.js Integration
```bash
npx @sentry/wizard@latest -i nextjs
```
This automatically:
- Installs `@sentry/nextjs`
- Creates `sentry.client.config.ts` and `sentry.server.config.ts`
- Updates `next.config.ts` with Sentry webpack plugin
### 3. Add Environment Variables
Add to `.env.local` (local) and Vercel Dashboard (production):
```bash
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
SENTRY_AUTH_TOKEN=sntrys_xxx # For source maps upload
```
### 4. Verify Setup
Trigger a test error and check Sentry Dashboard:
```typescript
// Temporary test - remove after verification
throw new Error("Sentry test error")
```
## What You Get
- Automatic error capture (client + server)
- Stack traces with source maps
- Error grouping and deduplication
- Email alerts for new errors
- Performance monitoring (optional)
## Alternative
**Vercel Error Tracking** - Built-in, simpler, but fewer features. Available in Vercel Dashboard under "Monitoring".
-67
View File
@@ -1,67 +0,0 @@
# Performance Monitoring
## Lighthouse Check (after every deployment)
1. Open Chrome DevTools (F12)
2. Go to Lighthouse tab
3. Select: Performance, Accessibility, Best Practices, SEO
4. Generate Report for both Mobile and Desktop
5. **Target: Score > 90** in all categories
## Common Performance Issues
### Unoptimized Images
```tsx
// Bad - unoptimized, no lazy loading
<img src="/large-image.jpg" />
// Good - Next.js Image component
import Image from 'next/image'
<Image src="/large-image.jpg" width={800} height={600} alt="Description" />
```
Next.js Image automatically: resizes, lazy-loads, serves WebP format.
### Large JavaScript Bundle
Use dynamic imports for heavy components that aren't needed on initial load:
```tsx
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading chart...</p>,
})
```
### Missing Loading States
Always show feedback during data fetching:
```tsx
// Use shadcn Skeleton component
import { Skeleton } from "@/components/ui/skeleton"
if (isLoading) return <Skeleton className="h-12 w-full" />
```
### No Caching Strategy
Cache slow database queries with `unstable_cache`:
```typescript
import { unstable_cache } from 'next/cache'
export const getStats = unstable_cache(
async () => {
const { data } = await supabase.from('stats').select('*')
return data
},
['dashboard-stats'],
{ revalidate: 3600 } // Refresh every hour
)
```
## Quick Wins Checklist
- [ ] All images use `next/image` component
- [ ] Heavy components use dynamic imports
- [ ] Loading states show skeleton/spinner
- [ ] Fonts loaded with `next/font`
- [ ] No unnecessary client-side JavaScript (`"use client"` only when needed)
## Automated Monitoring
- **Vercel Analytics** - Automatic on Pro plan, shows Core Web Vitals
- **Vercel Speed Insights** - Real user performance data
-101
View File
@@ -1,101 +0,0 @@
# Rate Limiting
Prevent abuse, DDoS attacks, and excessive API usage.
## When to Add Rate Limiting
- **MVP:** Optional (focus on features first)
- **Production with users:** Recommended on auth endpoints and public APIs
- **Public-facing APIs:** Required
## Setup with Upstash Redis
### 1. Install Dependencies
```bash
npm install @upstash/ratelimit @upstash/redis
```
### 2. Create Upstash Account
- Go to [upstash.com](https://upstash.com) (free tier: 10k requests/day)
- Create a Redis database
- Copy REST URL and token
### 3. Add Environment Variables
```bash
# .env.local
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
```
### 4. Create Rate Limiter
```typescript
// src/lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
})
```
### 5. Use in API Routes
```typescript
// src/app/api/example/route.ts
import { ratelimit } from '@/lib/rate-limit'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const { success, limit, remaining } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
},
}
)
}
// Process request normally...
}
```
### 6. Use in Middleware (Global)
```typescript
// middleware.ts
import { ratelimit } from '@/lib/rate-limit'
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
// Only rate limit API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const { success } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json({ error: 'Too Many Requests' }, { status: 429 })
}
}
}
export const config = {
matcher: '/api/:path*',
}
```
## Recommended Limits
| Endpoint Type | Limit | Window |
|--------------|-------|--------|
| Login/Register | 5 requests | 1 minute |
| Password Reset | 3 requests | 5 minutes |
| General API | 30 requests | 10 seconds |
| File Upload | 5 requests | 1 minute |
## Alternative
**Vercel Edge Config** - Simpler but less flexible. Built into Vercel, no external service needed.
-64
View File
@@ -1,64 +0,0 @@
# Security Headers Configuration
Protect against XSS, Clickjacking, MIME sniffing, and other common web attacks.
## Setup
Add security headers to `next.config.ts`:
```typescript
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
],
},
]
},
}
export default nextConfig
```
## What Each Header Does
| Header | Protection |
|--------|-----------|
| X-Frame-Options: DENY | Prevents your site from being embedded in iframes (clickjacking) |
| X-Content-Type-Options: nosniff | Prevents browsers from guessing content types (MIME sniffing) |
| Referrer-Policy | Controls how much URL info is sent to other sites |
| Strict-Transport-Security | Forces HTTPS connections |
## Verify After Deployment
1. Open Chrome DevTools
2. Go to Network tab
3. Click on any request to your site
4. Check Response Headers section
5. Verify all 4 headers are present
## Advanced (Optional)
**Content-Security-Policy (CSP)** - The most powerful header, but can break your app if misconfigured. Only add after thorough testing:
```
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
```
Start with report-only mode first: `Content-Security-Policy-Report-Only`
+18
View File
@@ -0,0 +1,18 @@
import next from 'eslint-config-next/core-web-vitals'
const config = [
{
ignores: [
'.next/**',
'out/**',
'build/**',
'node_modules/**',
'playwright-report/**',
'test-results/**',
'next-env.d.ts',
],
},
...next,
]
export default config
+17 -14
View File
@@ -1,21 +1,24 @@
# Feature Index
> Central tracking for all features. Updated by skills automatically.
> Zentrales Tracking aller Features. Wird von den Skills automatisch gepflegt.
## Status Legend
- **Roadmap** - `/init` done, feature identified in feature map, no spec file yet
- **Planned** - `/write-spec` done, full spec written, architecture not yet designed
- **Architected** - `/architecture` done, tech design approved, ready to build
- **In Progress** - `/frontend` or `/backend` active or completed, not yet in QA
- **In Review** - `/qa` active, testing in progress
- **Approved** - `/qa` passed, no critical/high bugs, ready to deploy
- **Deployed** - `/deploy` done, live in production
## Status
`Roadmap` → `Planned` → `Architected` → `In Progress` → `In Review` → `Approved` → `Deployed`
| Status | Bedeutung |
|--------|-----------|
| Roadmap | In `/init` identifiziert, noch keine Spec-Datei |
| Planned | `/write-spec` fertig — Spec geschrieben |
| Architected | `/architecture` fertig — technisches Design steht |
| In Progress | `/frontend` oder `/backend` läuft oder ist fertig |
| In Review | `/qa` läuft |
| Approved | `/qa` bestanden, keine Critical/High-Bugs |
| Deployed | `/deploy` fertig, live |
## Features
| ID | Feature | Status | Spec | Created |
|----|---------|--------|------|---------|
| ID | Feature | Priorität | Abhängigkeiten | Status | Spec |
|----|---------|-----------|----------------|--------|------|
<!-- Add features above this line -->
## Next Available ID: PROJ-1
## Nächste freie ID: PROJ-1
-112
View File
@@ -1,112 +0,0 @@
# Feature Specifications
Dieser Ordner enthält detaillierte Feature Specs vom Requirements Engineer.
## Naming Convention
`PROJ-X-feature-name.md`
Beispiele:
- `PROJ-1-user-authentication.md`
- `PROJ-2-kanban-board.md`
- `PROJ-3-file-attachments.md`
## Was gehört in eine Feature Spec?
### 1. User Stories
Beschreibe, was der User tun möchte:
```markdown
Als [User-Typ] möchte ich [Aktion] um [Ziel zu erreichen]
```
### 2. Acceptance Criteria
Konkrete, testbare Kriterien:
```markdown
- [ ] User kann Email + Passwort eingeben
- [ ] Passwort muss mindestens 8 Zeichen lang sein
- [ ] Nach Registration wird User automatisch eingeloggt
```
### 3. Edge Cases
Was passiert bei unerwarteten Situationen:
```markdown
- Was passiert bei doppelter Email?
- Was passiert bei Netzwerkfehler?
- Was passiert bei gleichzeitigen Edits?
```
### 4. Tech Design (vom Solution Architect)
```markdown
## Database Schema
CREATE TABLE tasks (...);
## Component Architecture
ProjectDashboard
├── ProjectList
│ └── ProjectCard
```
### 5. QA Test Results (vom QA Engineer)
Am Ende des Feature-Dokuments fügt QA die Test-Ergebnisse hinzu:
```markdown
---
## QA Test Results
**Tested:** 2026-01-12
**App URL:** http://localhost:3000
### Acceptance Criteria Status
- [x] AC-1: User kann Email + Passwort eingeben
- [x] AC-2: Passwort mindestens 8 Zeichen
- [ ] ❌ BUG: Doppelte Email wird nicht abgelehnt
### Bugs Found
**BUG-1: Doppelte Email-Registrierung**
- **Severity:** High
- **Steps to Reproduce:** 1. Register with email, 2. Try again with same email
- **Expected:** Error message
- **Actual:** Silent failure
```
### 6. Deployment Status (vom DevOps Engineer)
```markdown
---
## Deployment
**Status:** ✅ Deployed
**Deployed:** 2026-01-13
**Production URL:** https://your-app.vercel.app
**Git Tag:** v1.0.0-PROJ-1
```
## Workflow
1. **Requirements Engineer** erstellt Feature Spec
2. **User** reviewed Spec und gibt Feedback
3. **Solution Architect** fügt Tech-Design hinzu
4. **User** approved finales Design
5. **Frontend/Backend Devs** implementieren (dokumentiert via Git Commits)
6. **QA Engineer** testet und fügt Test-Ergebnisse zum Feature-Dokument hinzu
7. **DevOps** deployed und fügt Deployment-Status zum Feature-Dokument hinzu
## Status-Tracking
Feature-Status wird direkt im Feature-Dokument getrackt:
```markdown
# PROJ-1: Feature Name
**Status:** 🔵 Planned | 🟡 In Progress | ✅ Deployed
**Created:** 2026-01-12
**Last Updated:** 2026-01-12
```
**Status-Bedeutung:**
- 🔵 Planned – Requirements sind geschrieben, ready for development
- 🟡 In Progress – Wird gerade gebaut
- ✅ Deployed – Live in Production
**Git als Single Source of Truth:**
- Alle Implementierungs-Details sind in Git Commits
- `git log --grep="PROJ-1"` zeigt alle Änderungen für dieses Feature
- Keine separate FEATURE_CHANGELOG.md nötig!
+22 -4
View File
@@ -1,7 +1,25 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
/* config options here */
};
// Emits .next/standalone — the Docker image copies only that, not node_modules.
output: 'standalone',
export default nextConfig;
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
],
},
]
},
}
export default nextConfig
+1483 -1876
View File
File diff suppressed because it is too large Load Diff
+11 -29
View File
@@ -1,53 +1,35 @@
{
"name": "ai-coding-starter-kit",
"version": "1.0.0",
"description": "AI Coding Starter Kit with AI Agent Team System for Claude Code",
"description": "Next.js starter with an AI-driven development workflow (skills, agents, feature specs) for Claude Code",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:all": "vitest run && playwright test"
"test:e2e": "playwright test --pass-with-no-tests",
"test:all": "npm run test && npm run test:e2e",
"docker:up": "docker compose up -d --build",
"docker:down": "docker compose down",
"docker:logs": "docker compose logs -f app"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.39.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.0",
"cmdk": "^1.1.1",
"lucide-react": "^0.562.0",
"next": "^16.1.1",
"next-themes": "^0.4.6",
"radix-ui": "^1.6.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.71.1",
"sonner": "^2.0.7",
"tailwind-merge": "^2.2.0",
"tailwind-merge": "^3.6.0",
"zod": "^4.3.5"
},
"devDependencies": {
File diff suppressed because one or more lines are too long
+14 -6
View File
@@ -1,22 +1,30 @@
import { defineConfig, devices } from '@playwright/test'
// Override with E2E_PORT when something else already occupies 3000 — otherwise
// reuseExistingServer would silently run the suite against that foreign server.
const PORT = Number(process.env.E2E_PORT ?? 3000)
const BASE_URL = `http://localhost:${PORT}`
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'html',
reporter: process.env.CI ? 'list' : 'html',
use: {
baseURL: 'http://localhost:3000',
baseURL: BASE_URL,
trace: 'on-first-retry',
},
// Both projects run on the chromium binary, so `npx playwright install chromium`
// is enough for the whole suite.
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
{ name: 'desktop', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['Pixel 5'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
command: `npm run dev -- --port ${PORT}`,
url: BASE_URL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
+1 -2
View File
@@ -1,8 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};
+2
View File
@@ -0,0 +1,2 @@
# Keeps public/ in git. The Dockerfile copies this directory, so it must exist
# in a fresh clone even while empty.
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M3 0h10l3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/><path fill="white" d="M3 4h10v1H3zM3 7h10v1H3zM3 10h7v1H3z"/></svg>

Before

Width:  |  Height:  |  Size: 233 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M1 8h14M8 1c1.5 1 2.5 3.1 2.5 7s-1 6-2.5 7m0-14c-1.5 1-2.5 3.1-2.5 7s1 6 2.5 7"/></svg>

Before

Width:  |  Height:  |  Size: 303 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6.2c.3 0 .4.1.4.4v12.9c0 .3-.1.4-.4.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4zm-22 0h6.2c.3 0 .5.1.5.4v12.9c0 .3-.2.4-.5.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4zm-11.4 0h6.2c.3 0 .5.1.5.4v12.9c0 .3-.2.4-.5.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4zm-11.4 0h6.2c.3 0 .4.1.4.4v12.9c0 .3-.1.4-.4.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4z"/><path fill="url(#a)" d="m317.2 78.9-.4 1.2h.4l21.1-27.3-21.1-27.3h-.4l.4 1.2a50 50 0 0 1 2.3 14.8 50 50 0 0 1-2.3 14.8l21.1 27.3h.4l-.4-1.2a50 50 0 0 0 2.3-14.8 50 50 0 0 0-2.3-14.8l.4-1.2h-.4l-21.1 27.3z"/><defs><linearGradient id="a" x1="267.5" x2="342.8" y1="27.8" y2="27.8" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-opacity="0"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 630 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><rect width="16" height="16" fill="currentColor" rx="1.5"/><path stroke="white" stroke-linecap="round" d="M1 4.5h14M5 4.5V15"/></svg>

Before

Width:  |  Height:  |  Size: 206 B

+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
#
# Pull the latest commit, rebuild the image and restart the stack.
# Rolls back to the previous commit if the new container never turns healthy.
#
# Usage: ./scripts/update.sh [--force] [--branch <name>]
# --force rebuild and restart even when there is no new commit
# --branch branch to deploy (default: the currently checked out branch)
set -Eeuo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
FORCE=0
BRANCH=""
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-120}"
while [ $# -gt 0 ]; do
case "$1" in
--force) FORCE=1; shift ;;
--branch) BRANCH="${2:?--branch needs a value}"; shift 2 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m!!\033[0m %s\n' "$*" >&2; }
compose() { docker compose "$@"; }
# --- preconditions -----------------------------------------------------------
command -v git >/dev/null || { err "git not found"; exit 1; }
docker compose version >/dev/null 2>&1 || { err "docker compose not available"; exit 1; }
[ -f .env ] || { err ".env missing — copy .env.example to .env and fill it in"; exit 1; }
if [ -n "$(git status --porcelain)" ]; then
err "working tree has uncommitted changes — refusing to update"
git status --short >&2
exit 1
fi
[ -n "$BRANCH" ] || BRANCH="$(git rev-parse --abbrev-ref HEAD)"
PREV_COMMIT="$(git rev-parse HEAD)"
# --- fetch -------------------------------------------------------------------
log "Fetching origin/$BRANCH"
git fetch --quiet origin "$BRANCH"
TARGET_COMMIT="$(git rev-parse "origin/$BRANCH")"
if [ "$PREV_COMMIT" = "$TARGET_COMMIT" ] && [ "$FORCE" -eq 0 ]; then
log "Already at $(git rev-parse --short HEAD) — nothing to do (use --force to rebuild anyway)"
exit 0
fi
log "Updating $(git rev-parse --short "$PREV_COMMIT") -> $(git rev-parse --short "$TARGET_COMMIT")"
git checkout --quiet "$BRANCH"
git merge --ff-only --quiet "origin/$BRANCH"
# --- health helper -----------------------------------------------------------
container_id() { compose ps -q app; }
wait_for_health() {
local deadline=$((SECONDS + HEALTH_TIMEOUT))
local cid status
while [ "$SECONDS" -lt "$deadline" ]; do
cid="$(container_id)"
if [ -n "$cid" ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$cid" 2>/dev/null || echo missing)"
case "$status" in
healthy|no-healthcheck) return 0 ;;
unhealthy) return 1 ;;
esac
fi
sleep 3
done
return 1
}
rollback() {
err "Deployment failed — rolling back to $(git rev-parse --short "$PREV_COMMIT")"
compose logs --tail 50 app >&2 || true
git reset --hard --quiet "$PREV_COMMIT"
compose build app
compose up -d app
if wait_for_health; then
err "Rolled back successfully. The stack runs the previous commit again."
else
err "ROLLBACK ALSO UNHEALTHY — manual intervention required."
fi
exit 1
}
# --- build & restart ---------------------------------------------------------
log "Building image"
compose build app || rollback
log "Restarting stack"
compose up -d app || rollback
log "Waiting for healthcheck (timeout ${HEALTH_TIMEOUT}s)"
wait_for_health || rollback
# --- cleanup -----------------------------------------------------------------
log "Pruning dangling images"
docker image prune -f >/dev/null
log "Deployed $(git rev-parse --short HEAD) — stack is healthy"
+7
View File
@@ -0,0 +1,7 @@
// Liveness probe for the Docker healthcheck and scripts/update.sh.
// Must stay dependency-free so it answers even when other services are down.
export const dynamic = 'force-dynamic'
export function GET() {
return Response.json({ status: 'ok' })
}
+117 -74
View File
@@ -1,83 +1,126 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@import "tw-animate-css";
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
@custom-variant dark (&:is(.dark *));
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border;
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
+18 -8
View File
@@ -1,21 +1,31 @@
import type { Metadata } from "next";
import "./globals.css";
import type { Metadata } from 'next'
import { ThemeProvider } from '@/components/theme-provider'
import { Toaster } from '@/components/ui/sonner'
import './globals.css'
export const metadata: Metadata = {
title: "AI Coding Starter Kit",
description: "Built with AI Agent Team System",
};
title: 'AI Coding Starter Kit',
description: 'Next.js starter with an AI-driven development workflow',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<html lang="en">
<html lang="de" suppressHydrationWarning>
<body className="antialiased">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster />
</ThemeProvider>
</body>
</html>
);
)
}
+46 -94
View File
@@ -1,101 +1,53 @@
import Image from 'next/image'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
const steps = [
{ cmd: '/init', text: 'PRD und Feature-Map anlegen (einmalig)' },
{ cmd: '/write-spec', text: 'Feature-Spec schreiben' },
{ cmd: '/architecture', text: 'Technisches Design entwerfen' },
{ cmd: '/frontend', text: 'UI bauen' },
{ cmd: '/backend', text: 'API und Datenhaltung bauen' },
{ cmd: '/qa', text: 'Gegen Acceptance Criteria testen' },
{ cmd: '/deploy', text: 'Docker-Compose-Stack ausrollen' },
]
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{' '}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<main className="mx-auto flex min-h-screen max-w-2xl flex-col justify-center gap-8 p-8">
<div className="space-y-2">
<h1 className="text-3xl font-semibold tracking-tight">
AI Coding Starter Kit
</h1>
<p className="text-muted-foreground">
Die Vorlage steht. Starte in Claude Code mit{' '}
<code className="bg-muted rounded px-1.5 py-0.5 text-sm font-medium">
/init
</code>{' '}
und beschreibe, was du bauen willst.
</p>
</div>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">Workflow</CardTitle>
</CardHeader>
<CardContent>
<ol className="space-y-2">
{steps.map((step) => (
<li key={step.cmd} className="flex gap-3 text-sm">
<code className="text-primary w-32 shrink-0 font-medium">
{step.cmd}
</code>
<span className="text-muted-foreground">{step.text}</span>
</li>
))}
</ol>
</CardContent>
</Card>
<p className="text-muted-foreground text-sm">
Unsicher, wo du stehst? <code className="font-medium">/help</code> zeigt
den aktuellen Stand und den nächsten Schritt.
</p>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import type { ComponentProps } from 'react'
export function ThemeProvider({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
-58
View File
@@ -1,58 +0,0 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
-141
View File
@@ -1,141 +0,0 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
-59
View File
@@ -1,59 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }
-50
View File
@@ -1,50 +0,0 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
-36
View File
@@ -1,36 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
-115
View File
@@ -1,115 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
+28 -20
View File
@@ -1,29 +1,34 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
@@ -33,24 +38,27 @@ const buttonVariants = cva(
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}
}) {
const Comp = asChild ? Slot.Root : "button"
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
}
export { Button, buttonVariants }
+65 -52
View File
@@ -2,78 +2,91 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
ref={ref}
data-slot="card"
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
)
}
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="card-header"
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
)
}
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
ref={ref}
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
)
}
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
-30
View File
@@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
-11
View File
@@ -1,11 +0,0 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
-153
View File
@@ -1,153 +0,0 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
-122
View File
@@ -1,122 +0,0 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
-200
View File
@@ -1,200 +0,0 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
+41 -52
View File
@@ -1,12 +1,13 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import type { Label as LabelPrimitive } from "radix-ui"
import { Slot } from "radix-ui"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
@@ -19,16 +20,18 @@ const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null)
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@@ -42,18 +45,14 @@ const FormField = <
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
if (!itemContext) {
throw new Error("useFormField should be used within <FormItem>")
}
const fieldState = getFieldState(fieldContext.name, formState)
const { id } = itemContext
return {
@@ -70,48 +69,47 @@ type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue | null>(null)
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
}
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
}
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
function FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
<Slot.Root
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
@@ -122,32 +120,24 @@ const FormControl = React.forwardRef<
{...props}
/>
)
})
FormControl.displayName = "FormControl"
}
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
data-slot="form-description"
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
}
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : children
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
@@ -155,16 +145,15 @@ const FormMessage = React.forwardRef<
return (
<p
ref={ref}
data-slot="form-message"
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
className={cn("text-sm text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
}
export {
useFormField,
+6 -7
View File
@@ -2,21 +2,20 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
}
export { Input }
+13 -15
View File
@@ -1,26 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
)
}
export { Label }
-128
View File
@@ -1,128 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
-117
View File
@@ -1,117 +0,0 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}
-31
View File
@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
-28
View File
@@ -1,28 +0,0 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
-44
View File
@@ -1,44 +0,0 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }
-48
View File
@@ -1,48 +0,0 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
-160
View File
@@ -1,160 +0,0 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
-31
View File
@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
-140
View File
@@ -1,140 +0,0 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
-773
View File
@@ -1,773 +0,0 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex w-full flex-1 flex-col bg-background",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
-15
View File
@@ -1,15 +0,0 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }
+19 -24
View File
@@ -1,16 +1,14 @@
"use client"
import {
CircleCheck,
Info,
LoaderCircle,
OctagonX,
TriangleAlert,
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
@@ -20,23 +18,20 @@ const Toaster = ({ ...props }: ToasterProps) => {
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheck className="h-4 w-4" />,
info: <Info className="h-4 w-4" />,
warning: <TriangleAlert className="h-4 w-4" />,
error: <OctagonX className="h-4 w-4" />,
loading: <LoaderCircle className="h-4 w-4 animate-spin" />,
}}
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
-29
View File
@@ -1,29 +0,0 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
-117
View File
@@ -1,117 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
-55
View File
@@ -1,55 +0,0 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
-22
View File
@@ -1,22 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }
-129
View File
@@ -1,129 +0,0 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
-35
View File
@@ -1,35 +0,0 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
-30
View File
@@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
-19
View File
@@ -1,19 +0,0 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
-194
View File
@@ -1,194 +0,0 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
-14
View File
@@ -1,14 +0,0 @@
// Supabase Client Setup
// Uncomment this file when you're ready to use Supabase
/*
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
*/
// For now, export a placeholder to avoid import errors
export const supabase = null;
+16
View File
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { cn } from './utils'
describe('cn', () => {
it('joins class names', () => {
expect(cn('px-2', 'py-1')).toBe('px-2 py-1')
})
it('drops falsy values', () => {
expect(cn('px-2', false && 'hidden', undefined)).toBe('px-2')
})
it('lets the later Tailwind class win over an earlier conflicting one', () => {
expect(cn('px-2', 'px-4')).toBe('px-4')
})
})
-95
View File
@@ -1,95 +0,0 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [],
};
export default config;
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from '@playwright/test'
// Playwright specs live here and must never be picked up by Vitest —
// vitest.config.ts scopes itself to src/ for exactly that reason.
test('start page renders', async ({ page }) => {
await page.goto('/')
await expect(
page.getByRole('heading', { name: 'AI Coding Starter Kit' })
).toBeVisible()
})
test('health endpoint answers', async ({ request }) => {
const response = await request.get('/api/health')
expect(response.ok()).toBe(true)
expect(await response.json()).toEqual({ status: 'ok' })
})

Some files were not shown because too many files have changed in this diff Show More