diff --git a/.cursor/rules/api-routes.mdc b/.cursor/rules/api-routes.mdc new file mode 100644 index 0000000..79d428c --- /dev/null +++ b/.cursor/rules/api-routes.mdc @@ -0,0 +1,76 @@ +--- +description: App Router API handler conventions (Next.js + Prisma + Zod) +globs: app/api/**/*.ts,lib/server/**/*.ts +alwaysApply: false +--- + +# API route anatomy + +Every DB-touching handler in `app/api/**/route.ts` follows the same skeleton. +Keep new routes within this shape so auth, config, and validation stay uniform. + +1. **Config guard (first line of the handler).** + + ```typescript + if (!isDatabaseConfigured()) return dbUnavailable(); + ``` + + From `lib/server/env` + `lib/server/responses`. Returns a consistent 503 + when `DATABASE_URL` is missing (local dev, preview builds). + +2. **Auth (when the route requires a user).** + + ```typescript + const user = await getSessionUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + ``` + + From `lib/server/session`. Never read session cookies or tokens directly. + +3. **Body parsing + validation (POST/PUT/PATCH).** + + ```typescript + const parsed = await readLimitedJson(request); + const result = mySchema.safeParse(parsed); + if (!result.success) return jsonFromZodError(result.error); + ``` + + Helpers live in `lib/server/validation/{requestBody,zodHttp}.ts`. All + payload schemas belong in `lib/server/validation/*.ts` (today: + `createFlowSchemas.ts`) — colocate new schemas there rather than inline in + the route. + +4. **Prisma access** via `import { prisma } from "lib/server/db"`. Do not + instantiate `PrismaClient` directly. + +5. **Responses** via `NextResponse.json(...)`. Shared shapes (`dbUnavailable`) + live in `lib/server/responses.ts`; add new shared responses there when a + pattern repeats in two routes. + +# Server-only isolation + +`lib/server/*` is the server boundary. Anything that: + +- imports `@prisma/client`, +- reads secrets from `env`, +- sends email, hashes tokens, or touches sessions + +…lives under `lib/server/`. Never import `lib/server/*` from client +components, `app/components/**`, or any file marked `"use client"`. Shared +logic safe for both sides goes in `lib/*`. + +# Deferred — follow existing code, don't invent + +These areas are still settling. Match whatever the nearest route already does +instead of introducing new patterns: + +- **Rate limiting.** `lib/server/rateLimit.ts` is an in-memory stopgap marked + for replacement. Reuse `rateLimitKey()` where limiting is needed; don't + design a new limiter. +- **Error response shape.** Currently `{ error: string }` + HTTP status. No + error codes yet — don't add a taxonomy until one is designed. +- **Pagination / filtering.** Only `rules/route.ts` paginates (`take` capped + at 100). Mirror it if you add list endpoints; don't invent cursors or + offset contracts unilaterally. diff --git a/.cursor/rules/component-props.mdc b/.cursor/rules/component-props.mdc new file mode 100644 index 0000000..200beec --- /dev/null +++ b/.cursor/rules/component-props.mdc @@ -0,0 +1,51 @@ +--- +description: Component prop conventions — lowercase-canonical enums, Figma traceability +globs: app/components/**/*.{ts,tsx} +alwaysApply: false +--- + +# Component prop alignment + +Figma is the source of truth for component **design** (existence, variants, +visual specification). The codebase implements those components using +idiomatic TypeScript naming. Enum props are **lowercase** in code; PascalCase +is a Figma-side concern only. + +## Enum prop convention + +- Types use lowercase string unions: `"small" | "medium" | "large"`. +- Do NOT add PascalCase variants to type unions. +- Do NOT call normalizers in containers. The container layer is for `memo`, + derived state, prop defaults, and bound logic — not for casing translation. +- Each enum prop has a sibling `__OPTIONS as const` array + exported alongside the type. Storybook `argTypes` and any runtime guard + consume that array as the single source of valid values. + +```typescript +export const CHIP_PALETTE_OPTIONS = ["primary", "secondary"] as const; +export type ChipPaletteValue = (typeof CHIP_PALETTE_OPTIONS)[number]; +``` + +## Figma traceability + +- Container docstring (required on every DS container): `Figma: + "" ()`. +- View root element: `data-figma-node=""` when the view maps to a + distinct Figma node. +- For create-flow screens, node ids come from `CREATE_FLOW_SCREEN_REGISTRY` + in `app/(app)/create/utils/createFlowScreenRegistry.ts`. For everything else, + pull the node id from the Figma file directly. Use `TODO(figma)` as a + placeholder rather than omitting the docstring entirely. + +```typescript +/** + * Figma: "Control / Incrementer" (17857:30943). A compact [ - value + ] + * row used for numeric step inputs. + */ +``` + +## Pasting from Figma + +Figma's "Inspect → Code" output emits PascalCase. When importing a snippet, +lowercase the enum values before committing — same pattern as removing +inline pixel values in favor of design tokens. diff --git a/.cursor/rules/component-structure.mdc b/.cursor/rules/component-structure.mdc new file mode 100644 index 0000000..449befe --- /dev/null +++ b/.cursor/rules/component-structure.mdc @@ -0,0 +1,67 @@ +--- +description: File-structure conventions for design-system components +globs: app/components/**/*.{ts,tsx} +alwaysApply: false +--- + +# Component file structure + +## Split-file pattern (default) + +Anything in `app/components/controls/**` and `app/components/utility/**` uses +a **4-file split**, one folder per component: + +``` +app/components/controls// + .types.ts // Public Props + internal ViewProps + .view.tsx // "use client"; pure render; exports memo(view) + .container.tsx // "use client"; memo; prop normalization & logic + index.tsx // re-exports default + public types +``` + +**Container** (`.container.tsx`): + +- Marked `"use client"`. +- Receives `Props`; computes derived state (clamps, ids, bounds, prop + defaults) and bound event handlers. +- Renders `<View />`. Containers do **not** translate prop casing — + enum props are lowercase end-to-end (see `component-props.mdc`). +- Default export: `memo(Container)` with `.displayName = ""`. +- Carries the Figma docstring (`Figma: "" ()`). + +**View** (`.view.tsx`): + +- Marked `"use client"`. +- Pure render of `ViewProps`. No data fetching, no derived business + logic, no enum casing translation. +- Default export: `memo(View)` with `.displayName = "View"`. + +**Types** (`.types.ts`): + +- Export `Props` (consumer-facing). +- Export `ViewProps` (the shape the view consumes — typically a + resolved superset of `Props`). +- Export any locally-defined value types (`SizeValue`, etc.) sourced + from the matching `*_OPTIONS` array in `lib/propNormalization.ts`. + +**Index** (`index.tsx`): + +```typescript +export { default } from "./.container"; +export type { Props } from "./.types"; +``` + +## Single-file pattern (exception) + +`app/components/buttons/*.tsx` and other trivially-presentational components +can stay as a single file when they have **no derived state and only a +handful of props** (e.g. `Button.tsx`, `InlineTextButton.tsx`). If you find +yourself adding state, side effects, or enum logic, promote it to the split +pattern. + +## Wrapper / group components + +Related composites live in a **sibling folder**, not inside the base +component's folder — mirror `CheckboxGroup/` ↔ `Checkbox/`, +`IncrementerBlock/` ↔ `Incrementer/`, etc. Each gets its own 4-file split. +Consumers import from the folder's `index.tsx`. diff --git a/.cursor/rules/create-flow.mdc b/.cursor/rules/create-flow.mdc new file mode 100644 index 0000000..fb76949 --- /dev/null +++ b/.cursor/rules/create-flow.mdc @@ -0,0 +1,59 @@ +--- +description: Create-flow structure & design-system reuse guardrails +globs: app/(app)/create/**/*.{ts,tsx},messages/en/create/**/*.json +alwaysApply: false +--- + +# Create-flow guardrails + +## Folder & file layout + +- Screens live in `app/(app)/create/screens//Screen.tsx` + where `` mirrors `CreateFlowLayoutKind` (`card`, `select`, + `right-rail`, `completed`, …). File + export name is the **step id**, never + the layout kind (e.g. `DecisionApproachesScreen`, not `RightRailScreen`). +- Step id ↔ layout kind mapping is declared in + `app/(app)/create/utils/createFlowScreenRegistry.ts`. Never branch on layout kind + inside a screen — pick the matching shell (`CreateFlowStepShell` / + `CreateFlowTwoColumnSelectShell`). +- Shared create-flow pieces go in `app/(app)/create/components/` (layout shells, + field composites). Generic primitives go in `app/components/`. + +## Use the design system — don't hand-roll + +Reach for these before writing new markup: + +| Need | Component | +| --- | --- | +| Labelled text-area section in a modal | `app/(app)/create/components/ModalTextAreaField` | +| Toggle-chip row + inline "+ Add" input | `app/(app)/create/components/ApplicableScopeField` | +| `[– value +]` numeric stepper (± label) | `app/components/controls/Incrementer` / `IncrementerBlock` | +| Mid-paragraph "expand / see all" link button | `app/components/buttons/InlineTextButton` | +| Help-icon + label above a control | `app/components/utility/InputLabel` (`helpIcon` prop) | +| Toggle chip (dim-but-clickable) | `Chip` with `state="Disabled" disabled={false}` | +| Card-click → structured creation modal | `Create` with `backdropVariant="loginYellow"` | + +If a screen grows a 2nd inline copy of any pattern above, **extract a shared +component** rather than duplicate. Local section components inside a screen +file are a smell once they're used more than once. + +## Copy & data + +- Step copy lives in `messages/en/create//.json` where + `` is one of `community`, `customRule`, `reviewAndComplete` + (matches Figma stages — see `docs/create-flow.md`). Cross-cutting chrome + (`footer.json`, `topNav.json`, `draftHydration.json`, + `templateReview.json`) and shared layout-shell strings (`select.json`, + `text.json`, `upload.json`) live at the `create/` root. Wire each new + JSON into `messages/en/index.ts` under the matching `create..*` + namespace (see `localization.mdc`). +- Modal `sections` defaults are DB-shaped seed placeholders, not UI + constants — expect replacement with live data. +- Modal `sections` defaults are DB-shaped seed placeholders, not UI + constants — expect replacement with live data. + +## Interaction tracking + +Every user interaction inside a create-flow screen must call +`markCreateFlowInteraction()` from `useCreateFlow()` before mutating state — +progress / footer logic depends on it. diff --git a/.cursor/rules/hooks.mdc b/.cursor/rules/hooks.mdc new file mode 100644 index 0000000..245199e --- /dev/null +++ b/.cursor/rules/hooks.mdc @@ -0,0 +1,59 @@ +--- +description: Custom hooks live in app/hooks; co-locate logic, document via TSDoc. +globs: app/hooks/**/*.{ts,tsx} +alwaysApply: false +--- + +# Custom hooks + +Reusable component logic lives in `app/hooks/`. Each hook is a small, focused +module with a TSDoc block that doubles as the API reference (no separate doc +file). + +## File layout + +- One file per hook: `app/hooks/use.ts`. +- Re-export from `app/hooks/index.ts`. Consumers import from the barrel: + `import { useFoo } from "../hooks";`. +- Companion unit test (when there is non-trivial logic): `tests/unit/hooks/`. + +## Authoring rules + +- Marked as a regular function (`export function useFoo() {}`); React handles + the `use*` naming convention. +- Wrap exposed callbacks in `useCallback` and computed values in `useMemo` + so consumers can list them in dependency arrays without churn. +- Read DOM/browser APIs only inside `useEffect` so the hook stays SSR-safe. +- Never throw on missing globals (e.g. `window`, `gtag`); guard and no-op. + +## TSDoc — the only reference + +Every exported hook gets a TSDoc block with: + +- 1–2 sentence summary. +- `@param` per argument and `@returns` describing the shape. +- `@example` showing the typical call site. + +```ts +/** + * Detect clicks outside a set of elements (e.g. close a dropdown). + * + * @param refs Elements that should NOT trigger the handler. + * @param handler Invoked when a click lands outside every ref. + * @param enabled Toggle without unmounting the consumer (default true). + * + * @example + * useClickOutside([menuRef, buttonRef], () => setOpen(false), open); + */ +export function useClickOutside( + refs: Array>, + handler: (event: MouseEvent | TouchEvent) => void, + enabled = true, +): void { /* ... */ } +``` + +## Container/view consumption + +Hooks belong in **container** files (per `component-structure.mdc`). Views +stay pure and read derived values via props — never call hooks that touch +state or side effects from a view. diff --git a/.cursor/rules/localization.mdc b/.cursor/rules/localization.mdc new file mode 100644 index 0000000..b141dd1 --- /dev/null +++ b/.cursor/rules/localization.mdc @@ -0,0 +1,65 @@ +--- +description: Text localization via messages/ bundles and useMessages() +globs: messages/**/*.{ts,json} +alwaysApply: false +--- + +# Text localization + +All user-visible copy lives in the typed messages bundle under `messages/en/` +and is read via `useMessages()` (fully typed) or `useTranslation()` (dot +notation). Never hard-code user-facing strings in components. + +## File layout + +- `messages/en/.json` for single-file areas (`common.json`, + `navigation.json`, `metadata.json`). +- `messages/en//.json` for areas with multiple buckets: + `components/*.json`, `pages/*.json`. One JSON per component / page — + don't shoehorn unrelated copy into a shared file. +- `messages/en/create//.json` — wizard steps grouped by Figma + stage (`community`, `customRule`, `reviewAndComplete`). Cross-cutting + chrome (footer, top nav, draft hydration, template review) and shared + layout-shell strings (`select.json`, `text.json`, `upload.json`) live at + the `create/` root. +- Optional `"_comment"` at the top of a JSON documents the bundle's purpose. + +## Registration — required + +Every new JSON must be wired into `messages/en/index.ts`: + +```typescript +import createConflictManagement from "./create/customRule/conflictManagement.json"; + +export default { + // … + create: { + customRule: { + conflictManagement: createConflictManagement, + }, + }, +}; +``` + +The default export **is** the type source for `useMessages()`; skipping this +step means consumers can't read your strings and TypeScript won't flag the gap. + +## Access pattern + +```typescript +import { useMessages } from "../contexts/MessagesContext"; + +const m = useMessages(); +const title = m.create.customRule.conflictManagement.page.compactTitle; // fully typed +``` + +Use `useTranslation(namespace)` only when you need dot-path lookup by dynamic +key; prefer direct property access for the type safety. + +## Key conventions + +- **Structural keys**: camelCase (`compactTitle`, + `sectionHeadings.corePrinciple`). +- **Content ids**: match the id consumers already use (card id, step id, URL + segment) — typically kebab-case (`"in-person-meetings"`, + `"peer-mediation"`). diff --git a/.cursor/rules/routes.mdc b/.cursor/rules/routes.mdc new file mode 100644 index 0000000..1f9d9dd --- /dev/null +++ b/.cursor/rules/routes.mdc @@ -0,0 +1,88 @@ +--- +description: App Router route organization (groups, layouts, chrome composition) +globs: app/**/*.{ts,tsx} +alwaysApply: false +--- + +# Route organization + +Top-level routes live inside **route groups** so each surface owns its own +layout and chrome. Groups are wrapping folders in `(parens)` — they organize +the file tree without affecting URLs. + +## Group map + +| Group | URL surface | Audience | Chrome | +|---|---|---|---| +| `app/(marketing)/` | `/`, `/learn`, `/blog`, `/templates`, future public pages | Public, indexable | TopNav (via root) + marketing `