From 8ed4593f730304ea041c06c221d88940173c99b7 Mon Sep 17 00:00:00 2001 From: Angelooooo <91385982+lalds@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:37:30 +0300 Subject: [PATCH] =?UTF-8?q?Refactor=20Gemini=20model=20display:=20filter?= =?UTF-8?q?=20old=20versions,=20group=20low/high=20p=D0=BAo,=20and=20impro?= =?UTF-8?q?ve=20naming=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor Gemini model display: filter old versions, group low/high pro, and improve naming * Fix potential negative minutes and refine Gemini 3 Pro grouping logic based on PR feedback * The account change process has been sped up * Address PR review: enhance error message and parameterize polling interval in process handler * Address PR review: fix potential race condition in database backup logic * fix: prevent renderer white screen and harden sentry gating * feat: localize model labels * chore: enable hoisted node-linker * chore: ignore .agent/.claude/.codex in prettier --------- Co-authored-by: Draculabo --- .agent/skills/openspec-apply-change/SKILL.md | 7 +- .../skills/openspec-archive-change/SKILL.md | 6 +- .../openspec-bulk-archive-change/SKILL.md | 60 +- .../skills/openspec-continue-change/SKILL.md | 15 +- .agent/skills/openspec-explore/SKILL.md | 29 +- .agent/skills/openspec-ff-change/SKILL.md | 51 +- .agent/skills/openspec-new-change/SKILL.md | 13 +- .agent/skills/openspec-onboard/SKILL.md | 32 +- .agent/skills/openspec-sync-specs/SKILL.md | 46 +- .agent/skills/openspec-verify-change/SKILL.md | 9 +- .agent/skills/ui-ux-pro-max/SKILL.md | 121 +- .../vercel-composition-patterns/AGENTS.md | 217 ++- .../vercel-composition-patterns/SKILL.md | 3 +- .../rules/architecture-avoid-boolean-props.md | 16 +- .../rules/architecture-compound-components.md | 22 +- .../patterns-children-over-render-props.md | 21 +- .../rules/patterns-explicit-variants.md | 14 +- .../rules/react19-no-forwardref.md | 10 +- .../rules/state-context-interface.md | 52 +- .../rules/state-decouple-implementation.md | 46 +- .../rules/state-lift-state.md | 36 +- .../vercel-react-best-practices/AGENTS.md | 1255 ++++++++--------- .../vercel-react-best-practices/SKILL.md | 24 +- .../rules/advanced-event-handler-refs.md | 30 +- .../rules/advanced-init-once.md | 18 +- .../rules/advanced-use-latest.md | 18 +- .../rules/async-api-routes.md | 21 +- .../rules/async-defer-await.md | 52 +- .../rules/async-dependencies.md | 33 +- .../rules/async-parallel.md | 12 +- .../rules/async-suspense-boundaries.md | 26 +- .../rules/bundle-barrel-imports.md | 22 +- .../rules/bundle-conditional.md | 20 +- .../rules/bundle-defer-third-party.md | 15 +- .../rules/bundle-dynamic-imports.md | 15 +- .../rules/bundle-preload.md | 20 +- .../rules/client-event-listeners.md | 46 +- .../rules/client-localstorage-schema.md | 33 +- .../rules/client-passive-event-listeners.md | 40 +- .../rules/client-swr-dedup.md | 22 +- .../rules/js-batch-dom-css.md | 75 +- .../rules/js-cache-function-results.md | 16 +- .../rules/js-cache-property-access.md | 8 +- .../rules/js-cache-storage.md | 28 +- .../rules/js-combine-iterations.md | 18 +- .../rules/js-early-exit.md | 24 +- .../rules/js-hoist-regexp.md | 6 +- .../rules/js-index-maps.md | 14 +- .../rules/js-length-check-first.md | 13 +- .../rules/js-min-max-loop.md | 52 +- .../rules/js-tosorted-immutable.md | 2 +- .../rules/rendering-activity.md | 4 +- .../rules/rendering-animate-svg-wrapper.md | 17 +- .../rules/rendering-conditional-render.md | 12 +- .../rules/rendering-content-visibility.md | 6 +- .../rules/rendering-hoist-jsx.md | 18 +- .../rules/rendering-hydration-no-flicker.md | 34 +- .../rendering-hydration-suppress-warning.md | 10 +- .../rules/rendering-usetransition-loading.md | 42 +- .../rules/rerender-defer-reads.md | 20 +- .../rules/rerender-dependencies.md | 18 +- .../rules/rerender-derived-state-no-effect.md | 20 +- .../rules/rerender-derived-state.md | 10 +- .../rules/rerender-functional-setstate.md | 41 +- .../rules/rerender-lazy-state-init.md | 34 +- .../rules/rerender-memo-with-default-value.md | 2 - .../rules/rerender-memo.md | 20 +- .../rules/rerender-move-effect-to-event.md | 20 +- .../rerender-simple-expression-in-memo.md | 10 +- .../rules/rerender-transitions.md | 24 +- .../rerender-use-ref-transient-values.md | 32 +- .../rules/server-after-nonblocking.md | 42 +- .../rules/server-auth-actions.md | 62 +- .../rules/server-cache-lru.md | 16 +- .../rules/server-cache-react.md | 34 +- .../rules/server-dedup-props.md | 6 +- .../rules/server-parallel-fetching.md | 30 +- .../rules/server-serialization.md | 16 +- .agent/skills/web-design-guidelines/SKILL.md | 3 +- .agent/workflows/opsx-apply.md | 3 + .agent/workflows/opsx-archive.md | 2 + .agent/workflows/opsx-bulk-archive.md | 56 +- .agent/workflows/opsx-continue.md | 11 +- .agent/workflows/opsx-explore.md | 22 +- .agent/workflows/opsx-ff.md | 47 +- .agent/workflows/opsx-new.md | 9 + .agent/workflows/opsx-onboard.md | 28 +- .agent/workflows/opsx-sync.md | 42 +- .agent/workflows/opsx-verify.md | 5 +- .claude/commands/opsx/apply.md | 5 +- .claude/commands/opsx/archive.md | 4 +- .claude/commands/opsx/bulk-archive.md | 58 +- .claude/commands/opsx/continue.md | 13 +- .claude/commands/opsx/explore.md | 26 +- .claude/commands/opsx/ff.md | 49 +- .claude/commands/opsx/new.md | 11 +- .claude/commands/opsx/onboard.md | 30 +- .claude/commands/opsx/sync.md | 44 +- .claude/commands/opsx/verify.md | 7 +- .claude/skills/openspec-apply-change/SKILL.md | 7 +- .../skills/openspec-archive-change/SKILL.md | 6 +- .../openspec-bulk-archive-change/SKILL.md | 60 +- .../skills/openspec-continue-change/SKILL.md | 15 +- .claude/skills/openspec-explore/SKILL.md | 29 +- .claude/skills/openspec-ff-change/SKILL.md | 51 +- .claude/skills/openspec-new-change/SKILL.md | 13 +- .claude/skills/openspec-onboard/SKILL.md | 32 +- .claude/skills/openspec-sync-specs/SKILL.md | 46 +- .../skills/openspec-verify-change/SKILL.md | 9 +- .codex/prompts/opsx-apply.md | 3 + .codex/prompts/opsx-archive.md | 2 + .codex/prompts/opsx-bulk-archive.md | 56 +- .codex/prompts/opsx-continue.md | 11 +- .codex/prompts/opsx-explore.md | 22 +- .codex/prompts/opsx-ff.md | 47 +- .codex/prompts/opsx-new.md | 9 + .codex/prompts/opsx-onboard.md | 28 +- .codex/prompts/opsx-sync.md | 42 +- .codex/prompts/opsx-verify.md | 5 +- .codex/skills/openspec-apply-change/SKILL.md | 7 +- .../skills/openspec-archive-change/SKILL.md | 6 +- .../openspec-bulk-archive-change/SKILL.md | 60 +- .../skills/openspec-continue-change/SKILL.md | 15 +- .codex/skills/openspec-explore/SKILL.md | 29 +- .codex/skills/openspec-ff-change/SKILL.md | 51 +- .codex/skills/openspec-new-change/SKILL.md | 13 +- .codex/skills/openspec-onboard/SKILL.md | 32 +- .codex/skills/openspec-sync-specs/SKILL.md | 46 +- .codex/skills/openspec-verify-change/SKILL.md | 9 +- .codex/skills/ui-ux-pro-max/SKILL.md | 121 +- .npmrc | 4 +- .prettierignore | 3 + AGENTS.md | 1 - package-lock.json | 11 - src/actions/cloud.ts | 15 +- src/components/CloudAccountCard.tsx | 212 +-- src/ipc/cloud/handler.ts | 57 +- src/ipc/cloud/router.ts | 5 +- src/ipc/device/handler.ts | 45 +- src/ipc/process/handler.ts | 9 +- src/ipc/switchFlow.ts | 14 +- src/lib/antigravity/ModelMapping.ts | 49 +- src/lib/antigravity/QuotaService.ts | 8 +- src/localization/i18n.ts | 9 + src/main.ts | 37 +- src/preload.ts | 2 +- src/renderer.ts | 19 +- src/routes/index.tsx | 8 +- src/server/modules/proxy/gemini.controller.ts | 17 +- src/server/modules/proxy/proxy.controller.ts | 22 +- src/server/modules/proxy/proxy.guard.ts | 6 +- src/styles/global.css | 74 +- .../scripts/project-id-request-validation.ts | 12 +- src/tests/unit/account.test.ts | 5 +- src/tests/unit/cloudHandler-sync.test.ts | 1 - src/tests/unit/protobuf-unified.test.ts | 6 +- src/tests/unit/proxy-parity-fixtures.test.ts | 6 +- src/types/cloudAccount.ts | 7 +- src/utils/protobuf.ts | 4 +- vite.main.config.mts | 25 +- vite.preload.config.mts | 25 +- vite.renderer.config.mts | 43 +- 162 files changed, 2981 insertions(+), 2541 deletions(-) diff --git a/.agent/skills/openspec-apply-change/SKILL.md b/.agent/skills/openspec-apply-change/SKILL.md index 47d4bc2..147e629 100644 --- a/.agent/skills/openspec-apply-change/SKILL.md +++ b/.agent/skills/openspec-apply-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Implement tasks from an OpenSpec change. @@ -25,9 +25,11 @@ Implement tasks from an OpenSpec change. Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). 2. **Check status to understand the schema** + ```bash openspec status --change "" --json ``` + Parse the JSON to understand: - `schemaName`: The workflow being used (e.g., "spec-driven") - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) @@ -139,6 +141,7 @@ What would you like to do? ``` **Guardrails** + - Keep going through tasks until done or blocked - Always read context files before starting (from the apply instructions output) - If task is ambiguous, pause and ask before implementing diff --git a/.agent/skills/openspec-archive-change/SKILL.md b/.agent/skills/openspec-archive-change/SKILL.md index 8fa4b23..95a800a 100644 --- a/.agent/skills/openspec-archive-change/SKILL.md +++ b/.agent/skills/openspec-archive-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Archive a completed change in the experimental workflow. @@ -68,6 +68,7 @@ Archive a completed change in the experimental workflow. 5. **Perform the archive** Create the archive directory if it doesn't exist: + ```bash mkdir -p openspec/changes/archive ``` @@ -105,6 +106,7 @@ All artifacts complete. All tasks complete. ``` **Guardrails** + - Always prompt for change selection if not provided - Use artifact graph (openspec status --json) for completion checking - Don't block archive on warnings - just inform and confirm diff --git a/.agent/skills/openspec-bulk-archive-change/SKILL.md b/.agent/skills/openspec-bulk-archive-change/SKILL.md index 719b279..03e3237 100644 --- a/.agent/skills/openspec-bulk-archive-change/SKILL.md +++ b/.agent/skills/openspec-bulk-archive-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Archive multiple completed changes in a single operation. @@ -37,16 +37,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig For each selected change, collect: a. **Artifact status** - Run `openspec status --change "" --json` - - Parse `schemaName` and `artifacts` list - - Note which artifacts are `done` vs other states + - Parse `schemaName` and `artifacts` list + - Note which artifacts are `done` vs other states b. **Task completion** - Read `openspec/changes//tasks.md` - - Count `- [ ]` (incomplete) vs `- [x]` (complete) - - If no tasks file exists, note as "No tasks" + - Count `- [ ]` (incomplete) vs `- [x]` (complete) + - If no tasks file exists, note as "No tasks" c. **Delta specs** - Check `openspec/changes//specs/` directory - - List which capability specs exist - - For each, extract requirement names (lines matching `### Requirement: `) + - List which capability specs exist + - For each, extract requirement names (lines matching `### Requirement: `) 4. **Detect spec conflicts** @@ -66,18 +66,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify b. **Search the codebase** for implementation evidence: - - Look for code implementing requirements from each delta spec - - Check for related files, functions, or tests + - Look for code implementing requirements from each delta spec + - Check for related files, functions, or tests c. **Determine resolution**: - - If only one change is actually implemented -> sync that one's specs - - If both implemented -> apply in chronological order (older first, newer overwrites) - - If neither implemented -> skip spec sync, warn user + - If only one change is actually implemented -> sync that one's specs + - If both implemented -> apply in chronological order (older first, newer overwrites) + - If neither implemented -> skip spec sync, warn user d. **Record resolution** for each conflict: - - Which change's specs to apply - - In what order (if both) - - Rationale (what was found in codebase) + - Which change's specs to apply + - In what order (if both) + - Rationale (what was found in codebase) 6. **Show consolidated status table** @@ -93,12 +93,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig ``` For conflicts, show the resolution: + ``` * Conflict resolution: - auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order) ``` For incomplete changes, show warnings: + ``` Warnings: - add-verify-skill: 1 incomplete artifact, 3 incomplete tasks @@ -107,7 +109,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig 7. **Confirm batch operation** Use **AskUserQuestion tool** with a single confirmation: - - "Archive N changes?" with options based on status - Options might include: - "Archive all N changes" @@ -121,20 +122,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge) - - For conflicts, apply in resolved order - - Track if sync was done + - Use the openspec-sync-specs approach (agent-driven intelligent merge) + - For conflicts, apply in resolved order + - Track if sync was done b. **Perform the archive**: - ```bash - mkdir -p openspec/changes/archive - mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD- - ``` + + ```bash + mkdir -p openspec/changes/archive + mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD- + ``` c. **Track outcome** for each change: - - Success: archived successfully - - Failed: error during archive (record error) - - Skipped: user chose not to archive (if applicable) + - Success: archived successfully + - Failed: error during archive (record error) + - Skipped: user chose not to archive (if applicable) 9. **Display summary** @@ -157,6 +159,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig ``` If any failures: + ``` Failed 1 change: - some-change: Archive directory already exists @@ -165,6 +168,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig **Conflict Resolution Examples** Example 1: Only one implemented + ``` Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt] @@ -180,6 +184,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only. ``` Example 2: Both implemented + ``` Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql] @@ -233,6 +238,7 @@ No active changes found. Use `/opsx:new` to create a new change. ``` **Guardrails** + - Allow any number of changes (1+ is fine, 2+ is the typical use case) - Always prompt for selection, never auto-select - Detect spec conflicts early and resolve by checking codebase diff --git a/.agent/skills/openspec-continue-change/SKILL.md b/.agent/skills/openspec-continue-change/SKILL.md index 5060b2f..0825819 100644 --- a/.agent/skills/openspec-continue-change/SKILL.md +++ b/.agent/skills/openspec-continue-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Continue working on a change by creating the next artifact. @@ -30,9 +30,11 @@ Continue working on a change by creating the next artifact. **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. 2. **Check current status** + ```bash openspec status --change "" --json ``` + Parse the JSON to understand current state. The response includes: - `schemaName`: The workflow schema being used (e.g., "spec-driven") - `artifacts`: Array of artifacts with their status ("done", "ready", "blocked") @@ -40,7 +42,7 @@ Continue working on a change by creating the next artifact. 3. **Act based on status**: - --- + *** **If all artifacts are complete (`isComplete: true`)**: - Congratulate the user @@ -48,7 +50,7 @@ Continue working on a change by creating the next artifact. - Suggest: "All artifacts created! You can now implement this change or archive it." - STOP - --- + *** **If artifacts are ready to create** (status shows artifacts with `status: "ready"`): - Pick the FIRST artifact with `status: "ready"` from the status output @@ -71,7 +73,7 @@ Continue working on a change by creating the next artifact. - Show what was created and what's now unlocked - STOP after creating ONE artifact - --- + *** **If no artifacts are ready (all blocked)**: - This shouldn't happen with a valid schema @@ -85,6 +87,7 @@ Continue working on a change by creating the next artifact. **Output** After each invocation, show: + - Which artifact was created - Schema workflow being used - Current progress (N/M complete) @@ -98,6 +101,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction` Common artifact patterns: **spec-driven schema** (proposal → specs → design → tasks): + - **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact. - The Capabilities section is critical - each capability listed will need a spec file. - **specs//spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name). @@ -107,6 +111,7 @@ Common artifact patterns: For other schemas, follow the `instruction` field from the CLI output. **Guardrails** + - Create ONE artifact per invocation - Always read dependency artifacts before creating a new one - Never skip artifacts or create out of order diff --git a/.agent/skills/openspec-explore/SKILL.md b/.agent/skills/openspec-explore/SKILL.md index 8ed4a76..13dff42 100644 --- a/.agent/skills/openspec-explore/SKILL.md +++ b/.agent/skills/openspec-explore/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. @@ -33,24 +33,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher Depending on what the user brings, you might: **Explore the problem space** + - Ask clarifying questions that emerge from what they said - Challenge assumptions - Reframe the problem - Find analogies **Investigate the codebase** + - Map existing architecture relevant to the discussion - Find integration points - Identify patterns already in use - Surface hidden complexity **Compare options** + - Brainstorm multiple approaches - Build comparison tables - Sketch tradeoffs - Recommend a path (if asked) **Visualize** + ``` ┌─────────────────────────────────────────┐ │ Use ASCII diagrams liberally │ @@ -69,6 +73,7 @@ Depending on what the user brings, you might: ``` **Surface risks and unknowns** + - Identify what could go wrong - Find gaps in understanding - Suggest spikes or investigations @@ -82,11 +87,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it. ### Check for context At the start, quickly check what exists: + ```bash openspec list --json ``` This tells you: + - If there are active changes - Their names, schemas, and status - What the user might be working on @@ -115,14 +122,14 @@ If the user mentions a change or you detect one is relevant: 3. **Offer to capture when decisions are made** - | Insight Type | Where to Capture | - |--------------|------------------| + | Insight Type | Where to Capture | + | -------------------------- | ---------------------------- | | New requirement discovered | `specs//spec.md` | - | Requirement changed | `specs//spec.md` | - | Design decision made | `design.md` | - | Scope changed | `proposal.md` | - | New work identified | `tasks.md` | - | Assumption invalidated | Relevant artifact | + | Requirement changed | `specs//spec.md` | + | Design decision made | `design.md` | + | Scope changed | `proposal.md` | + | New work identified | `tasks.md` | + | Assumption invalidated | Relevant artifact | Example offers: - "That's a design decision. Capture it in design.md?" @@ -147,6 +154,7 @@ If the user mentions a change or you detect one is relevant: ## Handling Different Entry Points **User brings a vague idea:** + ``` User: I'm thinking about adding real-time collaboration @@ -170,6 +178,7 @@ You: Real-time collab is a big space. Let me think about this... ``` **User brings a specific problem:** + ``` User: The auth system is a mess @@ -201,6 +210,7 @@ You: [reads codebase] ``` **User is stuck mid-implementation:** + ``` User: /opsx:explore add-auth-system The OAuth integration is more complex than expected @@ -218,6 +228,7 @@ You: [reads change artifacts] ``` **User wants to compare options:** + ``` User: Should we use Postgres or SQLite? diff --git a/.agent/skills/openspec-ff-change/SKILL.md b/.agent/skills/openspec-ff-change/SKILL.md index d586012..5c9bfd0 100644 --- a/.agent/skills/openspec-ff-change/SKILL.md +++ b/.agent/skills/openspec-ff-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Fast-forward through artifact creation - generate everything needed to start implementation in one go. @@ -18,6 +18,7 @@ Fast-forward through artifact creation - generate everything needed to start imp 1. **If no clear input provided, ask what they want to build** Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: + > "What change do you want to work on? Describe what you want to build or fix." From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). @@ -25,15 +26,19 @@ Fast-forward through artifact creation - generate everything needed to start imp **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. 2. **Create the change directory** + ```bash openspec new change "" ``` + This creates a scaffolded change at `openspec/changes//`. 3. **Get the artifact build order** + ```bash openspec status --change "" --json ``` + Parse the JSON to get: - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) - `artifacts`: list of all artifacts with their status and dependencies @@ -45,30 +50,30 @@ Fast-forward through artifact creation - generate everything needed to start imp Loop through artifacts in dependency order (artifacts with no pending dependencies first): a. **For each artifact that is `ready` (dependencies satisfied)**: - - Get instructions: - ```bash - openspec instructions --change "" --json - ``` - - The instructions JSON includes: - - `context`: Project background (constraints for you - do NOT include in output) - - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) - - `template`: The structure to use for your output file - - `instruction`: Schema-specific guidance for this artifact type - - `outputPath`: Where to write the artifact - - `dependencies`: Completed artifacts to read for context - - Read any completed dependency files for context - - Create the artifact file using `template` as the structure - - Apply `context` and `rules` as constraints - but do NOT copy them into the file - - Show brief progress: "✓ Created " + - Get instructions: + ```bash + openspec instructions --change "" --json + ``` + - The instructions JSON includes: + - `context`: Project background (constraints for you - do NOT include in output) + - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) + - `template`: The structure to use for your output file + - `instruction`: Schema-specific guidance for this artifact type + - `outputPath`: Where to write the artifact + - `dependencies`: Completed artifacts to read for context + - Read any completed dependency files for context + - Create the artifact file using `template` as the structure + - Apply `context` and `rules` as constraints - but do NOT copy them into the file + - Show brief progress: "✓ Created " b. **Continue until all `applyRequires` artifacts are complete** - - After creating each artifact, re-run `openspec status --change "" --json` - - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array - - Stop when all `applyRequires` artifacts are done + - After creating each artifact, re-run `openspec status --change "" --json` + - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array + - Stop when all `applyRequires` artifacts are done c. **If an artifact requires user input** (unclear context): - - Use **AskUserQuestion tool** to clarify - - Then continue with creation + - Use **AskUserQuestion tool** to clarify + - Then continue with creation 5. **Show final status** ```bash @@ -78,6 +83,7 @@ Fast-forward through artifact creation - generate everything needed to start imp **Output** After completing all artifacts, summarize: + - Change name and location - List of artifacts created with brief descriptions - What's ready: "All artifacts created! Ready for implementation." @@ -94,6 +100,7 @@ After completing all artifacts, summarize: - These guide what you write, but should never appear in the output **Guardrails** + - Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) - Always read dependency artifacts before creating a new one - If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum diff --git a/.agent/skills/openspec-new-change/SKILL.md b/.agent/skills/openspec-new-change/SKILL.md index 37ac7ba..fd3ad14 100644 --- a/.agent/skills/openspec-new-change/SKILL.md +++ b/.agent/skills/openspec-new-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Start a new change using the experimental artifact-driven approach. @@ -18,6 +18,7 @@ Start a new change using the experimental artifact-driven approach. 1. **If no clear input provided, ask what they want to build** Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: + > "What change do you want to work on? Describe what you want to build or fix." From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). @@ -35,24 +36,30 @@ Start a new change using the experimental artifact-driven approach. **Otherwise**: Omit `--schema` to use the default. 3. **Create the change directory** + ```bash openspec new change "" ``` + Add `--schema ` only if the user requested a specific workflow. This creates a scaffolded change at `openspec/changes//` with the selected schema. 4. **Show the artifact status** + ```bash openspec status --change "" ``` + This shows which artifacts need to be created and which are ready (dependencies satisfied). 5. **Get instructions for the first artifact** The first artifact depends on the schema (e.g., `proposal` for spec-driven). Check the status output to find the first artifact with status "ready". + ```bash openspec instructions --change "" ``` + This outputs the template and context for creating the first artifact. 6. **STOP and wait for user direction** @@ -60,6 +67,7 @@ Start a new change using the experimental artifact-driven approach. **Output** After completing the steps, summarize: + - Change name and location - Schema/workflow being used and its artifact sequence - Current status (0/N artifacts complete) @@ -67,6 +75,7 @@ After completing the steps, summarize: - Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue." **Guardrails** + - Do NOT create any artifacts yet - just show the instructions - Do NOT advance beyond showing the first artifact template - If the name is invalid (not kebab-case), ask for a valid name diff --git a/.agent/skills/openspec-onboard/SKILL.md b/.agent/skills/openspec-onboard/SKILL.md index 4a03038..b830062 100644 --- a/.agent/skills/openspec-onboard/SKILL.md +++ b/.agent/skills/openspec-onboard/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. @@ -22,6 +22,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED" ``` **If not initialized:** + > OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. Stop here if not initialized. @@ -66,6 +67,7 @@ Scan the codebase for small improvement opportunities. Look for: 6. **Missing validation** - User input handlers without validation Also check recent git activity: + ```bash git log --oneline -10 2>/dev/null || echo "No git history" ``` @@ -101,6 +103,7 @@ Which task interests you? (Pick a number or describe your own) ``` **If nothing found:** Fall back to asking what the user wants to build: + > I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix? ### Scope Guardrail @@ -133,6 +136,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y ``` Spend 1-2 minutes investigating the relevant code: + - Read the file(s) involved - Draw a quick ASCII diagram if it helps - Note any considerations @@ -158,6 +162,7 @@ Now let's create a change to hold our work. ## Phase 4: Create the Change **EXPLAIN:** + ``` ## Creating a Change @@ -167,21 +172,25 @@ Let me create one for our task. ``` **DO:** Create the change with a derived kebab-case name: + ```bash openspec new change "" ``` **SHOW:** + ``` Created: `openspec/changes//` The folder structure: ``` + openspec/changes// -├── proposal.md ← Why we're doing this (empty, we'll fill it) -├── design.md ← How we'll build it (empty) -├── specs/ ← Detailed requirements (empty) -└── tasks.md ← Implementation checklist (empty) +├── proposal.md ← Why we're doing this (empty, we'll fill it) +├── design.md ← How we'll build it (empty) +├── specs/ ← Detailed requirements (empty) +└── tasks.md ← Implementation checklist (empty) + ``` Now let's fill in the first artifact—the proposal. @@ -192,6 +201,7 @@ Now let's fill in the first artifact—the proposal. ## Phase 5: Proposal **EXPLAIN:** + ``` ## The Proposal @@ -236,9 +246,11 @@ Does this capture the intent? I can adjust before we save it. **PAUSE** - Wait for user approval/feedback. After approval, save the proposal: + ```bash openspec instructions proposal --change "" --json ``` + Then write the content to `openspec/changes//proposal.md`. ``` @@ -252,6 +264,7 @@ Next up: specs. ## Phase 6: Specs **EXPLAIN:** + ``` ## Specs @@ -261,6 +274,7 @@ For a small task like this, we might only need one spec file. ``` **DO:** Create the spec file: + ```bash mkdir -p openspec/changes//specs/ ``` @@ -296,6 +310,7 @@ Save to `openspec/changes//specs//spec.md`. ## Phase 7: Design **EXPLAIN:** + ``` ## Design @@ -341,6 +356,7 @@ Save to `openspec/changes//design.md`. ## Phase 8: Tasks **EXPLAIN:** + ``` ## Tasks @@ -379,6 +395,7 @@ Save to `openspec/changes//tasks.md`. ## Phase 9: Apply (Implementation) **EXPLAIN:** + ``` ## Implementation @@ -413,6 +430,7 @@ The change is implemented! One more step—let's archive it. ## Phase 10: Archive **EXPLAIN:** + ``` ## Archiving @@ -422,11 +440,13 @@ Archived changes become your project's decision history—you can always find th ``` **DO:** + ```bash openspec archive "" ``` **SHOW:** + ``` Archived to: `openspec/changes/archive/YYYY-MM-DD-/` diff --git a/.agent/skills/openspec-sync-specs/SKILL.md b/.agent/skills/openspec-sync-specs/SKILL.md index 4c7e3aa..5154438 100644 --- a/.agent/skills/openspec-sync-specs/SKILL.md +++ b/.agent/skills/openspec-sync-specs/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Sync delta specs from a change to main specs. @@ -47,28 +47,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e c. **Apply changes intelligently**: - **ADDED Requirements:** - - If requirement doesn't exist in main spec → add it - - If requirement already exists → update it to match (treat as implicit MODIFIED) + **ADDED Requirements:** + - If requirement doesn't exist in main spec → add it + - If requirement already exists → update it to match (treat as implicit MODIFIED) - **MODIFIED Requirements:** - - Find the requirement in main spec - - Apply the changes - this can be: - - Adding new scenarios (don't need to copy existing ones) - - Modifying existing scenarios - - Changing the requirement description - - Preserve scenarios/content not mentioned in the delta + **MODIFIED Requirements:** + - Find the requirement in main spec + - Apply the changes - this can be: + - Adding new scenarios (don't need to copy existing ones) + - Modifying existing scenarios + - Changing the requirement description + - Preserve scenarios/content not mentioned in the delta - **REMOVED Requirements:** - - Remove the entire requirement block from main spec + **REMOVED Requirements:** + - Remove the entire requirement block from main spec - **RENAMED Requirements:** - - Find the FROM requirement, rename to TO + **RENAMED Requirements:** + - Find the FROM requirement, rename to TO d. **Create new main spec** if capability doesn't exist yet: - - Create `openspec/specs//spec.md` - - Add Purpose section (can be brief, mark as TBD) - - Add Requirements section with the ADDED requirements + - Create `openspec/specs//spec.md` + - Add Purpose section (can be brief, mark as TBD) + - Add Requirements section with the ADDED requirements 4. **Show summary** @@ -82,16 +82,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e ## ADDED Requirements ### Requirement: New Feature + The system SHALL do something new. #### Scenario: Basic case + - **WHEN** user does X - **THEN** system does Y ## MODIFIED Requirements ### Requirement: Existing Feature + #### Scenario: New scenario to add + - **WHEN** user does A - **THEN** system does B @@ -108,8 +112,9 @@ The system SHALL do something new. **Key Principle: Intelligent Merging** Unlike programmatic merging, you can apply **partial updates**: + - To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios -- The delta represents *intent*, not a wholesale replacement +- The delta represents _intent_, not a wholesale replacement - Use your judgment to merge changes sensibly **Output On Success** @@ -131,6 +136,7 @@ Main specs are now updated. The change remains active - archive when implementat ``` **Guardrails** + - Read both delta and main specs before making changes - Preserve existing content not mentioned in delta - If something is unclear, ask for clarification diff --git a/.agent/skills/openspec-verify-change/SKILL.md b/.agent/skills/openspec-verify-change/SKILL.md index 443ac5f..90fc30f 100644 --- a/.agent/skills/openspec-verify-change/SKILL.md +++ b/.agent/skills/openspec-verify-change/SKILL.md @@ -5,8 +5,8 @@ license: MIT compatibility: Requires openspec CLI. metadata: author: openspec - version: "1.0" - generatedBy: "1.1.1" + version: '1.0' + generatedBy: '1.1.1' --- Verify that an implementation matches the change artifacts (specs, tasks, design). @@ -26,9 +26,11 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. 2. **Check status to understand the schema** + ```bash openspec status --change "" --json ``` + Parse the JSON to understand: - `schemaName`: The workflow being used (e.g., "spec-driven") - Which artifacts exist for this change @@ -110,6 +112,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design 8. **Generate Verification Report** **Summary Scorecard**: + ``` ## Verification Report: @@ -122,7 +125,6 @@ Verify that an implementation matches the change artifacts (specs, tasks, design ``` **Issues by Priority**: - 1. **CRITICAL** (Must fix before archive): - Incomplete tasks - Missing requirement implementations @@ -161,6 +163,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design **Output Format** Use clear markdown with: + - Table for summary scorecard - Grouped lists for issues (CRITICAL/WARNING/SUGGESTION) - Code references in format: `file.ts:123` diff --git a/.agent/skills/ui-ux-pro-max/SKILL.md b/.agent/skills/ui-ux-pro-max/SKILL.md index 72ac2c3..22032ab 100644 --- a/.agent/skills/ui-ux-pro-max/SKILL.md +++ b/.agent/skills/ui-ux-pro-max/SKILL.md @@ -2,6 +2,7 @@ name: ui-ux-pro-max description: UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 9 stacks. --- + # ui-ux-pro-max Comprehensive design guide for web and mobile applications. Contains 67 styles, 96 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 13 technology stacks. Searchable database with priority-based recommendations. @@ -17,16 +18,19 @@ python3 --version || python --version If Python is not installed, install it based on user's OS: **macOS:** + ```bash brew install python3 ``` **Ubuntu/Debian:** + ```bash sudo apt update && sudo apt install python3 ``` **Windows:** + ```powershell winget install Python.Python.3.12 ``` @@ -40,6 +44,7 @@ When user requests UI/UX work (design, build, create, implement, review, fix, im ### Step 1: Analyze User Requirements Extract key information from user request: + - **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc. - **Style keywords**: minimal, playful, professional, elegant, dark mode, etc. - **Industry**: healthcare, fintech, gaming, education, etc. @@ -54,12 +59,14 @@ python3 skills/ui-ux-pro-max/scripts/search.py " " --design-system --persi ``` This creates: + - `design-system/MASTER.md` — Global Source of Truth with all design rules - `design-system/pages/` — Folder for page-specific overrides **With page-specific override:** + ```bash python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" --page "dashboard" ``` This also creates: + - `design-system/pages/dashboard.md` — Page-specific deviations from Master **How hierarchical retrieval works:** + 1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md` 2. If the page file exists, its rules **override** the Master file 3. If not, use `design-system/MASTER.md` exclusively @@ -99,13 +110,13 @@ python3 skills/ui-ux-pro-max/scripts/search.py "" --domain [-n **When to use detailed searches:** -| Need | Domain | Example | -|------|--------|---------| -| More style options | `style` | `--domain style "glassmorphism dark"` | -| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` | -| UX best practices | `ux` | `--domain ux "animation accessibility"` | -| Alternative fonts | `typography` | `--domain typography "elegant luxury"` | -| Landing structure | `landing` | `--domain landing "hero social-proof"` | +| Need | Domain | Example | +| --------------------- | ------------ | --------------------------------------- | +| More style options | `style` | `--domain style "glassmorphism dark"` | +| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` | +| UX best practices | `ux` | `--domain ux "animation accessibility"` | +| Alternative fonts | `typography` | `--domain typography "elegant luxury"` | +| Landing structure | `landing` | `--domain landing "hero social-proof"` | ### Step 4: Stack Guidelines (Default: html-tailwind) @@ -123,32 +134,32 @@ Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui` ### Available Domains -| Domain | Use For | Example Keywords | -|--------|---------|------------------| -| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | -| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | -| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | -| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | -| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | -| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | -| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | -| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | -| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize | -| `prompt` | AI prompts, CSS keywords | (style name) | +| Domain | Use For | Example Keywords | +| ------------ | ------------------------------------ | -------------------------------------------------------- | +| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | +| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | +| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | +| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | +| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | +| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | +| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | +| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize | +| `prompt` | AI prompts, CSS keywords | (style name) | ### Available Stacks -| Stack | Focus | -|-------|-------| -| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) | -| `react` | State, hooks, performance, patterns | -| `nextjs` | SSR, routing, images, API routes | -| `vue` | Composition API, Pinia, Vue Router | -| `svelte` | Runes, stores, SvelteKit | -| `swiftui` | Views, State, Navigation, Animation | -| `react-native` | Components, Navigation, Lists | -| `flutter` | Widgets, State, Layout, Theming | -| `shadcn` | shadcn/ui components, theming, forms, patterns | +| Stack | Focus | +| ----------------- | ----------------------------------------------------- | +| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) | +| `react` | State, hooks, performance, patterns | +| `nextjs` | SSR, routing, images, API routes | +| `vue` | Composition API, Pinia, Vue Router | +| `svelte` | Runes, stores, SvelteKit | +| `swiftui` | Views, State, Navigation, Animation | +| `react-native` | Components, Navigation, Lists | +| `flutter` | Widgets, State, Layout, Theming | +| `shadcn` | shadcn/ui components, theming, forms, patterns | | `jetpack-compose` | Composables, Modifiers, State Hoisting, Recomposition | --- @@ -158,6 +169,7 @@ Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui` **User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp" ### Step 1: Analyze Requirements + - Product type: Beauty/Spa service - Style keywords: elegant, professional, soft - Industry: Beauty/Wellness @@ -222,37 +234,37 @@ These are frequently overlooked issues that make UI look unprofessional: ### Icons & Visual Elements -| Rule | Do | Don't | -|------|----|----- | -| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons | -| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout | -| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths | -| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly | +| Rule | Do | Don't | +| -------------------------- | ----------------------------------------------- | -------------------------------------- | +| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons | +| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout | +| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths | +| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly | ### Interaction & Cursor -| Rule | Do | Don't | -|------|----|----- | -| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements | -| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive | -| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) | +| Rule | Do | Don't | +| ---------------------- | ----------------------------------------------------- | -------------------------------------------- | +| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements | +| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive | +| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) | ### Light/Dark Mode Contrast -| Rule | Do | Don't | -|------|----|----- | -| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) | -| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text | -| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter | -| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) | +| Rule | Do | Don't | +| ------------------------- | ----------------------------------- | --------------------------------------- | +| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) | +| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text | +| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter | +| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) | ### Layout & Spacing -| Rule | Do | Don't | -|------|----|----- | -| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` | -| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements | -| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths | +| Rule | Do | Don't | +| ------------------------ | ----------------------------------- | -------------------------------------- | +| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` | +| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements | +| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths | --- @@ -261,6 +273,7 @@ These are frequently overlooked issues that make UI look unprofessional: Before delivering UI code, verify these items: ### Visual Quality + - [ ] No emojis used as icons (use SVG instead) - [ ] All icons from consistent icon set (Heroicons/Lucide) - [ ] Brand logos are correct (verified from Simple Icons) @@ -268,24 +281,28 @@ Before delivering UI code, verify these items: - [ ] Use theme colors directly (bg-primary) not var() wrapper ### Interaction + - [ ] All clickable elements have `cursor-pointer` - [ ] Hover states provide clear visual feedback - [ ] Transitions are smooth (150-300ms) - [ ] Focus states visible for keyboard navigation ### Light/Dark Mode + - [ ] Light mode text has sufficient contrast (4.5:1 minimum) - [ ] Glass/transparent elements visible in light mode - [ ] Borders visible in both modes - [ ] Test both modes before delivery ### Layout + - [ ] Floating elements have proper spacing from edges - [ ] No content hidden behind fixed navbars - [ ] Responsive at 375px, 768px, 1024px, 1440px - [ ] No horizontal scroll on mobile ### Accessibility + - [ ] All images have alt text - [ ] Form inputs have labels - [ ] Color is not the only indicator diff --git a/.agent/skills/vercel-composition-patterns/AGENTS.md b/.agent/skills/vercel-composition-patterns/AGENTS.md index 558bf9a..595a734 100644 --- a/.agent/skills/vercel-composition-patterns/AGENTS.md +++ b/.agent/skills/vercel-composition-patterns/AGENTS.md @@ -73,16 +73,10 @@ function Composer({ ) : isThread ? ( ) : null} - {isEditing ? ( - - ) : isForwarding ? ( - - ) : ( - - )} + {isEditing ? : isForwarding ? : }
- ) + ); } ``` @@ -102,7 +96,7 @@ function ChannelComposer() { - ) + ); } // Thread composer - adds "also send to channel" field @@ -118,7 +112,7 @@ function ThreadComposer({ channelId }: { channelId: string }) { - ) + ); } // Edit composer - different footer actions @@ -133,7 +127,7 @@ function EditComposer() { - ) + ); } ``` @@ -177,25 +171,21 @@ function Composer({
)} - ) + ); } ``` **Correct: compound components with shared context** ```tsx -const ComposerContext = createContext(null) +const ComposerContext = createContext(null); function ComposerProvider({ children, state, actions, meta }: ProviderProps) { - return ( - - {children} - - ) + return {children}; } function ComposerFrame({ children }: { children: React.ReactNode }) { - return
{children}
+ return
{children}
; } function ComposerInput() { @@ -203,21 +193,21 @@ function ComposerInput() { state, actions: { update }, meta: { inputRef }, - } = use(ComposerContext) + } = use(ComposerContext); return ( update((s) => ({ ...s, input: text }))} /> - ) + ); } function ComposerSubmit() { const { actions: { submit }, - } = use(ComposerContext) - return + } = use(ComposerContext); + return ; } // Export as compound component @@ -231,7 +221,7 @@ const Composer = { Attachments: ComposerAttachments, Formatting: ComposerFormatting, Emojis: ComposerEmojis, -} +}; ``` **Usage:** @@ -275,18 +265,15 @@ useState, Zustand, or a server sync. ```tsx function ChannelComposer({ channelId }: { channelId: string }) { // UI component knows about global state implementation - const state = useGlobalChannelState(channelId) - const { submit, updateInput } = useChannelSync(channelId) + const state = useGlobalChannelState(channelId); + const { submit, updateInput } = useChannelSync(channelId); return ( - sync.updateInput(text)} - /> + sync.updateInput(text)} /> sync.submit()} /> - ) + ); } ``` @@ -298,21 +285,17 @@ function ChannelProvider({ channelId, children, }: { - channelId: string - children: React.ReactNode + channelId: string; + children: React.ReactNode; }) { - const { state, update, submit } = useGlobalChannel(channelId) - const inputRef = useRef(null) + const { state, update, submit } = useGlobalChannel(channelId); + const inputRef = useRef(null); return ( - + {children} - ) + ); } // UI component only knows about the context interface @@ -325,7 +308,7 @@ function ChannelComposer() { - ) + ); } // Usage @@ -334,7 +317,7 @@ function Channel({ channelId }: { channelId: string }) { - ) + ); } ``` @@ -343,28 +326,25 @@ function Channel({ channelId }: { channelId: string }) { ```tsx // Local state for ephemeral forms function ForwardMessageProvider({ children }) { - const [state, setState] = useState(initialState) - const forwardMessage = useForwardMessage() + const [state, setState] = useState(initialState); + const forwardMessage = useForwardMessage(); return ( - + {children} - ) + ); } // Global synced state for channels function ChannelProvider({ channelId, children }) { - const { state, update, submit } = useGlobalChannel(channelId) + const { state, update, submit } = useGlobalChannel(channelId); return ( {children} - ) + ); } ``` @@ -393,8 +373,8 @@ dependency-injectable. ```tsx function ComposerInput() { // Tightly coupled to a specific hook - const { input, setInput } = useChannelComposerState() - return + const { input, setInput } = useChannelComposerState(); + return ; } ``` @@ -403,27 +383,27 @@ function ComposerInput() { ```tsx // Define a GENERIC interface that any provider can implement interface ComposerState { - input: string - attachments: Attachment[] - isSubmitting: boolean + input: string; + attachments: Attachment[]; + isSubmitting: boolean; } interface ComposerActions { - update: (updater: (state: ComposerState) => ComposerState) => void - submit: () => void + update: (updater: (state: ComposerState) => ComposerState) => void; + submit: () => void; } interface ComposerMeta { - inputRef: React.RefObject + inputRef: React.RefObject; } interface ComposerContextValue { - state: ComposerState - actions: ComposerActions - meta: ComposerMeta + state: ComposerState; + actions: ComposerActions; + meta: ComposerMeta; } -const ComposerContext = createContext(null) +const ComposerContext = createContext(null); ``` **UI components consume the interface, not the implementation:** @@ -434,7 +414,7 @@ function ComposerInput() { state, actions: { update }, meta, - } = use(ComposerContext) + } = use(ComposerContext); // This component works with ANY provider that implements the interface return ( @@ -443,7 +423,7 @@ function ComposerInput() { value={state.input} onChangeText={(text) => update((s) => ({ ...s, input: text }))} /> - ) + ); } ``` @@ -452,9 +432,9 @@ function ComposerInput() { ```tsx // Provider A: Local state for ephemeral forms function ForwardMessageProvider({ children }: { children: React.ReactNode }) { - const [state, setState] = useState(initialState) - const inputRef = useRef(null) - const submit = useForwardMessage() + const [state, setState] = useState(initialState); + const inputRef = useRef(null); + const submit = useForwardMessage(); return ( {children} - ) + ); } // Provider B: Global synced state for channels function ChannelProvider({ channelId, children }: Props) { - const { state, update, submit } = useGlobalChannel(channelId) - const inputRef = useRef(null) + const { state, update, submit } = useGlobalChannel(channelId); + const inputRef = useRef(null); return ( {children} - ) + ); } ``` @@ -534,21 +514,21 @@ function ForwardMessageDialog() { - ) + ); } // This button lives OUTSIDE Composer.Frame but can still submit based on its context! function ForwardButton() { const { actions: { submit }, - } = use(ComposerContext) - return + } = use(ComposerContext); + return ; } // This preview lives OUTSIDE Composer.Frame but can read composer's state! function MessagePreview() { - const { state } = use(ComposerContext) - return + const { state } = use(ComposerContext); + return ; } ``` @@ -582,15 +562,15 @@ or awkward refs. ```tsx function ForwardMessageComposer() { - const [state, setState] = useState(initialState) - const forwardMessage = useForwardMessage() + const [state, setState] = useState(initialState); + const forwardMessage = useForwardMessage(); return ( - ) + ); } // Problem: How does this button access composer state? @@ -604,7 +584,7 @@ function ForwardMessageDialog() { {/* Needs to call submit */} - ) + ); } ``` @@ -612,20 +592,20 @@ function ForwardMessageDialog() { ```tsx function ForwardMessageDialog() { - const [input, setInput] = useState('') + const [input, setInput] = useState(''); return ( - ) + ); } function ForwardMessageComposer({ onInputChange }) { - const [state, setState] = useState(initialState) + const [state, setState] = useState(initialState); useEffect(() => { - onInputChange(state.input) // Sync on every change 😬 - }, [state.input]) + onInputChange(state.input); // Sync on every change 😬 + }, [state.input]); } ``` @@ -633,13 +613,13 @@ function ForwardMessageComposer({ onInputChange }) { ```tsx function ForwardMessageDialog() { - const stateRef = useRef(null) + const stateRef = useRef(null); return ( submit(stateRef.current)} /> - ) + ); } ``` @@ -647,9 +627,9 @@ function ForwardMessageDialog() { ```tsx function ForwardMessageProvider({ children }: { children: React.ReactNode }) { - const [state, setState] = useState(initialState) - const forwardMessage = useForwardMessage() - const inputRef = useRef(null) + const [state, setState] = useState(initialState); + const forwardMessage = useForwardMessage(); + const inputRef = useRef(null); return ( {children} - ) + ); } function ForwardMessageDialog() { @@ -674,12 +654,12 @@ function ForwardMessageDialog() { - ) + ); } function ForwardButton() { - const { actions } = use(Composer.Context) - return + const { actions } = use(Composer.Context); + return ; } ``` @@ -718,13 +698,7 @@ itself. ```tsx // What does this component actually render? - + ``` **Correct: explicit variants** @@ -760,7 +734,7 @@ function ThreadComposer({ channelId }: { channelId: string }) { - ) + ); } function EditMessageComposer({ messageId }: { messageId: string }) { @@ -776,7 +750,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) { - ) + ); } function ForwardMessageComposer({ messageId }: { messageId: string }) { @@ -791,7 +765,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) { - ) + ); } ``` @@ -823,9 +797,9 @@ function Composer({ renderFooter, renderActions, }: { - renderHeader?: () => React.ReactNode - renderFooter?: () => React.ReactNode - renderActions?: () => React.ReactNode + renderHeader?: () => React.ReactNode; + renderFooter?: () => React.ReactNode; + renderActions?: () => React.ReactNode; }) { return (
@@ -834,7 +808,7 @@ function Composer({ {renderFooter ? renderFooter() : } {renderActions?.()} - ) + ); } // Usage is awkward and inflexible @@ -849,18 +823,18 @@ return ( )} renderActions={() => } /> -) +); ``` **Correct: compound components with children** ```tsx function ComposerFrame({ children }: { children: React.ReactNode }) { - return
{children}
+ return
{children}
; } function ComposerFooter({ children }: { children: React.ReactNode }) { - return
{children}
+ return
{children}
; } // Usage is flexible @@ -874,17 +848,14 @@ return ( -) +); ``` **When render props are appropriate:** ```tsx // Render props work well when you need to pass data back - } -/> + } /> ``` Use render props when the parent needs to provide data or state to the child. @@ -911,28 +882,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and ` ```tsx const ComposerInput = forwardRef((props, ref) => { - return -}) + return ; +}); ``` **Correct: ref as a regular prop** ```tsx function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref }) { - return + return ; } ``` **Incorrect: useContext in React 19** ```tsx -const value = useContext(MyContext) +const value = useContext(MyContext); ``` **Correct: use instead of useContext** ```tsx -const value = use(MyContext) +const value = use(MyContext); ``` `use()` can also be called conditionally, unlike `useContext()`. diff --git a/.agent/skills/vercel-composition-patterns/SKILL.md b/.agent/skills/vercel-composition-patterns/SKILL.md index d07025b..ddfb53e 100644 --- a/.agent/skills/vercel-composition-patterns/SKILL.md +++ b/.agent/skills/vercel-composition-patterns/SKILL.md @@ -1,7 +1,6 @@ --- name: vercel-composition-patterns -description: - React composition patterns that scale. Use when refactoring components with +description: React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 diff --git a/.agent/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md b/.agent/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md index ccee19c..8a0f998 100644 --- a/.agent/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +++ b/.agent/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md @@ -32,16 +32,10 @@ function Composer({ ) : isThread ? ( ) : null} - {isEditing ? ( - - ) : isForwarding ? ( - - ) : ( - - )} + {isEditing ? : isForwarding ? : }
- ) + ); } ``` @@ -61,7 +55,7 @@ function ChannelComposer() { - ) + ); } // Thread composer - adds "also send to channel" field @@ -77,7 +71,7 @@ function ThreadComposer({ channelId }: { channelId: string }) { - ) + ); } // Edit composer - different footer actions @@ -92,7 +86,7 @@ function EditComposer() { - ) + ); } ``` diff --git a/.agent/skills/vercel-composition-patterns/rules/architecture-compound-components.md b/.agent/skills/vercel-composition-patterns/rules/architecture-compound-components.md index e5e3043..e6677a7 100644 --- a/.agent/skills/vercel-composition-patterns/rules/architecture-compound-components.md +++ b/.agent/skills/vercel-composition-patterns/rules/architecture-compound-components.md @@ -37,25 +37,21 @@ function Composer({
)} - ) + ); } ``` **Correct (compound components with shared context):** ```tsx -const ComposerContext = createContext(null) +const ComposerContext = createContext(null); function ComposerProvider({ children, state, actions, meta }: ProviderProps) { - return ( - - {children} - - ) + return {children}; } function ComposerFrame({ children }: { children: React.ReactNode }) { - return
{children}
+ return
{children}
; } function ComposerInput() { @@ -63,21 +59,21 @@ function ComposerInput() { state, actions: { update }, meta: { inputRef }, - } = use(ComposerContext) + } = use(ComposerContext); return ( update((s) => ({ ...s, input: text }))} /> - ) + ); } function ComposerSubmit() { const { actions: { submit }, - } = use(ComposerContext) - return + } = use(ComposerContext); + return ; } // Export as compound component @@ -91,7 +87,7 @@ const Composer = { Attachments: ComposerAttachments, Formatting: ComposerFormatting, Emojis: ComposerEmojis, -} +}; ``` **Usage:** diff --git a/.agent/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md b/.agent/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md index d4345ee..7c0ea4c 100644 --- a/.agent/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +++ b/.agent/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md @@ -19,9 +19,9 @@ function Composer({ renderFooter, renderActions, }: { - renderHeader?: () => React.ReactNode - renderFooter?: () => React.ReactNode - renderActions?: () => React.ReactNode + renderHeader?: () => React.ReactNode; + renderFooter?: () => React.ReactNode; + renderActions?: () => React.ReactNode; }) { return (
@@ -30,7 +30,7 @@ function Composer({ {renderFooter ? renderFooter() : } {renderActions?.()} - ) + ); } // Usage is awkward and inflexible @@ -45,18 +45,18 @@ return ( )} renderActions={() => } /> -) +); ``` **Correct (compound components with children):** ```tsx function ComposerFrame({ children }: { children: React.ReactNode }) { - return
{children}
+ return
{children}
; } function ComposerFooter({ children }: { children: React.ReactNode }) { - return
{children}
+ return
{children}
; } // Usage is flexible @@ -70,17 +70,14 @@ return ( -) +); ``` **When render props are appropriate:** ```tsx // Render props work well when you need to pass data back - } -/> + } /> ``` Use render props when the parent needs to provide data or state to the child. diff --git a/.agent/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md b/.agent/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md index 56e32e8..b20382c 100644 --- a/.agent/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +++ b/.agent/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md @@ -15,13 +15,7 @@ itself. ```tsx // What does this component actually render? - + ``` **Correct (explicit variants):** @@ -56,7 +50,7 @@ function ThreadComposer({ channelId }: { channelId: string }) { - ) + ); } function EditMessageComposer({ messageId }: { messageId: string }) { @@ -72,7 +66,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) { - ) + ); } function ForwardMessageComposer({ messageId }: { messageId: string }) { @@ -87,7 +81,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) { - ) + ); } ``` diff --git a/.agent/skills/vercel-composition-patterns/rules/react19-no-forwardref.md b/.agent/skills/vercel-composition-patterns/rules/react19-no-forwardref.md index e0d8f8a..d5e80a7 100644 --- a/.agent/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +++ b/.agent/skills/vercel-composition-patterns/rules/react19-no-forwardref.md @@ -15,28 +15,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and ` ```tsx const ComposerInput = forwardRef((props, ref) => { - return -}) + return ; +}); ``` **Correct (ref as a regular prop):** ```tsx function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref }) { - return + return ; } ``` **Incorrect (useContext in React 19):** ```tsx -const value = useContext(MyContext) +const value = useContext(MyContext); ``` **Correct (use instead of useContext):** ```tsx -const value = use(MyContext) +const value = use(MyContext); ``` `use()` can also be called conditionally, unlike `useContext()`. diff --git a/.agent/skills/vercel-composition-patterns/rules/state-context-interface.md b/.agent/skills/vercel-composition-patterns/rules/state-context-interface.md index d961bed..6a680ea 100644 --- a/.agent/skills/vercel-composition-patterns/rules/state-context-interface.md +++ b/.agent/skills/vercel-composition-patterns/rules/state-context-interface.md @@ -20,8 +20,8 @@ dependency-injectable. ```tsx function ComposerInput() { // Tightly coupled to a specific hook - const { input, setInput } = useChannelComposerState() - return + const { input, setInput } = useChannelComposerState(); + return ; } ``` @@ -30,27 +30,27 @@ function ComposerInput() { ```tsx // Define a GENERIC interface that any provider can implement interface ComposerState { - input: string - attachments: Attachment[] - isSubmitting: boolean + input: string; + attachments: Attachment[]; + isSubmitting: boolean; } interface ComposerActions { - update: (updater: (state: ComposerState) => ComposerState) => void - submit: () => void + update: (updater: (state: ComposerState) => ComposerState) => void; + submit: () => void; } interface ComposerMeta { - inputRef: React.RefObject + inputRef: React.RefObject; } interface ComposerContextValue { - state: ComposerState - actions: ComposerActions - meta: ComposerMeta + state: ComposerState; + actions: ComposerActions; + meta: ComposerMeta; } -const ComposerContext = createContext(null) +const ComposerContext = createContext(null); ``` **UI components consume the interface, not the implementation:** @@ -61,7 +61,7 @@ function ComposerInput() { state, actions: { update }, meta, - } = use(ComposerContext) + } = use(ComposerContext); // This component works with ANY provider that implements the interface return ( @@ -70,7 +70,7 @@ function ComposerInput() { value={state.input} onChangeText={(text) => update((s) => ({ ...s, input: text }))} /> - ) + ); } ``` @@ -79,9 +79,9 @@ function ComposerInput() { ```tsx // Provider A: Local state for ephemeral forms function ForwardMessageProvider({ children }: { children: React.ReactNode }) { - const [state, setState] = useState(initialState) - const inputRef = useRef(null) - const submit = useForwardMessage() + const [state, setState] = useState(initialState); + const inputRef = useRef(null); + const submit = useForwardMessage(); return ( {children} - ) + ); } // Provider B: Global synced state for channels function ChannelProvider({ channelId, children }: Props) { - const { state, update, submit } = useGlobalChannel(channelId) - const inputRef = useRef(null) + const { state, update, submit } = useGlobalChannel(channelId); + const inputRef = useRef(null); return ( {children} - ) + ); } ``` @@ -165,21 +165,21 @@ function ForwardMessageDialog() { - ) + ); } // This button lives OUTSIDE Composer.Frame but can still submit based on its context! function ForwardButton() { const { actions: { submit }, - } = use(ComposerContext) - return + } = use(ComposerContext); + return ; } // This preview lives OUTSIDE Composer.Frame but can read composer's state! function MessagePreview() { - const { state } = use(ComposerContext) - return + const { state } = use(ComposerContext); + return ; } ``` diff --git a/.agent/skills/vercel-composition-patterns/rules/state-decouple-implementation.md b/.agent/skills/vercel-composition-patterns/rules/state-decouple-implementation.md index 71a5afa..f15432d 100644 --- a/.agent/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +++ b/.agent/skills/vercel-composition-patterns/rules/state-decouple-implementation.md @@ -16,18 +16,15 @@ useState, Zustand, or a server sync. ```tsx function ChannelComposer({ channelId }: { channelId: string }) { // UI component knows about global state implementation - const state = useGlobalChannelState(channelId) - const { submit, updateInput } = useChannelSync(channelId) + const state = useGlobalChannelState(channelId); + const { submit, updateInput } = useChannelSync(channelId); return ( - sync.updateInput(text)} - /> + sync.updateInput(text)} /> sync.submit()} /> - ) + ); } ``` @@ -39,21 +36,17 @@ function ChannelProvider({ channelId, children, }: { - channelId: string - children: React.ReactNode + channelId: string; + children: React.ReactNode; }) { - const { state, update, submit } = useGlobalChannel(channelId) - const inputRef = useRef(null) + const { state, update, submit } = useGlobalChannel(channelId); + const inputRef = useRef(null); return ( - + {children} - ) + ); } // UI component only knows about the context interface @@ -66,7 +59,7 @@ function ChannelComposer() { - ) + ); } // Usage @@ -75,7 +68,7 @@ function Channel({ channelId }: { channelId: string }) { - ) + ); } ``` @@ -84,28 +77,25 @@ function Channel({ channelId }: { channelId: string }) { ```tsx // Local state for ephemeral forms function ForwardMessageProvider({ children }) { - const [state, setState] = useState(initialState) - const forwardMessage = useForwardMessage() + const [state, setState] = useState(initialState); + const forwardMessage = useForwardMessage(); return ( - + {children} - ) + ); } // Global synced state for channels function ChannelProvider({ channelId, children }) { - const { state, update, submit } = useGlobalChannel(channelId) + const { state, update, submit } = useGlobalChannel(channelId); return ( {children} - ) + ); } ``` diff --git a/.agent/skills/vercel-composition-patterns/rules/state-lift-state.md b/.agent/skills/vercel-composition-patterns/rules/state-lift-state.md index d7fe27b..b1e5e8e 100644 --- a/.agent/skills/vercel-composition-patterns/rules/state-lift-state.md +++ b/.agent/skills/vercel-composition-patterns/rules/state-lift-state.md @@ -15,15 +15,15 @@ or awkward refs. ```tsx function ForwardMessageComposer() { - const [state, setState] = useState(initialState) - const forwardMessage = useForwardMessage() + const [state, setState] = useState(initialState); + const forwardMessage = useForwardMessage(); return ( - ) + ); } // Problem: How does this button access composer state? @@ -37,7 +37,7 @@ function ForwardMessageDialog() { {/* Needs to call submit */} - ) + ); } ``` @@ -45,20 +45,20 @@ function ForwardMessageDialog() { ```tsx function ForwardMessageDialog() { - const [input, setInput] = useState('') + const [input, setInput] = useState(''); return ( - ) + ); } function ForwardMessageComposer({ onInputChange }) { - const [state, setState] = useState(initialState) + const [state, setState] = useState(initialState); useEffect(() => { - onInputChange(state.input) // Sync on every change 😬 - }, [state.input]) + onInputChange(state.input); // Sync on every change 😬 + }, [state.input]); } ``` @@ -66,13 +66,13 @@ function ForwardMessageComposer({ onInputChange }) { ```tsx function ForwardMessageDialog() { - const stateRef = useRef(null) + const stateRef = useRef(null); return ( submit(stateRef.current)} /> - ) + ); } ``` @@ -80,9 +80,9 @@ function ForwardMessageDialog() { ```tsx function ForwardMessageProvider({ children }: { children: React.ReactNode }) { - const [state, setState] = useState(initialState) - const forwardMessage = useForwardMessage() - const inputRef = useRef(null) + const [state, setState] = useState(initialState); + const forwardMessage = useForwardMessage(); + const inputRef = useRef(null); return ( {children} - ) + ); } function ForwardMessageDialog() { @@ -107,12 +107,12 @@ function ForwardMessageDialog() { - ) + ); } function ForwardButton() { - const { actions } = use(Composer.Context) - return + const { actions } = use(Composer.Context); + return ; } ``` diff --git a/.agent/skills/vercel-react-best-practices/AGENTS.md b/.agent/skills/vercel-react-best-practices/AGENTS.md index db951ab..c5d62f5 100644 --- a/.agent/skills/vercel-react-best-practices/AGENTS.md +++ b/.agent/skills/vercel-react-best-practices/AGENTS.md @@ -104,15 +104,15 @@ Move `await` operations into the branches where they're actually used to avoid b ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { - const userData = await fetchUserData(userId) - + const userData = await fetchUserData(userId); + if (skipProcessing) { // Returns immediately but still waited for userData - return { skipped: true } + return { skipped: true }; } - + // Only this branch uses userData - return processUserData(userData) + return processUserData(userData); } ``` @@ -122,12 +122,12 @@ async function handleRequest(userId: string, skipProcessing: boolean) { async function handleRequest(userId: string, skipProcessing: boolean) { if (skipProcessing) { // Returns immediately without waiting - return { skipped: true } + return { skipped: true }; } - + // Fetch only when needed - const userData = await fetchUserData(userId) - return processUserData(userData) + const userData = await fetchUserData(userId); + return processUserData(userData); } ``` @@ -136,35 +136,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) { ```typescript // Incorrect: always fetches permissions async function updateResource(resourceId: string, userId: string) { - const permissions = await fetchPermissions(userId) - const resource = await getResource(resourceId) - + const permissions = await fetchPermissions(userId); + const resource = await getResource(resourceId); + if (!resource) { - return { error: 'Not found' } + return { error: 'Not found' }; } - + if (!permissions.canEdit) { - return { error: 'Forbidden' } + return { error: 'Forbidden' }; } - - return await updateResourceData(resource, permissions) + + return await updateResourceData(resource, permissions); } // Correct: fetches only when needed async function updateResource(resourceId: string, userId: string) { - const resource = await getResource(resourceId) - + const resource = await getResource(resourceId); + if (!resource) { - return { error: 'Not found' } + return { error: 'Not found' }; } - - const permissions = await fetchPermissions(userId) - + + const permissions = await fetchPermissions(userId); + if (!permissions.canEdit) { - return { error: 'Forbidden' } + return { error: 'Forbidden' }; } - - return await updateResourceData(resource, permissions) + + return await updateResourceData(resource, permissions); } ``` @@ -179,38 +179,35 @@ For operations with partial dependencies, use `better-all` to maximize paralleli **Incorrect: profile waits for config unnecessarily** ```typescript -const [user, config] = await Promise.all([ - fetchUser(), - fetchConfig() -]) -const profile = await fetchProfile(user.id) +const [user, config] = await Promise.all([fetchUser(), fetchConfig()]); +const profile = await fetchProfile(user.id); ``` **Correct: config and profile run in parallel** ```typescript -import { all } from 'better-all' +import { all } from 'better-all'; const { user, config, profile } = await all({ - async user() { return fetchUser() }, - async config() { return fetchConfig() }, + async user() { + return fetchUser(); + }, + async config() { + return fetchConfig(); + }, async profile() { - return fetchProfile((await this.$.user).id) - } -}) + return fetchProfile((await this.$.user).id); + }, +}); ``` **Alternative without extra dependencies:** ```typescript -const userPromise = fetchUser() -const profilePromise = userPromise.then(user => fetchProfile(user.id)) +const userPromise = fetchUser(); +const profilePromise = userPromise.then((user) => fetchProfile(user.id)); -const [user, config, profile] = await Promise.all([ - userPromise, - fetchConfig(), - profilePromise -]) +const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]); ``` We can also create all the promises first, and do `Promise.all()` at the end. @@ -227,10 +224,10 @@ In API routes and Server Actions, start independent operations immediately, even ```typescript export async function GET(request: Request) { - const session = await auth() - const config = await fetchConfig() - const data = await fetchData(session.user.id) - return Response.json({ data, config }) + const session = await auth(); + const config = await fetchConfig(); + const data = await fetchData(session.user.id); + return Response.json({ data, config }); } ``` @@ -238,14 +235,11 @@ export async function GET(request: Request) { ```typescript export async function GET(request: Request) { - const sessionPromise = auth() - const configPromise = fetchConfig() - const session = await sessionPromise - const [config, data] = await Promise.all([ - configPromise, - fetchData(session.user.id) - ]) - return Response.json({ data, config }) + const sessionPromise = auth(); + const configPromise = fetchConfig(); + const session = await sessionPromise; + const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]); + return Response.json({ data, config }); } ``` @@ -260,19 +254,15 @@ When async operations have no interdependencies, execute them concurrently using **Incorrect: sequential execution, 3 round trips** ```typescript -const user = await fetchUser() -const posts = await fetchPosts() -const comments = await fetchComments() +const user = await fetchUser(); +const posts = await fetchPosts(); +const comments = await fetchComments(); ``` **Correct: parallel execution, 1 round trip** ```typescript -const [user, posts, comments] = await Promise.all([ - fetchUser(), - fetchPosts(), - fetchComments() -]) +const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]); ``` ### 1.5 Strategic Suspense Boundaries @@ -285,8 +275,8 @@ Instead of awaiting data in async components before returning JSX, use Suspense ```tsx async function Page() { - const data = await fetchData() // Blocks entire page - + const data = await fetchData(); // Blocks entire page + return (
Sidebar
@@ -296,7 +286,7 @@ async function Page() {
Footer
- ) + ); } ``` @@ -317,12 +307,12 @@ function Page() {
Footer
- ) + ); } async function DataDisplay() { - const data = await fetchData() // Only blocks this component - return
{data.content}
+ const data = await fetchData(); // Only blocks this component + return
{data.content}
; } ``` @@ -333,8 +323,8 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. ```tsx function Page() { // Start fetch immediately, but don't await - const dataPromise = fetchData() - + const dataPromise = fetchData(); + return (
Sidebar
@@ -345,17 +335,17 @@ function Page() {
Footer
- ) + ); } function DataDisplay({ dataPromise }: { dataPromise: Promise }) { - const data = use(dataPromise) // Unwraps the promise - return
{data.content}
+ const data = use(dataPromise); // Unwraps the promise + return
{data.content}
; } function DataSummary({ dataPromise }: { dataPromise: Promise }) { - const data = use(dataPromise) // Reuses the same promise - return
{data.summary}
+ const data = use(dataPromise); // Reuses the same promise + return
{data.summary}
; } ``` @@ -394,24 +384,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the **Incorrect: imports entire library** ```tsx -import { Check, X, Menu } from 'lucide-react' +import { Check, X, Menu } from 'lucide-react'; // Loads 1,583 modules, takes ~2.8s extra in dev // Runtime cost: 200-800ms on every cold start -import { Button, TextField } from '@mui/material' +import { Button, TextField } from '@mui/material'; // Loads 2,225 modules, takes ~4.2s extra in dev ``` **Correct: imports only what you need** ```tsx -import Check from 'lucide-react/dist/esm/icons/check' -import X from 'lucide-react/dist/esm/icons/x' -import Menu from 'lucide-react/dist/esm/icons/menu' +import Check from 'lucide-react/dist/esm/icons/check'; +import X from 'lucide-react/dist/esm/icons/x'; +import Menu from 'lucide-react/dist/esm/icons/menu'; // Loads only 3 modules (~2KB vs ~1MB) -import Button from '@mui/material/Button' -import TextField from '@mui/material/TextField' +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; // Loads only what you use ``` @@ -421,12 +411,12 @@ import TextField from '@mui/material/TextField' // next.config.js - use optimizePackageImports module.exports = { experimental: { - optimizePackageImports: ['lucide-react', '@mui/material'] - } -} + optimizePackageImports: ['lucide-react', '@mui/material'], + }, +}; // Then you can keep the ergonomic barrel imports: -import { Check, X, Menu } from 'lucide-react' +import { Check, X, Menu } from 'lucide-react'; // Automatically transformed to direct imports at build time ``` @@ -445,19 +435,25 @@ Load large data or modules only when a feature is activated. **Example: lazy-load animation frames** ```tsx -function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) { - const [frames, setFrames] = useState(null) +function AnimationPlayer({ + enabled, + setEnabled, +}: { + enabled: boolean; + setEnabled: React.Dispatch>; +}) { + const [frames, setFrames] = useState(null); useEffect(() => { if (enabled && !frames && typeof window !== 'undefined') { import('./animation-frames.js') - .then(mod => setFrames(mod.frames)) - .catch(() => setEnabled(false)) + .then((mod) => setFrames(mod.frames)) + .catch(() => setEnabled(false)); } - }, [enabled, frames, setEnabled]) + }, [enabled, frames, setEnabled]); - if (!frames) return - return + if (!frames) return ; + return ; } ``` @@ -472,7 +468,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a **Incorrect: blocks initial bundle** ```tsx -import { Analytics } from '@vercel/analytics/react' +import { Analytics } from '@vercel/analytics/react'; export default function RootLayout({ children }) { return ( @@ -482,19 +478,18 @@ export default function RootLayout({ children }) { - ) + ); } ``` **Correct: loads after hydration** ```tsx -import dynamic from 'next/dynamic' +import dynamic from 'next/dynamic'; -const Analytics = dynamic( - () => import('@vercel/analytics/react').then(m => m.Analytics), - { ssr: false } -) +const Analytics = dynamic(() => import('@vercel/analytics/react').then((m) => m.Analytics), { + ssr: false, +}); export default function RootLayout({ children }) { return ( @@ -504,7 +499,7 @@ export default function RootLayout({ children }) { - ) + ); } ``` @@ -517,25 +512,24 @@ Use `next/dynamic` to lazy-load large components not needed on initial render. **Incorrect: Monaco bundles with main chunk ~300KB** ```tsx -import { MonacoEditor } from './monaco-editor' +import { MonacoEditor } from './monaco-editor'; function CodePanel({ code }: { code: string }) { - return + return ; } ``` **Correct: Monaco loads on demand** ```tsx -import dynamic from 'next/dynamic' +import dynamic from 'next/dynamic'; -const MonacoEditor = dynamic( - () => import('./monaco-editor').then(m => m.MonacoEditor), - { ssr: false } -) +const MonacoEditor = dynamic(() => import('./monaco-editor').then((m) => m.MonacoEditor), { + ssr: false, +}); function CodePanel({ code }: { code: string }) { - return + return ; } ``` @@ -551,19 +545,15 @@ Preload heavy bundles before they're needed to reduce perceived latency. function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== 'undefined') { - void import('./monaco-editor') + void import('./monaco-editor'); } - } + }; return ( - - ) + ); } ``` @@ -573,13 +563,11 @@ function EditorButton({ onClick }: { onClick: () => void }) { function FlagsProvider({ children, flags }: Props) { useEffect(() => { if (flags.editorEnabled && typeof window !== 'undefined') { - void import('./monaco-editor').then(mod => mod.init()) + void import('./monaco-editor').then((mod) => mod.init()); } - }, [flags.editorEnabled]) + }, [flags.editorEnabled]); - return - {children} - + return {children}; } ``` @@ -604,80 +592,80 @@ Next.js documentation explicitly states: "Treat Server Actions with the same sec **Incorrect: no authentication check** ```typescript -'use server' +'use server'; export async function deleteUser(userId: string) { // Anyone can call this! No auth check - await db.user.delete({ where: { id: userId } }) - return { success: true } + await db.user.delete({ where: { id: userId } }); + return { success: true }; } ``` **Correct: authentication inside the action** ```typescript -'use server' +'use server'; -import { verifySession } from '@/lib/auth' -import { unauthorized } from '@/lib/errors' +import { verifySession } from '@/lib/auth'; +import { unauthorized } from '@/lib/errors'; export async function deleteUser(userId: string) { // Always check auth inside the action - const session = await verifySession() - + const session = await verifySession(); + if (!session) { - throw unauthorized('Must be logged in') + throw unauthorized('Must be logged in'); } - + // Check authorization too if (session.user.role !== 'admin' && session.user.id !== userId) { - throw unauthorized('Cannot delete other users') + throw unauthorized('Cannot delete other users'); } - - await db.user.delete({ where: { id: userId } }) - return { success: true } + + await db.user.delete({ where: { id: userId } }); + return { success: true }; } ``` **With input validation:** ```typescript -'use server' +'use server'; -import { verifySession } from '@/lib/auth' -import { z } from 'zod' +import { verifySession } from '@/lib/auth'; +import { z } from 'zod'; const updateProfileSchema = z.object({ userId: z.string().uuid(), name: z.string().min(1).max(100), - email: z.string().email() -}) + email: z.string().email(), +}); export async function updateProfile(data: unknown) { // Validate input first - const validated = updateProfileSchema.parse(data) - + const validated = updateProfileSchema.parse(data); + // Then authenticate - const session = await verifySession() + const session = await verifySession(); if (!session) { - throw new Error('Unauthorized') + throw new Error('Unauthorized'); } - + // Then authorize if (session.user.id !== validated.userId) { - throw new Error('Can only update own profile') + throw new Error('Can only update own profile'); } - + // Finally perform the mutation await db.user.update({ where: { id: validated.userId }, data: { name: validated.name, - email: validated.email - } - }) - - return { success: true } + email: validated.email, + }, + }); + + return { success: true }; } ``` @@ -700,11 +688,11 @@ RSC→client serialization deduplicates by object reference, not value. Same ref ```tsx // RSC: send once - +; // Client: transform there -'use client' -const sorted = useMemo(() => [...usernames].sort(), [usernames]) +('use client'); +const sorted = useMemo(() => [...usernames].sort(), [usernames]); ``` **Nested deduplication behavior:** @@ -753,20 +741,20 @@ Deduplication works recursively. Impact varies by data type: **Implementation:** ```typescript -import { LRUCache } from 'lru-cache' +import { LRUCache } from 'lru-cache'; const cache = new LRUCache({ max: 1000, - ttl: 5 * 60 * 1000 // 5 minutes -}) + ttl: 5 * 60 * 1000, // 5 minutes +}); export async function getUser(id: string) { - const cached = cache.get(id) - if (cached) return cached + const cached = cache.get(id); + if (cached) return cached; - const user = await db.user.findUnique({ where: { id } }) - cache.set(id, user) - return user + const user = await db.user.findUnique({ where: { id } }); + cache.set(id, user); + return user; } // Request 1: DB query, result cached @@ -791,13 +779,13 @@ The React Server/Client boundary serializes all object properties into strings a ```tsx async function Page() { - const user = await fetchUser() // 50 fields - return + const user = await fetchUser(); // 50 fields + return ; } -'use client' +('use client'); function Profile({ user }: { user: User }) { - return
{user.name}
// uses 1 field + return
{user.name}
; // uses 1 field } ``` @@ -805,13 +793,13 @@ function Profile({ user }: { user: User }) { ```tsx async function Page() { - const user = await fetchUser() - return + const user = await fetchUser(); + return ; } -'use client' +('use client'); function Profile({ name }: { name: string }) { - return
{name}
+ return
{name}
; } ``` @@ -825,18 +813,18 @@ React Server Components execute sequentially within a tree. Restructure with com ```tsx export default async function Page() { - const header = await fetchHeader() + const header = await fetchHeader(); return (
{header}
- ) + ); } async function Sidebar() { - const items = await fetchSidebarItems() - return + const items = await fetchSidebarItems(); + return ; } ``` @@ -844,13 +832,13 @@ async function Sidebar() { ```tsx async function Header() { - const data = await fetchHeader() - return
{data}
+ const data = await fetchHeader(); + return
{data}
; } async function Sidebar() { - const items = await fetchSidebarItems() - return + const items = await fetchSidebarItems(); + return ; } export default function Page() { @@ -859,7 +847,7 @@ export default function Page() {
- ) + ); } ``` @@ -867,13 +855,13 @@ export default function Page() { ```tsx async function Header() { - const data = await fetchHeader() - return
{data}
+ const data = await fetchHeader(); + return
{data}
; } async function Sidebar() { - const items = await fetchSidebarItems() - return + const items = await fetchSidebarItems(); + return ; } function Layout({ children }: { children: ReactNode }) { @@ -882,7 +870,7 @@ function Layout({ children }: { children: ReactNode }) {
{children} - ) + ); } export default function Page() { @@ -890,7 +878,7 @@ export default function Page() { - ) + ); } ``` @@ -903,15 +891,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da **Usage:** ```typescript -import { cache } from 'react' +import { cache } from 'react'; export const getCurrentUser = cache(async () => { - const session = await auth() - if (!session?.user?.id) return null + const session = await auth(); + if (!session?.user?.id) return null; return await db.user.findUnique({ - where: { id: session.user.id } - }) -}) + where: { id: session.user.id }, + }); +}); ``` Within a single request, multiple calls to `getCurrentUser()` execute the query only once. @@ -924,20 +912,20 @@ Within a single request, multiple calls to `getCurrentUser()` execute the query ```typescript const getUser = cache(async (params: { uid: number }) => { - return await db.user.findUnique({ where: { id: params.uid } }) -}) + return await db.user.findUnique({ where: { id: params.uid } }); +}); // Each call creates new object, never hits cache -getUser({ uid: 1 }) -getUser({ uid: 1 }) // Cache miss, runs query again +getUser({ uid: 1 }); +getUser({ uid: 1 }); // Cache miss, runs query again ``` **Correct: cache hit** ```typescript -const params = { uid: 1 } -getUser(params) // Query runs -getUser(params) // Cache hit (same reference) +const params = { uid: 1 }; +getUser(params); // Query runs +getUser(params); // Cache hit (same reference) ``` If you must pass objects, pass the same reference: @@ -969,46 +957,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is **Incorrect: blocks response** ```tsx -import { logUserAction } from '@/app/utils' +import { logUserAction } from '@/app/utils'; export async function POST(request: Request) { // Perform mutation - await updateDatabase(request) - + await updateDatabase(request); + // Logging blocks the response - const userAgent = request.headers.get('user-agent') || 'unknown' - await logUserAction({ userAgent }) - + const userAgent = request.headers.get('user-agent') || 'unknown'; + await logUserAction({ userAgent }); + return new Response(JSON.stringify({ status: 'success' }), { status: 200, - headers: { 'Content-Type': 'application/json' } - }) + headers: { 'Content-Type': 'application/json' }, + }); } ``` **Correct: non-blocking** ```tsx -import { after } from 'next/server' -import { headers, cookies } from 'next/headers' -import { logUserAction } from '@/app/utils' +import { after } from 'next/server'; +import { headers, cookies } from 'next/headers'; +import { logUserAction } from '@/app/utils'; export async function POST(request: Request) { // Perform mutation - await updateDatabase(request) - + await updateDatabase(request); + // Log after response is sent after(async () => { - const userAgent = (await headers()).get('user-agent') || 'unknown' - const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' - - logUserAction({ sessionCookie, userAgent }) - }) - + const userAgent = (await headers()).get('user-agent') || 'unknown'; + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'; + + logUserAction({ sessionCookie, userAgent }); + }); + return new Response(JSON.stringify({ status: 'success' }), { status: 200, - headers: { 'Content-Type': 'application/json' } - }) + headers: { 'Content-Type': 'application/json' }, + }); } ``` @@ -1055,12 +1043,12 @@ function useKeyboardShortcut(key: string, callback: () => void) { useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.metaKey && e.key === key) { - callback() + callback(); } - } - window.addEventListener('keydown', handler) - return () => window.removeEventListener('keydown', handler) - }, [key, callback]) + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [key, callback]); } ``` @@ -1069,45 +1057,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg **Correct: N instances = 1 listener** ```tsx -import useSWRSubscription from 'swr/subscription' +import useSWRSubscription from 'swr/subscription'; // Module-level Map to track callbacks per key -const keyCallbacks = new Map void>>() +const keyCallbacks = new Map void>>(); function useKeyboardShortcut(key: string, callback: () => void) { // Register this callback in the Map useEffect(() => { if (!keyCallbacks.has(key)) { - keyCallbacks.set(key, new Set()) + keyCallbacks.set(key, new Set()); } - keyCallbacks.get(key)!.add(callback) + keyCallbacks.get(key)!.add(callback); return () => { - const set = keyCallbacks.get(key) + const set = keyCallbacks.get(key); if (set) { - set.delete(callback) + set.delete(callback); if (set.size === 0) { - keyCallbacks.delete(key) + keyCallbacks.delete(key); } } - } - }, [key, callback]) + }; + }, [key, callback]); useSWRSubscription('global-keydown', () => { const handler = (e: KeyboardEvent) => { if (e.metaKey && keyCallbacks.has(e.key)) { - keyCallbacks.get(e.key)!.forEach(cb => cb()) + keyCallbacks.get(e.key)!.forEach((cb) => cb()); } - } - window.addEventListener('keydown', handler) - return () => window.removeEventListener('keydown', handler) - }) + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }); } function Profile() { // Multiple shortcuts will share the same listener - useKeyboardShortcut('p', () => { /* ... */ }) - useKeyboardShortcut('k', () => { /* ... */ }) + useKeyboardShortcut('p', () => { + /* ... */ + }); + useKeyboardShortcut('k', () => { + /* ... */ + }); // ... } ``` @@ -1122,34 +1114,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s ```typescript useEffect(() => { - const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) - const handleWheel = (e: WheelEvent) => console.log(e.deltaY) - - document.addEventListener('touchstart', handleTouch) - document.addEventListener('wheel', handleWheel) - + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); + const handleWheel = (e: WheelEvent) => console.log(e.deltaY); + + document.addEventListener('touchstart', handleTouch); + document.addEventListener('wheel', handleWheel); + return () => { - document.removeEventListener('touchstart', handleTouch) - document.removeEventListener('wheel', handleWheel) - } -}, []) + document.removeEventListener('touchstart', handleTouch); + document.removeEventListener('wheel', handleWheel); + }; +}, []); ``` **Correct:** ```typescript useEffect(() => { - const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) - const handleWheel = (e: WheelEvent) => console.log(e.deltaY) - - document.addEventListener('touchstart', handleTouch, { passive: true }) - document.addEventListener('wheel', handleWheel, { passive: true }) - + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); + const handleWheel = (e: WheelEvent) => console.log(e.deltaY); + + document.addEventListener('touchstart', handleTouch, { passive: true }); + document.addEventListener('wheel', handleWheel, { passive: true }); + return () => { - document.removeEventListener('touchstart', handleTouch) - document.removeEventListener('wheel', handleWheel) - } -}, []) + document.removeEventListener('touchstart', handleTouch); + document.removeEventListener('wheel', handleWheel); + }; +}, []); ``` **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. @@ -1166,43 +1158,43 @@ SWR enables request deduplication, caching, and revalidation across component in ```tsx function UserList() { - const [users, setUsers] = useState([]) + const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') - .then(r => r.json()) - .then(setUsers) - }, []) + .then((r) => r.json()) + .then(setUsers); + }, []); } ``` **Correct: multiple instances share one request** ```tsx -import useSWR from 'swr' +import useSWR from 'swr'; function UserList() { - const { data: users } = useSWR('/api/users', fetcher) + const { data: users } = useSWR('/api/users', fetcher); } ``` **For immutable data:** ```tsx -import { useImmutableSWR } from '@/lib/swr' +import { useImmutableSWR } from '@/lib/swr'; function StaticContent() { - const { data } = useImmutableSWR('/api/config', fetcher) + const { data } = useImmutableSWR('/api/config', fetcher); } ``` **For mutations:** ```tsx -import { useSWRMutation } from 'swr/mutation' +import { useSWRMutation } from 'swr/mutation'; function UpdateButton() { - const { trigger } = useSWRMutation('/api/user', updateUser) - return + const { trigger } = useSWRMutation('/api/user', updateUser); + return ; } ``` @@ -1218,18 +1210,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic ```typescript // No version, stores everything, no error handling -localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) -const data = localStorage.getItem('userConfig') +localStorage.setItem('userConfig', JSON.stringify(fullUserObject)); +const data = localStorage.getItem('userConfig'); ``` **Correct:** ```typescript -const VERSION = 'v2' +const VERSION = 'v2'; function saveConfig(config: { theme: string; language: string }) { try { - localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)); } catch { // Throws in incognito/private browsing, quota exceeded, or disabled } @@ -1237,21 +1229,21 @@ function saveConfig(config: { theme: string; language: string }) { function loadConfig() { try { - const data = localStorage.getItem(`userConfig:${VERSION}`) - return data ? JSON.parse(data) : null + const data = localStorage.getItem(`userConfig:${VERSION}`); + return data ? JSON.parse(data) : null; } catch { - return null + return null; } } // Migration from v1 to v2 function migrate() { try { - const v1 = localStorage.getItem('userConfig:v1') + const v1 = localStorage.getItem('userConfig:v1'); if (v1) { - const old = JSON.parse(v1) - saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }) - localStorage.removeItem('userConfig:v1') + const old = JSON.parse(v1); + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }); + localStorage.removeItem('userConfig:v1'); } } catch {} } @@ -1263,10 +1255,13 @@ function migrate() { // User object has 20+ fields, only store what UI needs function cachePrefs(user: FullUser) { try { - localStorage.setItem('prefs:v1', JSON.stringify({ - theme: user.preferences.theme, - notifications: user.preferences.notifications - })) + localStorage.setItem( + 'prefs:v1', + JSON.stringify({ + theme: user.preferences.theme, + notifications: user.preferences.notifications, + }), + ); } catch {} } ``` @@ -1293,15 +1288,15 @@ If a value can be computed from current props/state, do not store it in state or ```tsx function Form() { - const [firstName, setFirstName] = useState('First') - const [lastName, setLastName] = useState('Last') - const [fullName, setFullName] = useState('') + const [firstName, setFirstName] = useState('First'); + const [lastName, setLastName] = useState('Last'); + const [fullName, setFullName] = useState(''); useEffect(() => { - setFullName(firstName + ' ' + lastName) - }, [firstName, lastName]) + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); - return

