@polymorphism-tech/morph-spec 9.4.0 → 9.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +23 -33
- package/framework/agents.json +6 -7
- package/framework/docs/CLI.md +3 -3
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/comment-guard.js +1 -0
- package/framework/hooks/claude-code/core/denylist.json +63 -1
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
- package/framework/hooks/claude-code/core/gate-guard.js +0 -8
- package/framework/hooks/claude-code/core/loop-detect.js +0 -3
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
- package/framework/hooks/claude-code/core/validate-completion.js +0 -4
- package/framework/hooks/claude-code/core/worktree-context.js +0 -2
- package/framework/hooks/claude-code/statusline.py +3 -44
- package/framework/hooks/shared/build-check.js +12 -19
- package/framework/hooks/shared/comment-rules.js +11 -1
- package/framework/hooks/shared/hook-response.js +0 -4
- package/framework/hooks/shared/payload-utils.js +1 -35
- package/framework/hooks/shared/phase-utils.js +0 -12
- package/framework/hooks/shared/slice-progress.js +0 -2
- package/framework/hooks/shared/state-reader.js +0 -19
- package/framework/hooks/shared/telemetry-logger.js +1 -1
- package/framework/rules/frontend-standards.md +1 -2
- package/framework/rules/nextjs-standards.md +9 -8
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +32 -29
- package/framework/standards/STANDARDS.meta.json +13 -15
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/frontend/admin-app/data-table.md +1 -3
- package/framework/standards/frontend/admin-app/page-structure.md +8 -4
- package/framework/standards/frontend/nextjs/app-router.md +3 -4
- package/framework/standards/frontend/nextjs/components.md +2 -15
- package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
- package/framework/standards/frontend/nextjs/lint.md +141 -0
- package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
- package/framework/standards/frontend/nextjs/project-structure.md +3 -2
- package/framework/standards/frontend/nextjs/state-management.md +6 -7
- package/framework/templates/README.md +4 -4
- package/framework/templates/REGISTRY.json +21 -309
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
- package/package.json +3 -2
- package/src/core/paths/output-schema.js +0 -9
- package/src/core/state/feature-identity.js +0 -2
- package/src/core/state/state-manager.js +1 -169
- package/src/core/templates/template-renderer.js +0 -23
- package/src/dashboard/app/lib/format.js +0 -7
- package/src/dashboard/app/lib/phases.js +0 -7
- package/src/dashboard/scanner/activity-scanner.js +0 -4
- package/src/lib/ai/ai-project-scan.js +1 -6
- package/src/lib/graph/graphify-runtime.js +0 -1
- package/src/lib/mutation/snapshot.js +1 -5
- package/src/lib/risk-detector.js +0 -5
- package/src/lib/stack/stack-profile.js +0 -4
- package/src/lib/stack/stack-scan.js +15 -4
- package/src/lib/tasks/eval-node.js +1 -7
- package/src/lib/validators/validation-runner.js +0 -43
- package/src/lib/validators/verify-runner.js +18 -2
- package/src/utils/agents-installer.js +12 -19
- package/src/utils/claude-settings-manager.js +0 -28
- package/src/utils/file-copier.js +0 -53
- package/framework/hooks/shared/activity-logger.js +0 -131
- package/framework/standards/frontend/design-system/naming.md +0 -68
- package/src/commands/validation/index.js +0 -2
- package/src/lib/validators/code-budget/index.js +0 -2
- package/src/lib/validators/contracts/index.js +0 -1
- package/src/lib/validators/nextjs/index.js +0 -1
- package/src/lib/validators/packages/index.js +0 -1
- package/src/lib/validators/reuse/index.js +0 -2
|
@@ -1211,26 +1211,7 @@
|
|
|
1211
1211
|
"tokens": "#stagger--timing",
|
|
1212
1212
|
"principles": "#css-animation-patterns"
|
|
1213
1213
|
},
|
|
1214
|
-
"digest": "CSS animation library for design-system.css
|
|
1215
|
-
},
|
|
1216
|
-
{
|
|
1217
|
-
"id": "frontend-design-system-naming",
|
|
1218
|
-
"name": "naming",
|
|
1219
|
-
"path": "frontend/design-system/naming.md",
|
|
1220
|
-
"category": "frontend",
|
|
1221
|
-
"subcategory": "design-system",
|
|
1222
|
-
"tags": [
|
|
1223
|
-
"frontend",
|
|
1224
|
-
"design-system"
|
|
1225
|
-
],
|
|
1226
|
-
"stacks": [
|
|
1227
|
-
"*"
|
|
1228
|
-
],
|
|
1229
|
-
"anchors": {
|
|
1230
|
-
"blocks": "#bem-for-reusable-components",
|
|
1231
|
-
"tokens": "#css-variables-with-namespace"
|
|
1232
|
-
},
|
|
1233
|
-
"digest": "CSS naming rests on four principles: a page prefix prevents conflicts, BEM for components gives clear hierarchy, namespaced variables avoid library collisions, and state suffixes keep modifiers consistent. Page-specific elements take the page name as prefix (.home-hero, .checkout-form, .dashboard-stats, .auth-form); reusable components use BEM: block .card, element .card__header, modifier .card--featured. Rule: BEM for reusable components, page prefix for page-specific elements. Custom properties go in :root under the --app- namespace (--app-primary, --app-spacing-md, --app-font-heading). State modifiers use fixed suffixes: --active, --disabled, --loading, --error (.button, .button--active, .button--disabled)."
|
|
1214
|
+
"digest": "CSS animation library for design-system.css (keyframes and the `.animate-*`/`.stagger-*` utility classes live in the file, not in a rule). For staggered lists set `animation-delay` AND `animation-fill-mode: backwards` (marked CRITICAL: otherwise elements flash before the delay). Always include the `@media (prefers-reduced-motion: reduce)` block (0.01ms durations, one iteration). Beyond CSS: Motion (formerly Framer Motion) for simple enter/exit, layout and gesture transitions (`frontend/nextjs/motion-patterns.md`); GSAP for ScrollTrigger, SVG morph, timelines (`frontend/gsap/gsap-core.md`)."
|
|
1234
1215
|
},
|
|
1235
1216
|
{
|
|
1236
1217
|
"id": "frontend-design-system-premium-finish",
|
|
@@ -1341,7 +1322,7 @@
|
|
|
1341
1322
|
"layout": "#root-layout--required-providers",
|
|
1342
1323
|
"routing": "#app-directory-structure"
|
|
1343
1324
|
},
|
|
1344
|
-
"digest": "Next.js 15 App Router, TypeScript only (no .js/.jsx). Server Components by default; add `'use client'` only for interaction (onClick, useState/useEffect/useRef), browser APIs, TanStack Query hooks and interactive shadcn/ui. `app/` is for routing only: business logic goes to `features/`, never in `page.tsx`. Never fetch data in a Client Component when a Server Component can
|
|
1325
|
+
"digest": "Next.js 15 App Router, TypeScript only (no .js/.jsx). Server Components by default; add `'use client'` only for interaction (onClick, useState/useEffect/useRef), browser APIs, TanStack Query hooks and interactive shadcn/ui. `app/` is for routing only: business logic goes to `features/`, never in `page.tsx`. Never fetch data in a Client Component when a Server Component can,. Co-locate `loading.tsx` and `error.tsx` beside `page.tsx`. Server Components call the .NET backend directly using the dedicated `BACKEND_API_PREFIX` from `@/lib/api/prefixes`, never the same-origin proxy prefix used by Client Components; always check `res.ok`. Avoid `route.ts` API routes (use the .NET API). The root `layout.tsx` wraps children in `QueryProvider` and the sonner `Toaster`. Fetch in `useEffect` and `'use client'` in the root layout are lint errors (frontend-nextjs-lint)."
|
|
1345
1326
|
},
|
|
1346
1327
|
{
|
|
1347
1328
|
"id": "frontend-nextjs-components",
|
|
@@ -1360,7 +1341,7 @@
|
|
|
1360
1341
|
"patterns": "#three-tier-hierarchy",
|
|
1361
1342
|
"shadcn": "#tier-1--shadcnui-primitives"
|
|
1362
1343
|
},
|
|
1363
|
-
"digest": "Three tiers (Next.js 16 + shadcn 4 CLI): `components/ui/` is shadcn CLI output (add via `npx shadcn@latest add`, NEVER edit because the CLI regenerates it; wrap it in `components/` instead); `components/` holds shared composed components with no business logic
|
|
1344
|
+
"digest": "Three tiers (Next.js 16 + shadcn 4 CLI): `components/ui/` is shadcn CLI output (add via `npx shadcn@latest add`, NEVER edit because the CLI regenerates it; wrap it in `components/` instead); `components/` holds shared composed components with no business logic (importing `features/` is a lint error, frontend-nextjs-lint); `features/*/components/` is feature-scoped and may know the domain. Add `'use client'` only if the component uses hooks, events or browser APIs (not on every component: unnecessary client JS). Never pass raw API/fetch data as props: the feature maps it to typed props. The shared data table is specified once in `admin-app/data-table.md`: do not write a second `components/data-table.tsx`; a feature list is a Tier 3 component that owns the columns and the URL-to-table mapping."
|
|
1364
1345
|
},
|
|
1365
1346
|
{
|
|
1366
1347
|
"id": "frontend-nextjs-data-fetching",
|
|
@@ -1379,7 +1360,7 @@
|
|
|
1379
1360
|
"server": "#server-component--tanstack-query-handoff",
|
|
1380
1361
|
"client": "#usequery-hook-pattern"
|
|
1381
1362
|
},
|
|
1382
|
-
"digest": "Server Components do the initial load and call the .NET backend directly with BACKEND_API_PREFIX; Client Components never call the backend — they hit a same-origin proxy route with PROXY_API_PREFIX. Two named constants, never one shared API_PREFIX: the side that drifts breaks silently at runtime. Client data is TanStack Query 5: useQuery for reads, useMutation for writes, then invalidateQueries({ queryKey: userKeys.lists() }) (object syntax). Keys come from a per-feature key factory, never hardcoded ['users']. Every response is parsed with a Zod schema. QueryClient
|
|
1363
|
+
"digest": "Server Components do the initial load and call the .NET backend directly with BACKEND_API_PREFIX; Client Components never call the backend — they hit a same-origin proxy route with PROXY_API_PREFIX. Two named constants, never one shared API_PREFIX: the side that drifts breaks silently at runtime. Client data is TanStack Query 5: useQuery for reads, useMutation for writes, then invalidateQueries({ queryKey: userKeys.lists() }) (object syntax). Keys come from a per-feature key factory, never hardcoded ['users']. Every response is parsed with a Zod schema. QueryClient is created once in useState; the Server Component passes initialData to the client hook. FORBIDDEN: fetching a whole collection to page/sort/filter in the browser — page on the server, query in the URL. Fetch in useEffect and a DB driver in Next are lint errors (frontend-nextjs-lint)."
|
|
1383
1364
|
},
|
|
1384
1365
|
{
|
|
1385
1366
|
"id": "frontend-nextjs-forms",
|
|
@@ -1420,7 +1401,7 @@
|
|
|
1420
1401
|
"scroll": "#section-3-scroll-animations",
|
|
1421
1402
|
"accessibility": "#section-7-wcag-accessibility-mandatory"
|
|
1422
1403
|
},
|
|
1423
|
-
"digest": "Component-level motion with Motion for React (the stack table's 'Framer Motion', its former name; motion 12+, current npm major 14): npm install motion and import from 'motion/react'. framer-motion is not deprecated (motion depends on it and re-exports it), but a codebase keeps one import path, motion/react
|
|
1404
|
+
"digest": "Component-level motion with Motion for React (the stack table's 'Framer Motion', its former name; motion 12+, current npm major 14): npm install motion and import from 'motion/react'. framer-motion is not deprecated (motion depends on it and re-exports it), but a codebase keeps one import path, motion/react (a framer-motion import is a lint error, frontend-nextjs-lint). Variants live in motion/variants.ts (fadeIn, slideInUp/Down/Left/Right, scaleIn, staggerContainer, staggerItem, pageTransition) and take durations and easings from @/lib/design-tokens (fast 0.15, base 0.3); never hardcode duration numbers or easing arrays. Scroll reveals: whileInView with viewport once: true for simple cases, useInView for control. Page transitions: AnimatePresence mode='wait' keyed by usePathname(), inside a 'use client' PageTransition provider -- the root app/layout.tsx stays a Server Component (no 'use client', keeps metadata) and just renders the provider. WCAG is mandatory: useReducedMotion() strips x/y/scale and keeps opacity, plus a prefers-reduced-motion CSS fallback. Scroll-driven scenes (ScrollTrigger, Lenis) belong to GSAP; never share an element with it."
|
|
1424
1405
|
},
|
|
1425
1406
|
{
|
|
1426
1407
|
"id": "frontend-nextjs-naming-conventions",
|
|
@@ -1439,7 +1420,7 @@
|
|
|
1439
1420
|
"files": "#complete-reference-table",
|
|
1440
1421
|
"components": "#complete-reference-table"
|
|
1441
1422
|
},
|
|
1442
|
-
"digest": "Next.js naming (prevents Linux case-sensitivity bugs). File names are ALWAYS kebab-case (`user-card.tsx`, never `UserCard.tsx`);
|
|
1423
|
+
"digest": "Next.js naming (prevents Linux case-sensitivity bugs). File names are ALWAYS kebab-case (`user-card.tsx`, never `UserCard.tsx`); components export PascalCase (`UserCard`); hook files are kebab-case too (`use-users.ts` -> `useUsers`, `use-user.ts` for a single GET, `use-create-user.ts`, `use-update-user.ts`, `use-delete-user.ts`). Zod schema files end in `.schemas.ts` (`createUserSchema`) and type files in `.types.ts`, with types derived via `z.infer` instead of hand-written duplicates. A feature `index.ts` re-exports. Named exports and the hook `use` prefix are lint errors, not prose (frontend-nextjs-lint). Never mix cases within a category (no `UserCard.tsx` next to `user-profile.tsx`)."
|
|
1443
1424
|
},
|
|
1444
1425
|
{
|
|
1445
1426
|
"id": "frontend-nextjs-nextjs-patterns",
|
|
@@ -1458,7 +1439,7 @@
|
|
|
1458
1439
|
"auth": "#route-handlers-bff-pattern",
|
|
1459
1440
|
"caching": "#react-query--neon"
|
|
1460
1441
|
},
|
|
1461
|
-
"digest": "Next.js 15 App Router (not Pages Router): Server Components by default, `'use client'` only for state, effects, events or browser APIs; colocate `loading.tsx` and `error.tsx`
|
|
1442
|
+
"digest": "Next.js 15 App Router (not Pages Router): Server Components by default, `'use client'` only for state, effects, events or browser APIs; colocate `loading.tsx` and `error.tsx` with `page.tsx`. Route Handlers act as a BFF: check `auth.getSession()` (401 if absent) and delegate to the .NET API; database access lives in the .NET backend via EF Core (a DB driver in Next is a lint error, frontend-nextjs-lint). Validate input with Zod on client and server (`safeParse`, 400). Fetch with TanStack Query (`useQuery`/`useMutation`, `invalidateQueries` on success). Forms: react-hook-form + `zodResolver`. shadcn is copied via `npx shadcn@latest add`, not an npm dependency. Keep `\"strict\": true` plus `noUncheckedIndexedAccess` (agents self-correct from compiler errors); API responses are a Zod schema + `z.infer`."
|
|
1462
1443
|
},
|
|
1463
1444
|
{
|
|
1464
1445
|
"id": "frontend-nextjs-project-structure",
|
|
@@ -1477,7 +1458,7 @@
|
|
|
1477
1458
|
"structure": "#canonical-folder-tree",
|
|
1478
1459
|
"organization": "#feature-index-pattern"
|
|
1479
1460
|
},
|
|
1480
|
-
"digest": "Next.js 15 App Router, src/ layout with the @/ alias, feature-based. src/app/ is routing only (layout, page, loading, error)
|
|
1461
|
+
"digest": "Next.js 15 App Router, src/ layout with the @/ alias, feature-based. src/app/ is routing only (layout, page, loading, error); screen logic lives in features/. Each src/features/{feature-name}/ holds components/, hooks/ (TanStack Query hooks such as use-users.ts, use-create-user.ts), types/ (*.types.ts plus *.schemas.ts with Zod and z.infer) and an index.ts that is the explicit public API. NEVER import one feature from another (extract to components/ or lib/); components/ is presentation only with NO business logic, and components/ui/ is shadcn, never edited directly. Deep imports from outside a feature, components/ -> features/, features/ -> app/ and shared hooks/ -> the API client are lint errors (frontend-nextjs-lint). lib/ holds api-client.ts (typed fetch wrapper for the .NET API) and query-client.tsx; types/api.ts has PaginatedResponse<T> and ApiError; env.mjs validates env vars with Zod."
|
|
1481
1462
|
},
|
|
1482
1463
|
{
|
|
1483
1464
|
"id": "frontend-nextjs-state-management",
|
|
@@ -1496,7 +1477,7 @@
|
|
|
1496
1477
|
"patterns": "#state-decision-tree",
|
|
1497
1478
|
"server": "#server-state-tanstack-query"
|
|
1498
1479
|
},
|
|
1499
|
-
"digest": "Server state (anything from the .NET API) lives in TanStack Query, never copied into `useState` via `useEffect`; derive state from server data instead of syncing it. Local UI state (modal open, form step, selected id) uses `useState`. A list's page, sort and filters, and a page's active tab, go in the URL (`searchParams`), never `useState`, so a refresh or shared link reopens the same view. Do not use React Context for server state (no caching or dedup); Context is for truly global theme, auth user or locale
|
|
1480
|
+
"digest": "Server state (anything from the .NET API) lives in TanStack Query, never copied into `useState` via `useEffect`; derive state from server data instead of syncing it. Local UI state (modal open, form step, selected id) uses `useState`. A list's page, sort and filters, and a page's active tab, go in the URL (`searchParams`), never `useState`, so a refresh or shared link reopens the same view. Do not use React Context for server state (no caching or dedup); Context is for truly global theme, auth user or locale. Zustand/Redux imports are lint errors (frontend-nextjs-lint); the one exception is an eslint-disable-next-line with the reason in a single store file, only if ALL hold: state needed in 5+ unrelated components, complex update logic, Context causes measurable re-render problems, and the state is not from the server."
|
|
1500
1481
|
},
|
|
1501
1482
|
{
|
|
1502
1483
|
"id": "frontend-nextjs-testing",
|
|
@@ -1830,7 +1811,7 @@
|
|
|
1830
1811
|
"not-worth": "#o-que-não-merece",
|
|
1831
1812
|
"size": "#tamanho"
|
|
1832
1813
|
},
|
|
1833
|
-
"digest": "Default is NO comment: code explains itself through names, types and tests. Before commenting, rename, extract a function or named constant, encode the state in the type, or write a test; comment only what none of these can express: an invariant the type does not express, an order that matters, an external quirk (third-party API, library bug with its link), or the contract of a package published for consumers outside the repo. Never process history (task, UC, ADR, FR, Gate, evaluator, spec.md, mockup: commit message and plan.md (## Decisões); in a test the link to the postcondition goes in the test NAME), never restate the code, no XML doc/JSDoc on application members that repeats the signature, no file headers, section banners, #region, commented-out code or loose TODOs, no claims about behavior (make them test assertions). One line is normal, ~3 is the ceiling; more than one comment line per ten code lines means the names are not doing the work. A heavily commented neighbour file is not a model. The comment-guard hook blocks an edit that ADDS process history, a banner/#region, a block over 3 lines or a doc-comment outside a published package; the verify validators node (code-budget) warns on added comments that cite process and on added comment density."
|
|
1814
|
+
"digest": "Default is NO comment: code explains itself through names, types and tests. Before commenting, rename, extract a function or named constant, encode the state in the type, or write a test; comment only what none of these can express: an invariant the type does not express, an order that matters, an external quirk (third-party API, library bug with its link), or the contract of a package published for consumers outside the repo. Never process history (task, UC, ADR, FR, Gate, evaluator, spec.md, mockup: commit message and plan.md (## Decisões); in a test the link to the postcondition goes in the test NAME), never restate the code, no XML doc/JSDoc on application members that repeats the signature, no file headers, section banners, #region, commented-out code or loose TODOs, no claims about behavior (make them test assertions). One line is normal, ~3 is the ceiling; more than one comment line per ten code lines means the names are not doing the work. A heavily commented neighbour file is not a model. The comment-guard hook blocks an edit that ADDS process history, a banner/#region, commented-out code, a block over 3 lines or a doc-comment outside a published package; the verify validators node (code-budget) warns on added comments that cite process and on added comment density."
|
|
1834
1815
|
},
|
|
1835
1816
|
{
|
|
1836
1817
|
"id": "ai-agents-tool-adapters",
|
|
@@ -1926,6 +1907,28 @@
|
|
|
1926
1907
|
"not-a-rule": "#o-que-não-é-regra"
|
|
1927
1908
|
},
|
|
1928
1909
|
"digest": "Rules with a reason that must never break are build errors, not prose: three files at the solution root, no .csproj change. Directory.Build.props adds Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0 (PrivateAssets=all), BannedSymbols.txt as AdditionalFiles and morph.globalconfig as GlobalAnalyzerConfigFiles (with Central Package Management the version goes to Directory.Packages.props). BannedSymbols.txt bans M:System.Guid.NewGuid (use Guid.CreateVersion7(): time-ordered, does not fragment the Postgres PK index). morph.globalconfig sets RS0030 and CA2016 (a CancellationToken received and not forwarded) to error. A new banned API is one BannedSymbols.txt line with the reason after the semicolon, never a prose rule. sealed, primary constructors, var, expression bodies and using order are taste: neither prose nor analyzer; follow the neighbouring file."
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
"id": "frontend-nextjs-lint",
|
|
1913
|
+
"name": "lint",
|
|
1914
|
+
"path": "frontend/nextjs/lint.md",
|
|
1915
|
+
"category": "frontend",
|
|
1916
|
+
"subcategory": "nextjs",
|
|
1917
|
+
"tags": [
|
|
1918
|
+
"frontend",
|
|
1919
|
+
"nextjs",
|
|
1920
|
+
"lint",
|
|
1921
|
+
"eslint"
|
|
1922
|
+
],
|
|
1923
|
+
"stacks": [
|
|
1924
|
+
"nextjs"
|
|
1925
|
+
],
|
|
1926
|
+
"anchors": {
|
|
1927
|
+
"files": "#arquivos",
|
|
1928
|
+
"rules": "#o-que-cada-regra-compra",
|
|
1929
|
+
"not-a-rule": "#o-que-não-é-regra"
|
|
1930
|
+
},
|
|
1931
|
+
"digest": "Rules with a reason that a machine can see are lint errors, not prose: one eslint.config.mjs at the Next app root, flat config spreading eslint-config-next/core-web-vitals and eslint-config-next/typescript, no new plugin. ESLint 9 (eslint-plugin-import/react peer range; the bundled typescript-eslint wants TypeScript <6.1); Next 16 next build no longer lints; the verify build node runs eslint --quiet on the files the feature changed (legacy lint debt never reddens a feature), from the app root, when eslint.config.* and eslint are present. no-restricted-imports bans framer-motion (one import path, motion/react), zustand/redux/@reduxjs/toolkit (server state is TanStack Query, page and filters in the URL), DB drivers pg/@neondatabase/serverless/@prisma/client/drizzle-orm/postgres (Next never touches the database; the .NET API does), the legacy shadcn @/components/ui/form, and @/features/*/* deep imports outside src/features (use the feature index.ts). import/no-restricted-paths: src/components never imports src/features, src/features never imports src/app, src/hooks never imports the API client (lib/api-client.ts, lib/api). no-restricted-syntax: fetch or apiClient called inside useEffect, and 'use client' in the root app/layout.tsx. import/no-default-export except Next convention files (page, layout, loading, error, not-found, template, default, route, sitemap, robots, icons, OG images) and *.config.*. @typescript-eslint/no-explicit-any error, no-console warn. A new restriction is one line in this file with the reason in its message; a one-off exception is eslint-disable-next-line with the reason. CSS class naming, animation class catalogs, optional className, Zod on props, motion counts and offsets, QueryClient defaults and prop-drilling depth are taste: neither prose nor lint."
|
|
1929
1932
|
}
|
|
1930
1933
|
]
|
|
1931
1934
|
}
|
|
@@ -819,17 +819,6 @@
|
|
|
819
819
|
"verifiedAgainst": "CSS animations/transitions (language-level, no framework).",
|
|
820
820
|
"provedBy": []
|
|
821
821
|
},
|
|
822
|
-
"frontend-design-system-naming": {
|
|
823
|
-
"aliases": [
|
|
824
|
-
"BEM",
|
|
825
|
-
"CSS naming",
|
|
826
|
-
"class naming",
|
|
827
|
-
"design token naming",
|
|
828
|
-
"component naming"
|
|
829
|
-
],
|
|
830
|
-
"verifiedAgainst": "CSS (BEM + custom properties — language-level, no framework).",
|
|
831
|
-
"provedBy": []
|
|
832
|
-
},
|
|
833
822
|
"frontend-design-system-premium-finish": {
|
|
834
823
|
"aliases": [
|
|
835
824
|
"premium finish",
|
|
@@ -1251,6 +1240,19 @@
|
|
|
1251
1240
|
],
|
|
1252
1241
|
"verifiedAgainst": ".NET SDK 10.0.402, Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0 (sonda: `Guid.NewGuid()` → erro RS0030, token não repassado → erro CA2016, código conforme → 0 erros).",
|
|
1253
1242
|
"provedBy": []
|
|
1243
|
+
},
|
|
1244
|
+
"frontend-nextjs-lint": {
|
|
1245
|
+
"aliases": [
|
|
1246
|
+
"eslint",
|
|
1247
|
+
"eslint.config.mjs",
|
|
1248
|
+
"lint rule",
|
|
1249
|
+
"no-restricted-imports",
|
|
1250
|
+
"no-restricted-paths",
|
|
1251
|
+
"eslint-config-next",
|
|
1252
|
+
"default export"
|
|
1253
|
+
],
|
|
1254
|
+
"verifiedAgainst": "eslint 9.39.5, eslint-config-next 16.4.2 (typescript-eslint 8.71.2, eslint-plugin-import 2.32.0), next 16.4.2, typescript 6.0.3 (sonda: um arquivo ruim por regra → erro da regra esperada, `no-console` → aviso, arquivos conformes e o scaffold `morph-spec scaffold nextjs` → 0 problemas).",
|
|
1255
|
+
"provedBy": []
|
|
1254
1256
|
}
|
|
1255
1257
|
},
|
|
1256
1258
|
"sharedAliases": {
|
|
@@ -1323,10 +1325,6 @@
|
|
|
1323
1325
|
"architecture-vertical-slice-vertical-slice",
|
|
1324
1326
|
"backend-dotnet-vsa-handler-patterns"
|
|
1325
1327
|
],
|
|
1326
|
-
"component naming": [
|
|
1327
|
-
"frontend-design-system-naming",
|
|
1328
|
-
"frontend-nextjs-naming-conventions"
|
|
1329
|
-
],
|
|
1330
1328
|
"video generation": [
|
|
1331
1329
|
"ai-agents-media-video",
|
|
1332
1330
|
"frontend-design-system-ai-video-generation"
|
|
@@ -93,7 +93,7 @@ await upsert({ key: doc.sourceId, hash: sha256(doc.body) });
|
|
|
93
93
|
## Na revisão
|
|
94
94
|
|
|
95
95
|
Na hora da edição, o hook `comment-guard` bloqueia o comentário **adicionado** que é história de
|
|
96
|
-
processo, banner/`#region`, bloco de mais de 3 linhas ou `///`/JSDoc em projeto que não é pacote
|
|
96
|
+
processo, banner/`#region`, código comentado, bloco de mais de 3 linhas ou `///`/JSDoc em projeto que não é pacote
|
|
97
97
|
publicado. Comentário que o arquivo já tinha nunca é acusado. Repetir o código em uma linha não é
|
|
98
98
|
detectável por forma: fica com a revisão abaixo.
|
|
99
99
|
|
|
@@ -82,9 +82,7 @@ The page parses the query, renders the toolbar, and wraps the results in a `Susp
|
|
|
82
82
|
// app/(admin)/products/page.tsx
|
|
83
83
|
import { Suspense } from 'react';
|
|
84
84
|
import { DataTableSkeleton } from '@/components/data-table-skeleton';
|
|
85
|
-
import { ProductResults } from '@/features/products
|
|
86
|
-
import { ProductToolbar } from '@/features/products/components/product-toolbar';
|
|
87
|
-
import { productListQuerySchema } from '@/features/products/types/product-list.schemas';
|
|
85
|
+
import { ProductResults, ProductToolbar, productListQuerySchema } from '@/features/products';
|
|
88
86
|
|
|
89
87
|
export default async function ProductsPage({
|
|
90
88
|
searchParams,
|
|
@@ -35,10 +35,14 @@ The page reads `tab` from `searchParams` and renders only that tab's content on
|
|
|
35
35
|
|
|
36
36
|
```tsx
|
|
37
37
|
// app/(admin)/products/[id]/page.tsx
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
import {
|
|
39
|
+
PRODUCT_TABS,
|
|
40
|
+
ProductDetailsTab,
|
|
41
|
+
ProductHistoryTab,
|
|
42
|
+
ProductTabs,
|
|
43
|
+
ProductVariantsTab,
|
|
44
|
+
type ProductTab,
|
|
45
|
+
} from '@/features/products';
|
|
42
46
|
|
|
43
47
|
export default async function ProductPage({
|
|
44
48
|
params,
|
|
@@ -16,9 +16,9 @@ Next.js App Router with TypeScript strict mode. Server Components by default, Cl
|
|
|
16
16
|
- ALWAYS default to Server Components — add `'use client'` only when needed
|
|
17
17
|
- ALWAYS keep `app/` directory for routing only — no business logic in page files
|
|
18
18
|
- NEVER put data fetching in Client Components when a Server Component can do it
|
|
19
|
-
- NEVER use `useEffect` to fetch data — use Server Components or TanStack Query
|
|
20
19
|
- ALWAYS co-locate loading/error UI: `loading.tsx`, `error.tsx` next to `page.tsx`
|
|
21
20
|
- ALWAYS use TypeScript — no `.js` or `.jsx` files in the project
|
|
21
|
+
- Fetch inside `useEffect` and `'use client'` in the root `layout.tsx` are lint errors (`frontend-nextjs-lint`)
|
|
22
22
|
|
|
23
23
|
## Server vs Client Components
|
|
24
24
|
|
|
@@ -68,7 +68,7 @@ src/app/
|
|
|
68
68
|
| `page.tsx` | Route segment UI | Server Component (default) |
|
|
69
69
|
| `layout.tsx` | Shared UI wrapper | Server Component |
|
|
70
70
|
| `loading.tsx` | Suspense fallback | Server Component |
|
|
71
|
-
| `error.tsx` | Error boundary |
|
|
71
|
+
| `error.tsx` | Error boundary | Client Component |
|
|
72
72
|
| `not-found.tsx` | 404 page | Server Component |
|
|
73
73
|
| `route.ts` | API route (avoid — use .NET API) | — |
|
|
74
74
|
|
|
@@ -80,7 +80,7 @@ the same-origin proxy prefix Client Components use. See `frontend/nextjs/data-fe
|
|
|
80
80
|
|
|
81
81
|
```tsx
|
|
82
82
|
// app/(dashboard)/users/page.tsx
|
|
83
|
-
import { UserList } from '@/features/users
|
|
83
|
+
import { UserList } from '@/features/users';
|
|
84
84
|
import { BACKEND_API_PREFIX } from '@/lib/api/prefixes'; // server-direct prefix ONLY
|
|
85
85
|
|
|
86
86
|
async function getUsers(): Promise<User[]> {
|
|
@@ -123,7 +123,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
123
123
|
| Wrong | Right | Why |
|
|
124
124
|
|-------|-------|-----|
|
|
125
125
|
| `'use client'` on every component | Only add when needed | Kills SSR benefits, bloats JS bundle |
|
|
126
|
-
| `useEffect(() => { fetch(...) }, [])` | Server Component fetch or TanStack Query | Two renders, no caching, no SSR |
|
|
127
126
|
| Business logic in `page.tsx` | Move to `features/` | pages are routes, not controllers |
|
|
128
127
|
| `fetch` without error handling | Always check `res.ok` | Silent failures in production |
|
|
129
128
|
|
|
@@ -14,10 +14,10 @@ Three-tier component hierarchy. `components/ui/` is shadcn primitives (never edi
|
|
|
14
14
|
## Core Rules
|
|
15
15
|
|
|
16
16
|
- NEVER edit files in `components/ui/` — they are regenerated by shadcn CLI
|
|
17
|
-
- NEVER import from `features/` inside `components/` — components know nothing about domain
|
|
18
17
|
- ALWAYS compose shadcn primitives in `components/` instead of editing them
|
|
19
18
|
- ALWAYS add `'use client'` only if the component uses hooks, events, or browser APIs
|
|
20
19
|
- NEVER pass raw API data directly to a component — transform to props first
|
|
20
|
+
- `components/` importing `features/` and the legacy `@/components/ui/form` are lint errors (`frontend-nextjs-lint`)
|
|
21
21
|
|
|
22
22
|
## Three-Tier Hierarchy
|
|
23
23
|
|
|
@@ -89,26 +89,13 @@ export function UsersPageHeader({ canInvite }: { canInvite: boolean }) {
|
|
|
89
89
|
|
|
90
90
|
A feature **list** is a Tier 3 component too: the feature owns the columns and the URL ⇄ table mapping, the shared table owns the rendering — `admin-app/data-table.md` → "The Feature Table".
|
|
91
91
|
|
|
92
|
-
## Component Props Conventions
|
|
93
|
-
|
|
94
|
-
```tsx
|
|
95
|
-
interface UserCardProps {
|
|
96
|
-
user: User;
|
|
97
|
-
onEdit?: (id: string) => void;
|
|
98
|
-
className?: string; // Always allow className for Tailwind override
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function UserCard({ user, onEdit, className }: UserCardProps) {}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
92
|
## Common Mistakes
|
|
105
93
|
|
|
106
94
|
| Wrong | Right | Why |
|
|
107
95
|
|-------|-------|-----|
|
|
108
96
|
| Edit `components/ui/button.tsx` | Wrap in `components/action-button.tsx` | shadcn CLI overwrites it |
|
|
109
|
-
| `import { useUsers } from '@/features/users'` in `components/` | Move to a feature component | Breaks tier isolation |
|
|
110
97
|
| `'use client'` on all components | Only on interactive ones | Unnecessary client JS |
|
|
111
|
-
| Pass raw fetch response as prop |
|
|
98
|
+
| Pass raw fetch response as prop | Map it to typed props in the feature | The component stops depending on the API shape |
|
|
112
99
|
|
|
113
100
|
---
|
|
114
101
|
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
Server Components for initial page load. TanStack Query for client mutations and interactive data.
|
|
12
|
+
Server Components for initial page load. TanStack Query for client mutations and interactive data.
|
|
13
13
|
|
|
14
14
|
## Core Rules
|
|
15
15
|
|
|
16
16
|
- ALWAYS use Server Components for initial data that doesn't require interactivity
|
|
17
17
|
- ALWAYS use TanStack Query (`useQuery`, `useMutation`) for client-side data needs
|
|
18
|
-
- NEVER use `useEffect(() => { fetch(...) }, [])` — this is the old pattern
|
|
19
18
|
- ALWAYS validate API responses with Zod before using them
|
|
20
19
|
- ALWAYS use query key factories for consistent cache invalidation
|
|
21
20
|
- NEVER fetch a whole collection to page, sort or filter it in the browser — a list is paged on the server, with the query in the URL (`admin-app/data-table.md`)
|
|
21
|
+
- Fetch inside `useEffect` and a DB driver in Next are lint errors (`frontend-nextjs-lint`)
|
|
22
22
|
|
|
23
23
|
## TanStack Query Setup
|
|
24
24
|
|
|
@@ -134,7 +134,6 @@ export function UserListClient({ initialData }: { initialData: User[] }) {
|
|
|
134
134
|
|
|
135
135
|
| Wrong | Right | Why |
|
|
136
136
|
|-------|-------|-----|
|
|
137
|
-
| `useEffect(() => { fetch(...) }, [])` | `useQuery(...)` | Two renders, no cache, no SSR |
|
|
138
137
|
| No Zod on API response | `schema.parse(await res.json())` | Type safety at runtime |
|
|
139
138
|
| Hardcoded query keys: `['users']` | Key factory: `userKeys.lists()` | Consistent invalidation |
|
|
140
139
|
| `queryClient.invalidateQueries(['users'])` | `{ queryKey: userKeys.lists() }` | Object syntax required in v5 |
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# ESLint Next.js — o lint cobra, a prosa não repete
|
|
2
|
+
|
|
3
|
+
> **Scope:** nextjs
|
|
4
|
+
> **Layer:** 0 (resumo injetado pela rule `nextjs-standards` (`**/*.ts`, `**/*.tsx`); o `stack-scan` acusa o projeto sem este arquivo)
|
|
5
|
+
> **Keywords:** eslint, lint, eslint.config.mjs, no-restricted-imports, no-restricted-paths, no-restricted-syntax, no-default-export, no-explicit-any, framer-motion, zustand
|
|
6
|
+
> **Load When:** projeto Next.js sem `eslint.config.mjs` com `no-restricted-imports`, ou o lint acusou uma destas regras
|
|
7
|
+
|
|
8
|
+
**Verified against:** eslint 9.39.5, eslint-config-next 16.4.2 (typescript-eslint 8.71.2, eslint-plugin-import 2.32.0), next 16.4.2, typescript 6.0.3 (sonda: um arquivo ruim por regra → erro da regra esperada, `no-console` → aviso, arquivos conformes e o scaffold `morph-spec scaffold nextjs` → 0 problemas). Last-verified: 2026-10-18.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Regra de código que tem motivo e que uma máquina enxerga vira **erro de lint**, não frase que o
|
|
13
|
+
agente relê a cada sessão. Um arquivo na raiz do app Next basta, e só usa o que o
|
|
14
|
+
`eslint-config-next` já instala (typescript-eslint e eslint-plugin-import): nenhum plugin novo.
|
|
15
|
+
|
|
16
|
+
## Arquivos
|
|
17
|
+
|
|
18
|
+
Dependências (`npm i -D eslint@9 eslint-config-next typescript`). ESLint 9 é a faixa que
|
|
19
|
+
`eslint-plugin-import` e `eslint-plugin-react` declaram como peer; o typescript-eslint do
|
|
20
|
+
`eslint-config-next` pede TypeScript `<6.1`.
|
|
21
|
+
|
|
22
|
+
No Next 16 o `next build` não roda mais o lint. O nó de build do `morph-spec verify` roda o
|
|
23
|
+
eslint (`--quiet`: só erro reprova) **só nos arquivos que a feature mudou**, na raiz do app, quando
|
|
24
|
+
acha `eslint.config.*` e o pacote `eslint` instalado: dívida de lint antiga nunca deixa a feature
|
|
25
|
+
vermelha. As zonas são relativas a essa raiz.
|
|
26
|
+
|
|
27
|
+
`eslint.config.mjs`:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
31
|
+
import nextVitals from 'eslint-config-next/core-web-vitals'
|
|
32
|
+
import nextTs from 'eslint-config-next/typescript'
|
|
33
|
+
|
|
34
|
+
const BANNED = [
|
|
35
|
+
[['framer-motion'], 'Motion entra por motion/react: um único caminho de import.'],
|
|
36
|
+
[['zustand', 'redux', '@reduxjs/toolkit'], 'Estado de servidor é do TanStack Query; página e filtro vão na URL.'],
|
|
37
|
+
[['pg', '@neondatabase/serverless', '@prisma/client', 'drizzle-orm', 'postgres'], 'O Next nunca fala com o banco: o dado vem da API .NET.'],
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const DEEP_FEATURE = {
|
|
41
|
+
group: ['@/features/*/*'],
|
|
42
|
+
message: 'Fora da feature, importe a API pública (@/features/x), nunca um caminho interno.',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const restrictedImports = (...extraPatterns) => ['error', {
|
|
46
|
+
paths: [
|
|
47
|
+
...BANNED.flatMap(([names, message]) => names.map((name) => ({ name, message }))),
|
|
48
|
+
{ name: '@/components/ui/form', message: 'Form legado do shadcn: Controller + Field (frontend-nextjs-forms).' },
|
|
49
|
+
],
|
|
50
|
+
patterns: [
|
|
51
|
+
...BANNED.map(([names, message]) => ({ group: names.map((n) => `${n}/*`), message })),
|
|
52
|
+
...extraPatterns,
|
|
53
|
+
],
|
|
54
|
+
}]
|
|
55
|
+
|
|
56
|
+
const FETCH_IN_EFFECT = {
|
|
57
|
+
selector:
|
|
58
|
+
"CallExpression[callee.name='useEffect'] CallExpression:matches([callee.name='fetch'], [callee.name='apiClient'], [callee.object.name='apiClient'])",
|
|
59
|
+
message: 'Dado do servidor vem de Server Component ou TanStack Query, nunca de fetch em useEffect.',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default defineConfig([
|
|
63
|
+
...nextVitals,
|
|
64
|
+
...nextTs,
|
|
65
|
+
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
|
|
66
|
+
{
|
|
67
|
+
rules: {
|
|
68
|
+
'no-restricted-imports': restrictedImports(DEEP_FEATURE),
|
|
69
|
+
'import/no-restricted-paths': ['error', {
|
|
70
|
+
zones: [
|
|
71
|
+
{ target: './src/components', from: './src/features', message: 'components/ é apresentação: não conhece feature.' },
|
|
72
|
+
{ target: './src/features', from: './src/app', message: 'app/ importa de features/, nunca o contrário.' },
|
|
73
|
+
{ target: './src/hooks', from: ['./src/lib/api-client.ts', './src/lib/api'], message: 'Hook compartilhado não chama API: isso é hook de feature.' },
|
|
74
|
+
],
|
|
75
|
+
}],
|
|
76
|
+
'no-restricted-syntax': ['error', FETCH_IN_EFFECT],
|
|
77
|
+
'import/no-default-export': 'error',
|
|
78
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
79
|
+
'no-console': 'warn',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
files: ['src/features/**'],
|
|
84
|
+
rules: { 'no-restricted-imports': restrictedImports() },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
files: ['app/layout.tsx', 'src/app/layout.tsx'],
|
|
88
|
+
rules: {
|
|
89
|
+
'no-restricted-syntax': ['error', FETCH_IN_EFFECT, {
|
|
90
|
+
selector: "Program > ExpressionStatement[directive='use client']",
|
|
91
|
+
message: "O layout raiz é Server Component (metadata): o 'use client' vai no provider que ele renderiza.",
|
|
92
|
+
}],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
files: [
|
|
97
|
+
'**/app/**/{page,layout,loading,error,global-error,not-found,forbidden,unauthorized,template,default,route}.{ts,tsx}',
|
|
98
|
+
'**/app/**/{sitemap,robots,manifest,icon,apple-icon,opengraph-image,twitter-image}.{ts,tsx}',
|
|
99
|
+
'**/*.config.{js,mjs,cjs,ts}',
|
|
100
|
+
],
|
|
101
|
+
rules: { 'import/no-default-export': 'off' },
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Cada override de `no-restricted-imports` e `no-restricted-syntax` substitui a lista inteira da
|
|
107
|
+
regra naquele arquivo, por isso o bloco do layout repete `FETCH_IN_EFFECT` e o de `features/`
|
|
108
|
+
repete os pacotes banidos.
|
|
109
|
+
|
|
110
|
+
## O que cada regra compra
|
|
111
|
+
|
|
112
|
+
| Regra | Bloqueia | Por quê |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `no-restricted-imports` `@/features/*/*` (fora de `src/features/`) | `app/`, `components/` e `lib/` lendo arquivo interno de uma feature | o `index.ts` é a API pública: refatorar dentro da feature não quebra quem está fora |
|
|
115
|
+
| `no-restricted-imports` `framer-motion` | o segundo caminho de import do Motion | `motion/react` e `framer-motion` juntos duplicam o runtime no bundle e dividem o código em duas APIs |
|
|
116
|
+
| `no-restricted-imports` `zustand`, `redux`, `@reduxjs/toolkit` | store global | cópia do estado do servidor fica velha sem o cache do TanStack Query; página e filtro na URL sobrevivem a refresh e link |
|
|
117
|
+
| `no-restricted-imports` `pg`, `@neondatabase/serverless`, `@prisma/client`, `drizzle-orm`, `postgres` | driver de banco no Next | regra de negócio e acesso a dados moram no .NET (EF Core); um segundo caminho ao banco pula a autorização dele |
|
|
118
|
+
| `no-restricted-imports` `@/components/ui/form` | o `Form` legado do shadcn | a documentação do shadcn é escrita em `Controller` + `Field`; a tela legada migra quando é tocada |
|
|
119
|
+
| `import/no-restricted-paths` `src/components` ↛ `src/features` | componente compartilhado que conhece domínio | ele deixa de ser compartilhável e cria ciclo feature → components → feature |
|
|
120
|
+
| `import/no-restricted-paths` `src/features` ↛ `src/app` | feature importando rota | a dependência é de mão única: a rota compõe a feature |
|
|
121
|
+
| `import/no-restricted-paths` `src/hooks` ↛ cliente da API | hook compartilhado chamando o backend | chamada à API é de feature, com a key factory dela; hook compartilhado é só de UI |
|
|
122
|
+
| `no-restricted-syntax` fetch/`apiClient` dentro de `useEffect` | busca de dado no efeito | sem cache, sem dedup, corrida entre requisições e cascata de render; Server Component ou TanStack Query resolvem |
|
|
123
|
+
| `no-restricted-syntax` `'use client'` em `app/layout.tsx` | layout raiz no cliente | perde `metadata` e manda a árvore inteira para o bundle do cliente |
|
|
124
|
+
| `import/no-default-export` (exceto arquivos de rota e `*.config.*`) | `export default` em componente e hook | nome único em todo import: busca e rename acham todos os usos; o Next exige `default` só nos arquivos de convenção |
|
|
125
|
+
| `@typescript-eslint/no-explicit-any` | `any` | desliga o tipo no ponto onde o dado da API entra; o certo é schema Zod + `z.infer` |
|
|
126
|
+
| `no-console` (aviso) | `console.log` esquecido | ruído no console do usuário e dado vazando; aviso, porque o diagnóstico local é legítimo |
|
|
127
|
+
|
|
128
|
+
Restrição nova entra como linha neste arquivo, com o porquê na `message`. Nunca uma regra nova
|
|
129
|
+
em prosa. Exceção pontual é `// eslint-disable-next-line <regra> -- <motivo>` na linha, nunca o
|
|
130
|
+
desligamento da regra no projeto.
|
|
131
|
+
|
|
132
|
+
## O que não é regra
|
|
133
|
+
|
|
134
|
+
Nome de classe CSS (BEM, prefixo de página, namespace `--app-`, sufixo de estado), catálogo de
|
|
135
|
+
classes `.animate-*`/`.stagger-*`, `className` opcional em toda prop, Zod em props, quantos
|
|
136
|
+
momentos de motion uma página tem, o deslocamento de uma transição, o skeleton com
|
|
137
|
+
`animate-pulse`, os defaults de `staleTime`/`retry` do `QueryClient` e quantos níveis de prop
|
|
138
|
+
drilling são aceitáveis são gosto: nem prosa nem lint. Siga o arquivo vizinho, e o formatter do
|
|
139
|
+
projeto decide a forma. Também saíram da prosa o `'use client'` no `error.tsx` (o `next build`
|
|
140
|
+
recusa sem ele) e o prefixo `use` do hook (o `react-hooks/rules-of-hooks` que vem no
|
|
141
|
+
`eslint-config-next` acusa hook chamado fora de função `useX`).
|
|
@@ -221,14 +221,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
221
221
|
|
|
222
222
|
- `mode="wait"` ensures the exit animation completes before the entering page renders.
|
|
223
223
|
- The `key` prop must change on navigation — use `usePathname()` from `next/navigation`.
|
|
224
|
-
- Keep page transitions subtle (`y: 8`, fast duration) — aggressive transitions hurt perceived performance.
|
|
225
224
|
|
|
226
225
|
---
|
|
227
226
|
|
|
228
227
|
## Section 6: Loading Patterns
|
|
229
228
|
|
|
230
|
-
Prefer CSS `animate-pulse` for skeleton loaders over Motion — CSS animations run off the main thread and have lower performance overhead.
|
|
231
|
-
|
|
232
229
|
```tsx
|
|
233
230
|
// components/skeleton.tsx
|
|
234
231
|
import { cn } from '@/lib/utils';
|
|
@@ -315,11 +312,8 @@ This CSS fallback catches animations outside Motion (Tailwind `animate-*`, third
|
|
|
315
312
|
| `duration: 0.8` for UI interactions | `duration: durations.fast` (0.15) for micro-interactions |
|
|
316
313
|
| Hardcoded easing `[0.4, 0, 0.2, 1]` | `ease: easings.standard` (from design-tokens) |
|
|
317
314
|
| No `useReducedMotion()` check | Always check or use CSS media query |
|
|
318
|
-
| Animating everything on the page | Reserve motion for 3-5 meaningful moments per page |
|
|
319
315
|
| Forgetting `animation-fill-mode: backwards` in stagger | Will cause a flash before the animation starts |
|
|
320
316
|
| `AnimatePresence` without `key` changes | Exit animations will never fire |
|
|
321
|
-
| Using `motion` for skeleton loaders | Use CSS `animate-pulse` — lower performance cost |
|
|
322
|
-
| `npm install framer-motion` / `import ... from 'framer-motion'` | `npm install motion` / `import ... from 'motion/react'` — same code underneath, but one import path per codebase |
|
|
323
317
|
| Need ScrollTrigger, SVG morph, or complex timelines | Use GSAP — see `frontend/gsap/gsap-core.md` |
|
|
324
318
|
|
|
325
319
|
---
|
|
@@ -333,7 +327,8 @@ npm install motion
|
|
|
333
327
|
> The package is `motion` and components import from `motion/react`. `framer-motion`
|
|
334
328
|
> is not deprecated: it is published with the same version and `motion` depends on
|
|
335
329
|
> it. New code still depends on `motion` and imports from `motion/react`, so the
|
|
336
|
-
> codebase has a single import path;
|
|
330
|
+
> codebase has a single import path; a `framer-motion` import is a lint error
|
|
331
|
+
> (`frontend-nextjs-lint`).
|
|
337
332
|
|
|
338
333
|
Verify the installed version supports the features used here (requires motion >= 11; current major on npm is 14):
|
|
339
334
|
|
|
@@ -15,9 +15,9 @@ Consistent naming prevents filesystem bugs (Linux case-sensitivity) and keeps th
|
|
|
15
15
|
|
|
16
16
|
- ALWAYS use kebab-case for file names — never PascalCase or camelCase
|
|
17
17
|
- ALWAYS use PascalCase for React component exports
|
|
18
|
-
- ALWAYS use camelCase starting with `use` for hook exports
|
|
19
18
|
- ALWAYS suffix schema files with `.schemas.ts` and type files with `.types.ts`
|
|
20
19
|
- NEVER mix cases in the same category — no `UserCard.tsx` alongside `user-profile.tsx`
|
|
20
|
+
- Named exports, the hook `use` prefix and `default` only in route files are lint errors (`frontend-nextjs-lint`), not rules to reread
|
|
21
21
|
|
|
22
22
|
## Complete Reference Table
|
|
23
23
|
|
|
@@ -63,7 +63,6 @@ export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
|
|
63
63
|
|-------|-------|-----|
|
|
64
64
|
| `UserCard.tsx` | `user-card.tsx` | Linux servers are case-sensitive |
|
|
65
65
|
| `useUsers.ts` | `use-users.ts` | Inconsistent with Next.js file conventions |
|
|
66
|
-
| `export default function UserCard` | `export function UserCard` | Named exports are more refactor-safe |
|
|
67
66
|
| `type User = { id: string }` | `type User = z.infer<typeof userSchema>` | Single source of truth |
|
|
68
67
|
|
|
69
68
|
---
|
|
@@ -16,7 +16,8 @@ Stack: Next.js 15 + Neon PostgreSQL + .NET Backend
|
|
|
16
16
|
- ALWAYS use App Router (not Pages Router)
|
|
17
17
|
- Default to Server Components -- add `'use client'` only when needed
|
|
18
18
|
- ALWAYS colocate loading.tsx and error.tsx with page.tsx
|
|
19
|
-
-
|
|
19
|
+
- Route Handlers are a BFF that delegates to the .NET API
|
|
20
|
+
- A DB driver, `any`, fetch inside `useEffect` and `'use client'` in the root layout are lint errors (`frontend-nextjs-lint`)
|
|
20
21
|
- ALWAYS validate inputs with Zod on both client and server
|
|
21
22
|
- Use TypeScript strict mode (`"strict": true`)
|
|
22
23
|
|
|
@@ -58,7 +59,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
58
59
|
// loading.tsx
|
|
59
60
|
export default function Loading() { return <div className="animate-pulse">Loading...</div>; }
|
|
60
61
|
|
|
61
|
-
// error.tsx
|
|
62
|
+
// error.tsx
|
|
62
63
|
'use client';
|
|
63
64
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
64
65
|
return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
|
|
@@ -187,7 +188,7 @@ export function DocumentForm() {
|
|
|
187
188
|
|
|
188
189
|
## shadcn/ui
|
|
189
190
|
|
|
190
|
-
Install: `npx shadcn@latest init` then `npx shadcn@latest add button input card dialog
|
|
191
|
+
Install: `npx shadcn@latest init` then `npx shadcn@latest add button input card dialog field`.
|
|
191
192
|
Components are copied to `components/ui/` -- NOT an npm dependency, your code to customize.
|
|
192
193
|
|
|
193
194
|
## TypeScript Strict Patterns
|
|
@@ -195,7 +196,7 @@ Components are copied to `components/ui/` -- NOT an npm dependency, your code to
|
|
|
195
196
|
| Pattern | Approach |
|
|
196
197
|
|---------|----------|
|
|
197
198
|
| API responses | Zod schema + `z.infer<typeof schema>` |
|
|
198
|
-
| Props | Explicit interface
|
|
199
|
+
| Props | Explicit interface |
|
|
199
200
|
| Event handlers | `React.ChangeEvent<HTMLInputElement>` |
|
|
200
201
|
| Null safety | `?.` over type assertions, `if (!data) return null` |
|
|
201
202
|
|
|
@@ -204,10 +205,7 @@ Components are copied to `components/ui/` -- NOT an npm dependency, your code to
|
|
|
204
205
|
| Wrong | Right | Why |
|
|
205
206
|
|-------|-------|-----|
|
|
206
207
|
| `'use client'` on every component | Default to Server Components | Unnecessary JS bundle size |
|
|
207
|
-
| Direct DB from client | Route Handler `/api/*` as BFF | Exposes queries, harder to secure |
|
|
208
|
-
| `any` for API responses | Zod schema + infer | No runtime safety |
|
|
209
208
|
| Missing loading.tsx | Colocate with page.tsx | Blank page during load |
|
|
210
|
-
| `useEffect` for data fetching | React Query `useQuery` | No caching, race conditions |
|
|
211
209
|
| Form validation on submit only | Zod resolver + react-hook-form | Delayed error feedback |
|
|
212
210
|
| shadcn as npm package | `npx shadcn@latest add` | Copy-paste system, not a dependency |
|
|
213
211
|
|