{fullName}

+ return

{fullName}

; } ``` @@ -1309,11 +1304,11 @@ function Form() { ```tsx function Form() { - const [firstName, setFirstName] = useState('First') - const [lastName, setLastName] = useState('Last') - const fullName = firstName + ' ' + lastName + const [firstName, setFirstName] = useState('First'); + const [lastName, setLastName] = useState('Last'); + const fullName = firstName + ' ' + lastName; - return

{fullName}

+ return

{fullName}

; } ``` @@ -1329,14 +1324,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i ```tsx function ShareButton({ chatId }: { chatId: string }) { - const searchParams = useSearchParams() + const searchParams = useSearchParams(); const handleShare = () => { - const ref = searchParams.get('ref') - shareChat(chatId, { ref }) - } + const ref = searchParams.get('ref'); + shareChat(chatId, { ref }); + }; - return + return ; } ``` @@ -1345,12 +1340,12 @@ function ShareButton({ chatId }: { chatId: string }) { ```tsx function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { - const params = new URLSearchParams(window.location.search) - const ref = params.get('ref') - shareChat(chatId, { ref }) - } + const params = new URLSearchParams(window.location.search); + const ref = params.get('ref'); + shareChat(chatId, { ref }); + }; - return + return ; } ``` @@ -1367,10 +1362,10 @@ Calling `useMemo` and comparing hook dependencies may consume more resources tha ```tsx function Header({ user, notifications }: Props) { const isLoading = useMemo(() => { - return user.isLoading || notifications.isLoading - }, [user.isLoading, notifications.isLoading]) + return user.isLoading || notifications.isLoading; + }, [user.isLoading, notifications.isLoading]); - if (isLoading) return + if (isLoading) return ; // return some markup } ``` @@ -1379,9 +1374,9 @@ function Header({ user, notifications }: Props) { ```tsx function Header({ user, notifications }: Props) { - const isLoading = user.isLoading || notifications.isLoading + const isLoading = user.isLoading || notifications.isLoading; - if (isLoading) return + if (isLoading) return ; // return some markup } ``` @@ -1429,12 +1424,12 @@ Extract expensive work into memoized components to enable early returns before c ```tsx function Profile({ user, loading }: Props) { const avatar = useMemo(() => { - const id = computeAvatarId(user) - return - }, [user]) + const id = computeAvatarId(user); + return ; + }, [user]); - if (loading) return - return
{avatar}
+ if (loading) return ; + return
{avatar}
; } ``` @@ -1442,17 +1437,17 @@ function Profile({ user, loading }: Props) { ```tsx const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { - const id = useMemo(() => computeAvatarId(user), [user]) - return -}) + const id = useMemo(() => computeAvatarId(user), [user]); + return ; +}); function Profile({ user, loading }: Props) { - if (loading) return + if (loading) return ; return (
- ) + ); } ``` @@ -1468,16 +1463,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs. ```tsx useEffect(() => { - console.log(user.id) -}, [user]) + console.log(user.id); +}, [user]); ``` **Correct: re-runs only when id changes** ```tsx useEffect(() => { - console.log(user.id) -}, [user.id]) + console.log(user.id); +}, [user.id]); ``` **For derived state, compute outside effect:** @@ -1486,17 +1481,17 @@ useEffect(() => { // Incorrect: runs on width=767, 766, 765... useEffect(() => { if (width < 768) { - enableMobileMode() + enableMobileMode(); } -}, [width]) +}, [width]); // Correct: runs only on boolean transition -const isMobile = width < 768 +const isMobile = width < 768; useEffect(() => { if (isMobile) { - enableMobileMode() + enableMobileMode(); } -}, [isMobile]) +}, [isMobile]); ``` ### 5.7 Put Interaction Logic in Event Handlers @@ -1509,17 +1504,17 @@ If a side effect is triggered by a specific user action (submit, click, drag), r ```tsx function Form() { - const [submitted, setSubmitted] = useState(false) - const theme = useContext(ThemeContext) + const [submitted, setSubmitted] = useState(false); + const theme = useContext(ThemeContext); useEffect(() => { if (submitted) { - post('/api/register') - showToast('Registered', theme) + post('/api/register'); + showToast('Registered', theme); } - }, [submitted, theme]) + }, [submitted, theme]); - return + return ; } ``` @@ -1527,14 +1522,14 @@ function Form() { ```tsx function Form() { - const theme = useContext(ThemeContext) + const theme = useContext(ThemeContext); function handleSubmit() { - post('/api/register') - showToast('Registered', theme) + post('/api/register'); + showToast('Registered', theme); } - return + return ; } ``` @@ -1550,9 +1545,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren ```tsx function Sidebar() { - const width = useWindowWidth() // updates continuously - const isMobile = width < 768 - return