cfsa-antigravity 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +324 -0
- package/package.json +34 -0
- package/template/.agent/instructions/commands.md +48 -0
- package/template/.agent/instructions/patterns.md +61 -0
- package/template/.agent/instructions/structure.md +29 -0
- package/template/.agent/instructions/tech-stack.md +43 -0
- package/template/.agent/instructions/workflow.md +41 -0
- package/template/.agent/kit-sync.md +15 -0
- package/template/.agent/rules/boundary-not-placeholder.md +146 -0
- package/template/.agent/rules/completion-checklist.md +48 -0
- package/template/.agent/rules/decision-classification.md +103 -0
- package/template/.agent/rules/extensibility.md +47 -0
- package/template/.agent/rules/question-vs-command.md +81 -0
- package/template/.agent/rules/security-first.md +43 -0
- package/template/.agent/rules/specificity-standards.md +54 -0
- package/template/.agent/rules/tdd-contract-first.md +57 -0
- package/template/.agent/rules/vertical-slices.md +42 -0
- package/template/.agent/skill-library/MANIFEST.md +480 -0
- package/template/.agent/skill-library/README.md +38 -0
- package/template/.agent/skill-library/meta/brand-guidelines/SKILL.md +73 -0
- package/template/.agent/skill-library/meta/claude-code/README.md +9 -0
- package/template/.agent/skill-library/meta/claude-code/agent-development/SKILL.md +415 -0
- package/template/.agent/skill-library/meta/claude-code/hook-development/SKILL.md +712 -0
- package/template/.agent/skill-library/meta/claude-code/plugin-structure/SKILL.md +476 -0
- package/template/.agent/skill-library/meta/git-advanced/SKILL.md +972 -0
- package/template/.agent/skill-library/meta/mcp-builder/SKILL.md +236 -0
- package/template/.agent/skill-library/meta/product-marketing-context/SKILL.md +241 -0
- package/template/.agent/skill-library/meta/regex-patterns/SKILL.md +751 -0
- package/template/.agent/skill-library/meta/tmux-processes/SKILL.md +210 -0
- package/template/.agent/skill-library/meta/using-tmux-for-interactive-commands/SKILL.md +178 -0
- package/template/.agent/skill-library/stack/3d/threejs-pro/SKILL.md +300 -0
- package/template/.agent/skill-library/stack/ai/ai-sdk/SKILL.md +77 -0
- package/template/.agent/skill-library/stack/ai/langchain/SKILL.md +530 -0
- package/template/.agent/skill-library/stack/ai/ollama/SKILL.md +321 -0
- package/template/.agent/skill-library/stack/ai/openai-sdk/SKILL.md +549 -0
- package/template/.agent/skill-library/stack/analytics/google-analytics/SKILL.md +153 -0
- package/template/.agent/skill-library/stack/api/graphql/SKILL.md +1061 -0
- package/template/.agent/skill-library/stack/api/trpc/SKILL.md +576 -0
- package/template/.agent/skill-library/stack/auth/authjs/SKILL.md +569 -0
- package/template/.agent/skill-library/stack/auth/clerk/SKILL.md +590 -0
- package/template/.agent/skill-library/stack/auth/firebase-auth/SKILL.md +734 -0
- package/template/.agent/skill-library/stack/cms/payload-cms/SKILL.md +573 -0
- package/template/.agent/skill-library/stack/cms/shopify/SKILL.md +1193 -0
- package/template/.agent/skill-library/stack/cms/wordpress/SKILL.md +1104 -0
- package/template/.agent/skill-library/stack/css/sass-scss/SKILL.md +1121 -0
- package/template/.agent/skill-library/stack/css/tailwind-css-patterns/SKILL.md +863 -0
- package/template/.agent/skill-library/stack/css/tailwind-design-system/SKILL.md +490 -0
- package/template/.agent/skill-library/stack/css/vanilla-css/SKILL.md +1078 -0
- package/template/.agent/skill-library/stack/databases/clickhouse/SKILL.md +311 -0
- package/template/.agent/skill-library/stack/databases/influxdb/SKILL.md +280 -0
- package/template/.agent/skill-library/stack/databases/lancedb/SKILL.md +415 -0
- package/template/.agent/skill-library/stack/databases/mongodb/SKILL.md +1169 -0
- package/template/.agent/skill-library/stack/databases/neo4j/SKILL.md +839 -0
- package/template/.agent/skill-library/stack/databases/pgvector/SKILL.md +241 -0
- package/template/.agent/skill-library/stack/databases/pinecone/SKILL.md +212 -0
- package/template/.agent/skill-library/stack/databases/postgresql/SKILL.md +658 -0
- package/template/.agent/skill-library/stack/databases/qdrant/SKILL.md +312 -0
- package/template/.agent/skill-library/stack/databases/redis/SKILL.md +1079 -0
- package/template/.agent/skill-library/stack/databases/spacetimedb/SKILL.md +532 -0
- package/template/.agent/skill-library/stack/databases/sqlite/SKILL.md +1132 -0
- package/template/.agent/skill-library/stack/databases/supabase/SKILL.md +640 -0
- package/template/.agent/skill-library/stack/databases/surrealdb-expert/SKILL.md +945 -0
- package/template/.agent/skill-library/stack/databases/timescaledb/SKILL.md +745 -0
- package/template/.agent/skill-library/stack/databases/weaviate/SKILL.md +218 -0
- package/template/.agent/skill-library/stack/devops/github-actions/SKILL.md +554 -0
- package/template/.agent/skill-library/stack/devops/kubernetes/SKILL.md +950 -0
- package/template/.agent/skill-library/stack/devops/nginx/SKILL.md +841 -0
- package/template/.agent/skill-library/stack/devops/terraform/SKILL.md +860 -0
- package/template/.agent/skill-library/stack/email/resend/SKILL.md +391 -0
- package/template/.agent/skill-library/stack/engines/godot/SKILL.md +488 -0
- package/template/.agent/skill-library/stack/extensions/chrome-extension/SKILL.md +375 -0
- package/template/.agent/skill-library/stack/extensions/vscode-extension/SKILL.md +453 -0
- package/template/.agent/skill-library/stack/frameworks/astro-framework/SKILL.md +162 -0
- package/template/.agent/skill-library/stack/frameworks/electron/SKILL.md +1286 -0
- package/template/.agent/skill-library/stack/frameworks/fastapi/SKILL.md +650 -0
- package/template/.agent/skill-library/stack/frameworks/hono/SKILL.md +90 -0
- package/template/.agent/skill-library/stack/frameworks/nestjs/SKILL.md +878 -0
- package/template/.agent/skill-library/stack/frameworks/nextjs/SKILL.md +635 -0
- package/template/.agent/skill-library/stack/frameworks/nuxt/SKILL.md +564 -0
- package/template/.agent/skill-library/stack/frameworks/sveltekit/SKILL.md +614 -0
- package/template/.agent/skill-library/stack/frameworks/tauri/SKILL.md +920 -0
- package/template/.agent/skill-library/stack/gamedev/godot/SKILL.md +1032 -0
- package/template/.agent/skill-library/stack/gamedev/unity/SKILL.md +1175 -0
- package/template/.agent/skill-library/stack/hosting/aws/SKILL.md +467 -0
- package/template/.agent/skill-library/stack/hosting/cloudflare/SKILL.md +201 -0
- package/template/.agent/skill-library/stack/hosting/docker-expert/SKILL.md +409 -0
- package/template/.agent/skill-library/stack/hosting/vercel/SKILL.md +484 -0
- package/template/.agent/skill-library/stack/languages/bash-scripting/SKILL.md +773 -0
- package/template/.agent/skill-library/stack/languages/c-cpp/SKILL.md +712 -0
- package/template/.agent/skill-library/stack/languages/gdscript/SKILL.md +789 -0
- package/template/.agent/skill-library/stack/languages/go/SKILL.md +664 -0
- package/template/.agent/skill-library/stack/languages/java/SKILL.md +778 -0
- package/template/.agent/skill-library/stack/languages/kotlin/SKILL.md +665 -0
- package/template/.agent/skill-library/stack/languages/python/SKILL.md +678 -0
- package/template/.agent/skill-library/stack/languages/rust/SKILL.md +673 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/SKILL.md +141 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/advanced-generics.md +90 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/branded-types.md +57 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/builder-pattern.md +71 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/common-pitfalls.md +135 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/conditional-types.md +27 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/decorators.md +98 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/discriminated-unions.md +62 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/mapped-types.md +53 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/performance-best-practices.md +104 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/template-literal-types.md +49 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/testing-types.md +112 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/type-guards.md +70 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/type-inference.md +101 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/utility-types.md +98 -0
- package/template/.agent/skill-library/stack/languages/vanilla-javascript/SKILL.md +803 -0
- package/template/.agent/skill-library/stack/messaging/kafka/SKILL.md +235 -0
- package/template/.agent/skill-library/stack/mobile/expo-react-native/SKILL.md +665 -0
- package/template/.agent/skill-library/stack/mobile/flutter/SKILL.md +316 -0
- package/template/.agent/skill-library/stack/mobile/react-native/SKILL.md +337 -0
- package/template/.agent/skill-library/stack/monitoring/posthog/SKILL.md +396 -0
- package/template/.agent/skill-library/stack/monitoring/sentry/SKILL.md +509 -0
- package/template/.agent/skill-library/stack/observability/datadog/SKILL.md +179 -0
- package/template/.agent/skill-library/stack/observability/distributed-tracing/SKILL.md +140 -0
- package/template/.agent/skill-library/stack/observability/logging-best-practices/SKILL.md +168 -0
- package/template/.agent/skill-library/stack/observability/opentelemetry/SKILL.md +164 -0
- package/template/.agent/skill-library/stack/observability/prometheus-grafana/SKILL.md +246 -0
- package/template/.agent/skill-library/stack/observability/python-observability/SKILL.md +158 -0
- package/template/.agent/skill-library/stack/orm/drizzle-orm/SKILL.md +613 -0
- package/template/.agent/skill-library/stack/orm/prisma/SKILL.md +744 -0
- package/template/.agent/skill-library/stack/payments/lemonsqueezy/SKILL.md +393 -0
- package/template/.agent/skill-library/stack/payments/stripe-integration/SKILL.md +457 -0
- package/template/.agent/skill-library/stack/queue/bullmq/SKILL.md +385 -0
- package/template/.agent/skill-library/stack/queue/inngest/SKILL.md +438 -0
- package/template/.agent/skill-library/stack/realtime/socketio/SKILL.md +595 -0
- package/template/.agent/skill-library/stack/search/elasticsearch/SKILL.md +248 -0
- package/template/.agent/skill-library/stack/search/meilisearch/SKILL.md +385 -0
- package/template/.agent/skill-library/stack/security/crypto-patterns/SKILL.md +437 -0
- package/template/.agent/skill-library/stack/security/csp-cors-headers/SKILL.md +588 -0
- package/template/.agent/skill-library/stack/security/dependency-auditing/SKILL.md +560 -0
- package/template/.agent/skill-library/stack/security/input-sanitization/SKILL.md +430 -0
- package/template/.agent/skill-library/stack/security/owasp-web-security/SKILL.md +421 -0
- package/template/.agent/skill-library/stack/state/tanstack-query/SKILL.md +637 -0
- package/template/.agent/skill-library/stack/state/zustand/SKILL.md +483 -0
- package/template/.agent/skill-library/stack/storage/aws-s3/SKILL.md +415 -0
- package/template/.agent/skill-library/stack/testing/playwright/SKILL.md +641 -0
- package/template/.agent/skill-library/stack/testing/storybook/SKILL.md +923 -0
- package/template/.agent/skill-library/stack/testing/testing-library/SKILL.md +872 -0
- package/template/.agent/skill-library/stack/testing/vitest/SKILL.md +714 -0
- package/template/.agent/skill-library/stack/ui/react-best-practices/SKILL.md +877 -0
- package/template/.agent/skill-library/stack/ui/react-composition-patterns/SKILL.md +1107 -0
- package/template/.agent/skill-library/stack/ui/react-flow/SKILL.md +425 -0
- package/template/.agent/skill-library/stack/ui/shadcn-ui/SKILL.md +703 -0
- package/template/.agent/skill-library/surface/api/api-caching/SKILL.md +458 -0
- package/template/.agent/skill-library/surface/api/api-documentation-openapi/SKILL.md +697 -0
- package/template/.agent/skill-library/surface/api/api-error-handling/SKILL.md +478 -0
- package/template/.agent/skill-library/surface/api/api-security-checklist/SKILL.md +147 -0
- package/template/.agent/skill-library/surface/api/api-versioning/SKILL.md +420 -0
- package/template/.agent/skill-library/surface/api/email-best-practices/SKILL.md +59 -0
- package/template/.agent/skill-library/surface/api/rate-limiting-abuse-protection/SKILL.md +147 -0
- package/template/.agent/skill-library/surface/api/rest-api-design/SKILL.md +478 -0
- package/template/.agent/skill-library/surface/api/webhook-design/SKILL.md +752 -0
- package/template/.agent/skill-library/surface/cli/cli-configuration-management/SKILL.md +445 -0
- package/template/.agent/skill-library/surface/cli/cli-error-diagnostics/SKILL.md +515 -0
- package/template/.agent/skill-library/surface/cli/cli-shell-integration/SKILL.md +479 -0
- package/template/.agent/skill-library/surface/cli/cli-ux-design/SKILL.md +477 -0
- package/template/.agent/skill-library/surface/desktop/desktop-app-distribution/SKILL.md +416 -0
- package/template/.agent/skill-library/surface/desktop/desktop-security-sandboxing/SKILL.md +407 -0
- package/template/.agent/skill-library/surface/desktop/desktop-ux-conventions/SKILL.md +361 -0
- package/template/.agent/skill-library/surface/desktop/native-os-integration/SKILL.md +563 -0
- package/template/.agent/skill-library/surface/extension/browser-extension-patterns/SKILL.md +482 -0
- package/template/.agent/skill-library/surface/extension/plugin-architecture-design/SKILL.md +632 -0
- package/template/.agent/skill-library/surface/extension/vscode-extension-development/SKILL.md +728 -0
- package/template/.agent/skill-library/surface/mobile/app-store-submission/SKILL.md +304 -0
- package/template/.agent/skill-library/surface/mobile/mobile-offline-sync/SKILL.md +443 -0
- package/template/.agent/skill-library/surface/mobile/mobile-responsive-patterns/SKILL.md +432 -0
- package/template/.agent/skill-library/surface/mobile/push-notifications/SKILL.md +495 -0
- package/template/.agent/skill-library/surface/web/accessibility-compliance/SKILL.md +827 -0
- package/template/.agent/skill-library/surface/web/ai-seo/SKILL.md +398 -0
- package/template/.agent/skill-library/surface/web/ai-seo/references/content-patterns.md +285 -0
- package/template/.agent/skill-library/surface/web/ai-seo/references/platform-ranking-factors.md +152 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/SKILL.md +309 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/references/event-library.md +260 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/references/ga4-implementation.md +300 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/references/gtm-implementation.md +390 -0
- package/template/.agent/skill-library/surface/web/authentication-ui-flows/SKILL.md +530 -0
- package/template/.agent/skill-library/surface/web/dark-mode-theming/SKILL.md +516 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/SKILL.md +105 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/charts.csv +26 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/colors.csv +97 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/landing.csv +31 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/styles.csv +59 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/typography.csv +58 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/ux-guidelines.csv +100 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/scripts/core.py +258 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/scripts/design_system.py +1067 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/scripts/search.py +106 -0
- package/template/.agent/skill-library/surface/web/form-handling-validation/SKILL.md +675 -0
- package/template/.agent/skill-library/surface/web/frontend-design/SKILL.md +1393 -0
- package/template/.agent/skill-library/surface/web/frontend-design/templates/cppn-hero.tsx +299 -0
- package/template/.agent/skill-library/surface/web/frontend-design/templates/wave-hero.tsx +875 -0
- package/template/.agent/skill-library/surface/web/frontend-verification/SKILL.md +111 -0
- package/template/.agent/skill-library/surface/web/frontend-verification/scripts/ux_audit.py +739 -0
- package/template/.agent/skill-library/surface/web/i18n-localization/SKILL.md +154 -0
- package/template/.agent/skill-library/surface/web/offline-first-pwa/SKILL.md +657 -0
- package/template/.agent/skill-library/surface/web/page-cro/SKILL.md +182 -0
- package/template/.agent/skill-library/surface/web/page-cro/references/experiments.md +248 -0
- package/template/.agent/skill-library/surface/web/programmatic-seo/SKILL.md +238 -0
- package/template/.agent/skill-library/surface/web/programmatic-seo/references/playbooks.md +308 -0
- package/template/.agent/skill-library/surface/web/schema-markup/SKILL.md +179 -0
- package/template/.agent/skill-library/surface/web/schema-markup/references/schema-examples.md +398 -0
- package/template/.agent/skill-library/surface/web/seo-audit/SKILL.md +394 -0
- package/template/.agent/skill-library/surface/web/seo-audit/references/ai-writing-detection.md +200 -0
- package/template/.agent/skill-library/surface/web/web-performance-optimization/SKILL.md +646 -0
- package/template/.agent/skill-library/surface/web/web-scraping/SKILL.md +58 -0
- package/template/.agent/skills/accessibility/SKILL.md +522 -0
- package/template/.agent/skills/accessibility/references/WCAG.md +162 -0
- package/template/.agent/skills/adversarial-review/SKILL.md +90 -0
- package/template/.agent/skills/antigravity-workflows/SKILL.md +81 -0
- package/template/.agent/skills/antigravity-workflows/resources/implementation-playbook.md +36 -0
- package/template/.agent/skills/api-design-principles/SKILL.md +37 -0
- package/template/.agent/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/template/.agent/skills/api-design-principles/assets/rest-api-template.py +182 -0
- package/template/.agent/skills/api-design-principles/references/graphql-schema-design.md +583 -0
- package/template/.agent/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/template/.agent/skills/api-design-principles/resources/implementation-playbook.md +513 -0
- package/template/.agent/skills/api-versioning/SKILL.md +420 -0
- package/template/.agent/skills/architecture-mapping/SKILL.md +219 -0
- package/template/.agent/skills/bootstrap-agents/SKILL.md +259 -0
- package/template/.agent/skills/brainstorming/SKILL.md +236 -0
- package/template/.agent/skills/brand-guidelines/SKILL.md +44 -0
- package/template/.agent/skills/clean-code/SKILL.md +94 -0
- package/template/.agent/skills/code-review-pro/SKILL.md +152 -0
- package/template/.agent/skills/concise-planning/SKILL.md +68 -0
- package/template/.agent/skills/cross-layer-consistency/SKILL.md +117 -0
- package/template/.agent/skills/database-schema-design/SKILL.md +429 -0
- package/template/.agent/skills/deployment-procedures/SKILL.md +241 -0
- package/template/.agent/skills/design-anti-cliche/SKILL.md +159 -0
- package/template/.agent/skills/design-direction/SKILL.md +45 -0
- package/template/.agent/skills/error-handling-patterns/SKILL.md +721 -0
- package/template/.agent/skills/find-skills/SKILL.md +145 -0
- package/template/.agent/skills/git-advanced/SKILL.md +972 -0
- package/template/.agent/skills/git-workflow/SKILL.md +420 -0
- package/template/.agent/skills/idea-extraction/SKILL.md +271 -0
- package/template/.agent/skills/logging-best-practices/SKILL.md +851 -0
- package/template/.agent/skills/migration-management/SKILL.md +384 -0
- package/template/.agent/skills/minimalist-surgical-development/SKILL.md +69 -0
- package/template/.agent/skills/parallel-agents/SKILL.md +165 -0
- package/template/.agent/skills/parallel-debugging/SKILL.md +135 -0
- package/template/.agent/skills/parallel-feature-development/SKILL.md +166 -0
- package/template/.agent/skills/performance-budgeting/SKILL.md +144 -0
- package/template/.agent/skills/pipeline-rubrics/SKILL.md +51 -0
- package/template/.agent/skills/pipeline-rubrics/references/architecture-rubric.md +19 -0
- package/template/.agent/skills/pipeline-rubrics/references/be-rubric.md +21 -0
- package/template/.agent/skills/pipeline-rubrics/references/fe-rubric.md +20 -0
- package/template/.agent/skills/pipeline-rubrics/references/ia-rubric.md +19 -0
- package/template/.agent/skills/pipeline-rubrics/references/scoring.md +28 -0
- package/template/.agent/skills/pipeline-rubrics/references/vision-rubric.md +11 -0
- package/template/.agent/skills/prd-templates/SKILL.md +88 -0
- package/template/.agent/skills/prd-templates/references/architecture-design-template.md +88 -0
- package/template/.agent/skills/prd-templates/references/be-spec-template.md +101 -0
- package/template/.agent/skills/prd-templates/references/data-placement-template.md +74 -0
- package/template/.agent/skills/prd-templates/references/decomposition-templates.md +211 -0
- package/template/.agent/skills/prd-templates/references/design-system-decisions.md +198 -0
- package/template/.agent/skills/prd-templates/references/engineering-standards-template.md +124 -0
- package/template/.agent/skills/prd-templates/references/fe-classification-procedures.md +47 -0
- package/template/.agent/skills/prd-templates/references/fe-spec-template.md +84 -0
- package/template/.agent/skills/prd-templates/references/infrastructure-report-template.md +71 -0
- package/template/.agent/skills/prd-templates/references/operational-templates.md +116 -0
- package/template/.agent/skills/prd-templates/references/placeholder-guard-template.md +21 -0
- package/template/.agent/skills/prd-templates/references/surface-model.md +61 -0
- package/template/.agent/skills/prd-templates/references/vision-template.md +66 -0
- package/template/.agent/skills/prompt-engineer/README.md +659 -0
- package/template/.agent/skills/prompt-engineer/SKILL.md +249 -0
- package/template/.agent/skills/regex-patterns/SKILL.md +751 -0
- package/template/.agent/skills/resolve-ambiguity/SKILL.md +278 -0
- package/template/.agent/skills/rest-api-design/SKILL.md +478 -0
- package/template/.agent/skills/security-scanning-security-hardening/SKILL.md +231 -0
- package/template/.agent/skills/session-continuity/SKILL.md +730 -0
- package/template/.agent/skills/session-continuity/protocols/01-session-resumption.md +38 -0
- package/template/.agent/skills/session-continuity/protocols/02-progress-generation.md +85 -0
- package/template/.agent/skills/session-continuity/protocols/03-progress-update.md +70 -0
- package/template/.agent/skills/session-continuity/protocols/04-pattern-extraction.md +60 -0
- package/template/.agent/skills/session-continuity/protocols/05-session-close.md +37 -0
- package/template/.agent/skills/session-continuity/protocols/06-decision-analysis.md +84 -0
- package/template/.agent/skills/session-continuity/protocols/07-spec-pipeline-generation.md +48 -0
- package/template/.agent/skills/session-continuity/protocols/08-spec-pipeline-update.md +43 -0
- package/template/.agent/skills/session-continuity/protocols/09-parallel-claim.md +122 -0
- package/template/.agent/skills/session-continuity/protocols/10-placeholder-verification-gate.md +104 -0
- package/template/.agent/skills/session-continuity/protocols/ambiguity-gates.md +48 -0
- package/template/.agent/skills/skill-creator/LICENSE.txt +202 -0
- package/template/.agent/skills/skill-creator/README.md +270 -0
- package/template/.agent/skills/skill-creator/SKILL.md +590 -0
- package/template/.agent/skills/skill-creator/references/output-patterns.md +82 -0
- package/template/.agent/skills/skill-creator/references/workflows.md +28 -0
- package/template/.agent/skills/skill-creator/scripts/init_skill.py +303 -0
- package/template/.agent/skills/skill-creator/scripts/package_skill.py +110 -0
- package/template/.agent/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/template/.agent/skills/spec-writing/SKILL.md +110 -0
- package/template/.agent/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/template/.agent/skills/systematic-debugging/SKILL.md +297 -0
- package/template/.agent/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/template/.agent/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/template/.agent/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/template/.agent/skills/systematic-debugging/find-polluter.sh +63 -0
- package/template/.agent/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/template/.agent/skills/systematic-debugging/test-academic.md +14 -0
- package/template/.agent/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/template/.agent/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/template/.agent/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/template/.agent/skills/tdd-workflow/SKILL.md +409 -0
- package/template/.agent/skills/tech-stack-catalog/SKILL.md +49 -0
- package/template/.agent/skills/tech-stack-catalog/references/constraint-questions.md +21 -0
- package/template/.agent/skills/tech-stack-catalog/references/dev-tooling-decisions.md +37 -0
- package/template/.agent/skills/tech-stack-catalog/references/surface-decision-tables.md +69 -0
- package/template/.agent/skills/technical-writer/SKILL.md +242 -0
- package/template/.agent/skills/testing-strategist/SKILL.md +932 -0
- package/template/.agent/skills/verification-before-completion/SKILL.md +145 -0
- package/template/.agent/skills/workflow-automation/SKILL.md +73 -0
- package/template/.agent/workflows/audit-ambiguity-execute.md +165 -0
- package/template/.agent/workflows/audit-ambiguity-rubrics.md +83 -0
- package/template/.agent/workflows/audit-ambiguity.md +64 -0
- package/template/.agent/workflows/bootstrap-agents-fill.md +201 -0
- package/template/.agent/workflows/bootstrap-agents-provision.md +197 -0
- package/template/.agent/workflows/bootstrap-agents.md +66 -0
- package/template/.agent/workflows/create-prd-architecture.md +119 -0
- package/template/.agent/workflows/create-prd-compile.md +138 -0
- package/template/.agent/workflows/create-prd-design-system.md +135 -0
- package/template/.agent/workflows/create-prd-security.md +113 -0
- package/template/.agent/workflows/create-prd-stack.md +91 -0
- package/template/.agent/workflows/create-prd.md +168 -0
- package/template/.agent/workflows/decompose-architecture-structure.md +82 -0
- package/template/.agent/workflows/decompose-architecture-validate.md +119 -0
- package/template/.agent/workflows/decompose-architecture.md +111 -0
- package/template/.agent/workflows/evolve-contract.md +98 -0
- package/template/.agent/workflows/evolve-feature-cascade.md +140 -0
- package/template/.agent/workflows/evolve-feature-classify.md +116 -0
- package/template/.agent/workflows/evolve-feature.md +56 -0
- package/template/.agent/workflows/ideate-discover.md +144 -0
- package/template/.agent/workflows/ideate-extract.md +129 -0
- package/template/.agent/workflows/ideate-validate.md +117 -0
- package/template/.agent/workflows/ideate.md +113 -0
- package/template/.agent/workflows/implement-slice-setup.md +113 -0
- package/template/.agent/workflows/implement-slice-tdd.md +198 -0
- package/template/.agent/workflows/implement-slice.md +50 -0
- package/template/.agent/workflows/plan-phase.md +202 -0
- package/template/.agent/workflows/propagate-decision-apply.md +135 -0
- package/template/.agent/workflows/propagate-decision-scan.md +147 -0
- package/template/.agent/workflows/propagate-decision.md +56 -0
- package/template/.agent/workflows/remediate-pipeline-assess.md +138 -0
- package/template/.agent/workflows/remediate-pipeline-execute.md +135 -0
- package/template/.agent/workflows/remediate-pipeline.md +55 -0
- package/template/.agent/workflows/resolve-ambiguity.md +82 -0
- package/template/.agent/workflows/sync-kit.md +209 -0
- package/template/.agent/workflows/update-architecture-map.md +74 -0
- package/template/.agent/workflows/validate-phase.md +219 -0
- package/template/.agent/workflows/verify-infrastructure.md +207 -0
- package/template/.agent/workflows/write-architecture-spec-deepen.md +139 -0
- package/template/.agent/workflows/write-architecture-spec-design.md +202 -0
- package/template/.agent/workflows/write-architecture-spec.md +63 -0
- package/template/.agent/workflows/write-be-spec-classify.md +165 -0
- package/template/.agent/workflows/write-be-spec-write.md +98 -0
- package/template/.agent/workflows/write-be-spec.md +76 -0
- package/template/.agent/workflows/write-fe-spec-classify.md +170 -0
- package/template/.agent/workflows/write-fe-spec-write.md +94 -0
- package/template/.agent/workflows/write-fe-spec.md +71 -0
- package/template/AGENTS.md +176 -0
- package/template/GEMINI.md +177 -0
- package/template/docs/README.md +187 -0
- package/template/docs/audits/.gitkeep +0 -0
- package/template/docs/audits/README.md +10 -0
- package/template/docs/plans/.gitkeep +0 -0
- package/template/docs/plans/README.md +21 -0
- package/template/docs/plans/be/.gitkeep +0 -0
- package/template/docs/plans/be/README.md +11 -0
- package/template/docs/plans/fe/.gitkeep +0 -0
- package/template/docs/plans/fe/README.md +11 -0
- package/template/docs/plans/ia/.gitkeep +0 -0
- package/template/docs/plans/ia/README.md +17 -0
- package/template/docs/plans/ia/deep-dives/.gitkeep +0 -0
- package/template/docs/plans/ia/deep-dives/README.md +5 -0
- package/template/docs/plans/phases/.gitkeep +0 -0
- package/template/docs/plans/phases/README.md +11 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-composition-patterns
|
|
3
|
+
description: "Comprehensive React composition and component design guide covering compound components, render props, custom hooks, explicit variants, lifting state, slot-based composition, polymorphic components, controlled vs uncontrolled patterns, provider pattern, state machines, form patterns, modal management, and list/item patterns. Use when designing component APIs, building reusable UI libraries, or structuring complex component hierarchies."
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# React Composition Patterns
|
|
8
|
+
|
|
9
|
+
## 1. Philosophy
|
|
10
|
+
|
|
11
|
+
Component composition is about **building complex UIs from simple, reusable pieces without tight coupling**. The goal is an API that is obvious to consumers, flexible enough for real-world use cases, and impossible to misuse.
|
|
12
|
+
|
|
13
|
+
**Key principles**:
|
|
14
|
+
- Composition over configuration. Prefer children and slots over giant prop objects.
|
|
15
|
+
- Explicit over implicit. A component with 5 clear variants beats one with 15 boolean props.
|
|
16
|
+
- Inversion of control. Let the consumer decide layout and behavior, not the component.
|
|
17
|
+
- Single responsibility. Each component does one thing. Compose them for complex behavior.
|
|
18
|
+
- Type safety. Component APIs must be fully typed so misuse is a compile error, not a runtime bug.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. Compound Components
|
|
23
|
+
|
|
24
|
+
Compound components share implicit state through React Context. The parent manages the state; children read from it. This gives consumers full control over layout and ordering while keeping behavior centralized.
|
|
25
|
+
|
|
26
|
+
### Basic Pattern
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { createContext, useContext, useState, type ReactNode } from "react";
|
|
30
|
+
|
|
31
|
+
// 1. Create context for shared state
|
|
32
|
+
interface AccordionContextValue {
|
|
33
|
+
openItems: Set<string>;
|
|
34
|
+
toggle: (id: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null);
|
|
38
|
+
|
|
39
|
+
function useAccordion() {
|
|
40
|
+
const context = useContext(AccordionContext);
|
|
41
|
+
if (!context) {
|
|
42
|
+
throw new Error("Accordion.Item must be used within an Accordion");
|
|
43
|
+
}
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Parent component manages state
|
|
48
|
+
function Accordion({
|
|
49
|
+
children,
|
|
50
|
+
multiple = false,
|
|
51
|
+
}: {
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
multiple?: boolean;
|
|
54
|
+
}) {
|
|
55
|
+
const [openItems, setOpenItems] = useState<Set<string>>(new Set());
|
|
56
|
+
|
|
57
|
+
function toggle(id: string) {
|
|
58
|
+
setOpenItems((prev) => {
|
|
59
|
+
const next = new Set(multiple ? prev : []);
|
|
60
|
+
if (prev.has(id)) {
|
|
61
|
+
next.delete(id);
|
|
62
|
+
} else {
|
|
63
|
+
next.add(id);
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<AccordionContext.Provider value={{ openItems, toggle }}>
|
|
71
|
+
<div role="region">{children}</div>
|
|
72
|
+
</AccordionContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Child components read shared state
|
|
77
|
+
function AccordionItem({
|
|
78
|
+
id,
|
|
79
|
+
title,
|
|
80
|
+
children,
|
|
81
|
+
}: {
|
|
82
|
+
id: string;
|
|
83
|
+
title: string;
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
}) {
|
|
86
|
+
const { openItems, toggle } = useAccordion();
|
|
87
|
+
const isOpen = openItems.has(id);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => toggle(id)}
|
|
93
|
+
aria-expanded={isOpen}
|
|
94
|
+
aria-controls={`accordion-panel-${id}`}
|
|
95
|
+
>
|
|
96
|
+
{title}
|
|
97
|
+
</button>
|
|
98
|
+
{isOpen && (
|
|
99
|
+
<div id={`accordion-panel-${id}`} role="region">
|
|
100
|
+
{children}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 4. Attach as static properties
|
|
108
|
+
Accordion.Item = AccordionItem;
|
|
109
|
+
|
|
110
|
+
// 5. Usage -- consumer controls layout and ordering
|
|
111
|
+
<Accordion multiple>
|
|
112
|
+
<Accordion.Item id="faq-1" title="What is this?">
|
|
113
|
+
<p>A compound component example.</p>
|
|
114
|
+
</Accordion.Item>
|
|
115
|
+
<Accordion.Item id="faq-2" title="Why use this pattern?">
|
|
116
|
+
<p>Flexible layout with shared state.</p>
|
|
117
|
+
</Accordion.Item>
|
|
118
|
+
</Accordion>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### When to Use Compound Components
|
|
122
|
+
|
|
123
|
+
| Use When | Do Not Use When |
|
|
124
|
+
|----------|-----------------|
|
|
125
|
+
| Multiple sub-components share state | A single component with a few props suffices |
|
|
126
|
+
| Consumer needs layout control | The layout is always the same |
|
|
127
|
+
| The parent-child relationship is semantic | Components are unrelated |
|
|
128
|
+
| You want to avoid prop drilling | A simple callback prop would work |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 3. Render Props and Children-as-Function
|
|
133
|
+
|
|
134
|
+
Render props pass rendering control to the consumer via a function. Use this when the component owns behavior but the consumer owns the visual output.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
interface MouseTrackerProps {
|
|
138
|
+
children: (position: { x: number; y: number }) => ReactNode;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function MouseTracker({ children }: MouseTrackerProps) {
|
|
142
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
|
|
146
|
+
{children(position)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Usage -- consumer decides what to render
|
|
152
|
+
<MouseTracker>
|
|
153
|
+
{({ x, y }) => (
|
|
154
|
+
<div>
|
|
155
|
+
Mouse is at ({x}, {y})
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</MouseTracker>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Modern Alternative: Custom Hooks
|
|
162
|
+
|
|
163
|
+
Render props are largely replaced by custom hooks. Prefer hooks unless you need to inject behavior into the JSX tree itself.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
// Prefer this
|
|
167
|
+
function useMousePosition() {
|
|
168
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
function handleMove(e: MouseEvent) {
|
|
172
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
173
|
+
}
|
|
174
|
+
window.addEventListener("mousemove", handleMove);
|
|
175
|
+
return () => window.removeEventListener("mousemove", handleMove);
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
return position;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Usage
|
|
182
|
+
function Component() {
|
|
183
|
+
const { x, y } = useMousePosition();
|
|
184
|
+
return <div>Mouse at ({x}, {y})</div>;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 4. Custom Hooks for Logic Extraction
|
|
191
|
+
|
|
192
|
+
Custom hooks extract reusable stateful logic from components. The component handles rendering; the hook handles behavior.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
// hooks/use-async.ts
|
|
196
|
+
import { useState, useCallback } from "react";
|
|
197
|
+
|
|
198
|
+
interface AsyncState<T> {
|
|
199
|
+
data: T | null;
|
|
200
|
+
error: Error | null;
|
|
201
|
+
loading: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function useAsync<T>(asyncFunction: () => Promise<T>) {
|
|
205
|
+
const [state, setState] = useState<AsyncState<T>>({
|
|
206
|
+
data: null,
|
|
207
|
+
error: null,
|
|
208
|
+
loading: false,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const execute = useCallback(async () => {
|
|
212
|
+
setState({ data: null, error: null, loading: true });
|
|
213
|
+
try {
|
|
214
|
+
const data = await asyncFunction();
|
|
215
|
+
setState({ data, error: null, loading: false });
|
|
216
|
+
return data;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
219
|
+
setState({ data: null, error: err, loading: false });
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}, [asyncFunction]);
|
|
223
|
+
|
|
224
|
+
return { ...state, execute };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Usage
|
|
228
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
229
|
+
const fetchUser = useCallback(() => api.getUser(userId), [userId]);
|
|
230
|
+
const { data: user, loading, error, execute } = useAsync(fetchUser);
|
|
231
|
+
|
|
232
|
+
useEffect(() => { execute(); }, [execute]);
|
|
233
|
+
|
|
234
|
+
if (loading) return <Skeleton />;
|
|
235
|
+
if (error) return <ErrorDisplay error={error} onRetry={execute} />;
|
|
236
|
+
if (!user) return null;
|
|
237
|
+
return <ProfileCard user={user} />;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Hook Composition
|
|
242
|
+
|
|
243
|
+
Hooks compose naturally. Build complex behavior from simple hooks.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
247
|
+
const [debounced, setDebounced] = useState(value);
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
251
|
+
return () => clearTimeout(timer);
|
|
252
|
+
}, [value, delay]);
|
|
253
|
+
|
|
254
|
+
return debounced;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function useSearch(query: string) {
|
|
258
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
259
|
+
const fetchResults = useCallback(
|
|
260
|
+
() => api.search(debouncedQuery),
|
|
261
|
+
[debouncedQuery]
|
|
262
|
+
);
|
|
263
|
+
return useAsync(fetchResults);
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 5. Explicit Variants Over Conditional Rendering
|
|
270
|
+
|
|
271
|
+
When a component has multiple visual modes, use explicit variant components instead of piling boolean props onto one component.
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
// BAD: Boolean props create an exponential combination space
|
|
275
|
+
<Button
|
|
276
|
+
primary
|
|
277
|
+
outline
|
|
278
|
+
small
|
|
279
|
+
loading
|
|
280
|
+
iconOnly
|
|
281
|
+
disabled
|
|
282
|
+
/>
|
|
283
|
+
// How many combinations is this? 2^6 = 64. Are they all valid?
|
|
284
|
+
|
|
285
|
+
// GOOD: Explicit variants with a discriminated union
|
|
286
|
+
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "destructive";
|
|
287
|
+
type ButtonSize = "sm" | "md" | "lg" | "icon";
|
|
288
|
+
|
|
289
|
+
interface ButtonProps {
|
|
290
|
+
variant?: ButtonVariant;
|
|
291
|
+
size?: ButtonSize;
|
|
292
|
+
loading?: boolean;
|
|
293
|
+
disabled?: boolean;
|
|
294
|
+
children: ReactNode;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function Button({
|
|
298
|
+
variant = "primary",
|
|
299
|
+
size = "md",
|
|
300
|
+
loading = false,
|
|
301
|
+
disabled = false,
|
|
302
|
+
children,
|
|
303
|
+
}: ButtonProps) {
|
|
304
|
+
return (
|
|
305
|
+
<button
|
|
306
|
+
className={cn(variantStyles[variant], sizeStyles[size])}
|
|
307
|
+
disabled={disabled || loading}
|
|
308
|
+
>
|
|
309
|
+
{loading && <Spinner size={size} />}
|
|
310
|
+
{children}
|
|
311
|
+
</button>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Separate Components for Separate Concerns
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
// BAD: One component with conditional rendering for every case
|
|
320
|
+
function Card({ type, ...props }: { type: "user" | "product" | "article" }) {
|
|
321
|
+
if (type === "user") return <div>...</div>;
|
|
322
|
+
if (type === "product") return <div>...</div>;
|
|
323
|
+
if (type === "article") return <div>...</div>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// GOOD: Separate components sharing a common base
|
|
327
|
+
function CardBase({ children, className }: { children: ReactNode; className?: string }) {
|
|
328
|
+
return <div className={cn("rounded-lg border p-4", className)}>{children}</div>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function UserCard({ user }: { user: User }) {
|
|
332
|
+
return (
|
|
333
|
+
<CardBase>
|
|
334
|
+
<Avatar src={user.avatar} />
|
|
335
|
+
<h3>{user.name}</h3>
|
|
336
|
+
</CardBase>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function ProductCard({ product }: { product: Product }) {
|
|
341
|
+
return (
|
|
342
|
+
<CardBase>
|
|
343
|
+
<img src={product.image} alt={product.name} />
|
|
344
|
+
<h3>{product.name}</h3>
|
|
345
|
+
<p>{product.price}</p>
|
|
346
|
+
</CardBase>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 6. Lifting State Up
|
|
354
|
+
|
|
355
|
+
When two sibling components need to share state, lift the state to their closest common parent.
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
// Problem: FilterBar and ResultsList both need the filter state
|
|
359
|
+
// Solution: Lift state to the parent
|
|
360
|
+
|
|
361
|
+
function SearchPage() {
|
|
362
|
+
const [filters, setFilters] = useState<Filters>({
|
|
363
|
+
category: "all",
|
|
364
|
+
sortBy: "relevance",
|
|
365
|
+
query: "",
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className="grid grid-cols-[250px_1fr]">
|
|
370
|
+
<FilterBar filters={filters} onFiltersChange={setFilters} />
|
|
371
|
+
<ResultsList filters={filters} />
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// FilterBar only knows about its own UI concerns
|
|
377
|
+
function FilterBar({
|
|
378
|
+
filters,
|
|
379
|
+
onFiltersChange,
|
|
380
|
+
}: {
|
|
381
|
+
filters: Filters;
|
|
382
|
+
onFiltersChange: (filters: Filters) => void;
|
|
383
|
+
}) {
|
|
384
|
+
return (
|
|
385
|
+
<aside>
|
|
386
|
+
<select
|
|
387
|
+
value={filters.category}
|
|
388
|
+
onChange={(e) =>
|
|
389
|
+
onFiltersChange({ ...filters, category: e.target.value })
|
|
390
|
+
}
|
|
391
|
+
>
|
|
392
|
+
<option value="all">All</option>
|
|
393
|
+
<option value="books">Books</option>
|
|
394
|
+
</select>
|
|
395
|
+
</aside>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ResultsList only knows about displaying results
|
|
400
|
+
function ResultsList({ filters }: { filters: Filters }) {
|
|
401
|
+
const results = useFilteredResults(filters);
|
|
402
|
+
return (
|
|
403
|
+
<ul>
|
|
404
|
+
{results.map((r) => (
|
|
405
|
+
<li key={r.id}>{r.title}</li>
|
|
406
|
+
))}
|
|
407
|
+
</ul>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 7. Slot-Based Composition
|
|
415
|
+
|
|
416
|
+
Slots let consumers inject content into specific positions within a component. Use named props for slots instead of relying on children ordering.
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
interface PageLayoutProps {
|
|
420
|
+
header: ReactNode;
|
|
421
|
+
sidebar: ReactNode;
|
|
422
|
+
children: ReactNode;
|
|
423
|
+
footer?: ReactNode;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function PageLayout({ header, sidebar, children, footer }: PageLayoutProps) {
|
|
427
|
+
return (
|
|
428
|
+
<div className="min-h-screen grid grid-rows-[auto_1fr_auto]">
|
|
429
|
+
<header>{header}</header>
|
|
430
|
+
<div className="grid grid-cols-[250px_1fr]">
|
|
431
|
+
<aside>{sidebar}</aside>
|
|
432
|
+
<main>{children}</main>
|
|
433
|
+
</div>
|
|
434
|
+
{footer && <footer>{footer}</footer>}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Usage -- consumer fills each slot
|
|
440
|
+
<PageLayout
|
|
441
|
+
header={<NavBar />}
|
|
442
|
+
sidebar={<SideMenu />}
|
|
443
|
+
footer={<FooterLinks />}
|
|
444
|
+
>
|
|
445
|
+
<DashboardContent />
|
|
446
|
+
</PageLayout>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Slot Pattern with Default Content
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
interface DialogProps {
|
|
453
|
+
title: ReactNode;
|
|
454
|
+
description?: ReactNode;
|
|
455
|
+
actions?: ReactNode;
|
|
456
|
+
children: ReactNode;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function Dialog({ title, description, actions, children }: DialogProps) {
|
|
460
|
+
return (
|
|
461
|
+
<div role="dialog" aria-labelledby="dialog-title">
|
|
462
|
+
<div id="dialog-title">{title}</div>
|
|
463
|
+
{description && <div>{description}</div>}
|
|
464
|
+
<div>{children}</div>
|
|
465
|
+
<div className="flex justify-end gap-2">
|
|
466
|
+
{actions ?? <button>Close</button>}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## 8. Polymorphic Components (as Prop)
|
|
476
|
+
|
|
477
|
+
Polymorphic components let consumers change the rendered HTML element while preserving the component's styling and behavior.
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
import { type ElementType, type ComponentPropsWithoutRef } from "react";
|
|
481
|
+
|
|
482
|
+
type TextProps<T extends ElementType = "span"> = {
|
|
483
|
+
as?: T;
|
|
484
|
+
size?: "sm" | "md" | "lg";
|
|
485
|
+
weight?: "normal" | "medium" | "bold";
|
|
486
|
+
children: ReactNode;
|
|
487
|
+
} & Omit<ComponentPropsWithoutRef<T>, "as" | "size" | "weight" | "children">;
|
|
488
|
+
|
|
489
|
+
function Text<T extends ElementType = "span">({
|
|
490
|
+
as,
|
|
491
|
+
size = "md",
|
|
492
|
+
weight = "normal",
|
|
493
|
+
children,
|
|
494
|
+
className,
|
|
495
|
+
...props
|
|
496
|
+
}: TextProps<T>) {
|
|
497
|
+
const Component = as || "span";
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<Component
|
|
501
|
+
className={cn(
|
|
502
|
+
sizeMap[size],
|
|
503
|
+
weightMap[weight],
|
|
504
|
+
className
|
|
505
|
+
)}
|
|
506
|
+
{...props}
|
|
507
|
+
>
|
|
508
|
+
{children}
|
|
509
|
+
</Component>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Usage -- same styling, different elements
|
|
514
|
+
<Text as="h1" size="lg" weight="bold">Page Title</Text>
|
|
515
|
+
<Text as="p" size="md">Body paragraph</Text>
|
|
516
|
+
<Text as="label" size="sm" htmlFor="email">Email</Text>
|
|
517
|
+
<Text as="a" href="/about" size="sm">About</Text>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## 9. Controlled vs Uncontrolled Components
|
|
523
|
+
|
|
524
|
+
A component is **controlled** when the parent owns its state. It is **uncontrolled** when it manages its own state internally. Good APIs support both.
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
interface ToggleProps {
|
|
528
|
+
// Controlled mode
|
|
529
|
+
checked?: boolean;
|
|
530
|
+
onChange?: (checked: boolean) => void;
|
|
531
|
+
// Uncontrolled mode
|
|
532
|
+
defaultChecked?: boolean;
|
|
533
|
+
// Shared
|
|
534
|
+
label: string;
|
|
535
|
+
disabled?: boolean;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function Toggle({
|
|
539
|
+
checked: controlledChecked,
|
|
540
|
+
onChange,
|
|
541
|
+
defaultChecked = false,
|
|
542
|
+
label,
|
|
543
|
+
disabled = false,
|
|
544
|
+
}: ToggleProps) {
|
|
545
|
+
// Internal state for uncontrolled mode
|
|
546
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
547
|
+
|
|
548
|
+
// Determine which mode we are in
|
|
549
|
+
const isControlled = controlledChecked !== undefined;
|
|
550
|
+
const isChecked = isControlled ? controlledChecked : internalChecked;
|
|
551
|
+
|
|
552
|
+
function handleChange() {
|
|
553
|
+
if (disabled) return;
|
|
554
|
+
|
|
555
|
+
const nextChecked = !isChecked;
|
|
556
|
+
|
|
557
|
+
if (!isControlled) {
|
|
558
|
+
setInternalChecked(nextChecked);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
onChange?.(nextChecked);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<button
|
|
566
|
+
role="switch"
|
|
567
|
+
aria-checked={isChecked}
|
|
568
|
+
aria-label={label}
|
|
569
|
+
disabled={disabled}
|
|
570
|
+
onClick={handleChange}
|
|
571
|
+
>
|
|
572
|
+
<span className={isChecked ? "translate-x-5" : "translate-x-0"} />
|
|
573
|
+
</button>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Uncontrolled -- component manages its own state
|
|
578
|
+
<Toggle label="Dark mode" defaultChecked />
|
|
579
|
+
|
|
580
|
+
// Controlled -- parent owns the state
|
|
581
|
+
const [isDark, setIsDark] = useState(false);
|
|
582
|
+
<Toggle label="Dark mode" checked={isDark} onChange={setIsDark} />
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## 10. Provider Pattern
|
|
588
|
+
|
|
589
|
+
The Provider pattern uses Context to inject dependencies (services, configuration, theme) deep into the component tree without prop drilling.
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
// contexts/feature-flags.tsx
|
|
593
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
594
|
+
|
|
595
|
+
interface FeatureFlags {
|
|
596
|
+
newDashboard: boolean;
|
|
597
|
+
betaSearch: boolean;
|
|
598
|
+
darkMode: boolean;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const FeatureFlagContext = createContext<FeatureFlags | null>(null);
|
|
602
|
+
|
|
603
|
+
export function FeatureFlagProvider({
|
|
604
|
+
flags,
|
|
605
|
+
children,
|
|
606
|
+
}: {
|
|
607
|
+
flags: FeatureFlags;
|
|
608
|
+
children: ReactNode;
|
|
609
|
+
}) {
|
|
610
|
+
return (
|
|
611
|
+
<FeatureFlagContext.Provider value={flags}>
|
|
612
|
+
{children}
|
|
613
|
+
</FeatureFlagContext.Provider>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function useFeatureFlag(flag: keyof FeatureFlags): boolean {
|
|
618
|
+
const context = useContext(FeatureFlagContext);
|
|
619
|
+
if (!context) {
|
|
620
|
+
throw new Error("useFeatureFlag must be used within FeatureFlagProvider");
|
|
621
|
+
}
|
|
622
|
+
return context[flag];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Usage in a component
|
|
626
|
+
function Dashboard() {
|
|
627
|
+
const showNewDashboard = useFeatureFlag("newDashboard");
|
|
628
|
+
|
|
629
|
+
if (showNewDashboard) {
|
|
630
|
+
return <NewDashboard />;
|
|
631
|
+
}
|
|
632
|
+
return <LegacyDashboard />;
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Avoid Overusing Context
|
|
637
|
+
|
|
638
|
+
Context triggers re-renders for all consumers when the value changes. Split contexts by update frequency.
|
|
639
|
+
|
|
640
|
+
```tsx
|
|
641
|
+
// BAD: One giant context that changes frequently
|
|
642
|
+
const AppContext = createContext({
|
|
643
|
+
user: null,
|
|
644
|
+
theme: "light",
|
|
645
|
+
notifications: [],
|
|
646
|
+
mousePosition: { x: 0, y: 0 }, // changes 60fps
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// GOOD: Split by update frequency
|
|
650
|
+
const UserContext = createContext<User | null>(null); // rarely changes
|
|
651
|
+
const ThemeContext = createContext<Theme>("light"); // rarely changes
|
|
652
|
+
const NotificationContext = createContext<Notification[]>([]); // sometimes changes
|
|
653
|
+
// mousePosition should be a hook, not context
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## 11. State Machines for Complex UI
|
|
659
|
+
|
|
660
|
+
When a component has many states with specific transition rules, use a state machine instead of multiple booleans.
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
// Without state machine -- boolean soup
|
|
664
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
665
|
+
const [isError, setIsError] = useState(false);
|
|
666
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
667
|
+
const [isRetrying, setIsRetrying] = useState(false);
|
|
668
|
+
// What happens when isLoading AND isError are both true?
|
|
669
|
+
|
|
670
|
+
// With a state machine -- impossible states are impossible
|
|
671
|
+
type UploadState =
|
|
672
|
+
| { status: "idle" }
|
|
673
|
+
| { status: "selecting" }
|
|
674
|
+
| { status: "uploading"; progress: number }
|
|
675
|
+
| { status: "success"; url: string }
|
|
676
|
+
| { status: "error"; message: string };
|
|
677
|
+
|
|
678
|
+
type UploadAction =
|
|
679
|
+
| { type: "SELECT_FILE" }
|
|
680
|
+
| { type: "START_UPLOAD" }
|
|
681
|
+
| { type: "PROGRESS"; progress: number }
|
|
682
|
+
| { type: "COMPLETE"; url: string }
|
|
683
|
+
| { type: "FAIL"; message: string }
|
|
684
|
+
| { type: "RESET" };
|
|
685
|
+
|
|
686
|
+
function uploadReducer(state: UploadState, action: UploadAction): UploadState {
|
|
687
|
+
switch (state.status) {
|
|
688
|
+
case "idle":
|
|
689
|
+
if (action.type === "SELECT_FILE") return { status: "selecting" };
|
|
690
|
+
return state;
|
|
691
|
+
case "selecting":
|
|
692
|
+
if (action.type === "START_UPLOAD") return { status: "uploading", progress: 0 };
|
|
693
|
+
if (action.type === "RESET") return { status: "idle" };
|
|
694
|
+
return state;
|
|
695
|
+
case "uploading":
|
|
696
|
+
if (action.type === "PROGRESS") return { status: "uploading", progress: action.progress };
|
|
697
|
+
if (action.type === "COMPLETE") return { status: "success", url: action.url };
|
|
698
|
+
if (action.type === "FAIL") return { status: "error", message: action.message };
|
|
699
|
+
return state;
|
|
700
|
+
case "success":
|
|
701
|
+
if (action.type === "RESET") return { status: "idle" };
|
|
702
|
+
return state;
|
|
703
|
+
case "error":
|
|
704
|
+
if (action.type === "START_UPLOAD") return { status: "uploading", progress: 0 };
|
|
705
|
+
if (action.type === "RESET") return { status: "idle" };
|
|
706
|
+
return state;
|
|
707
|
+
default:
|
|
708
|
+
return state;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function FileUploader() {
|
|
713
|
+
const [state, dispatch] = useReducer(uploadReducer, { status: "idle" });
|
|
714
|
+
|
|
715
|
+
// Each status has exactly one UI representation
|
|
716
|
+
switch (state.status) {
|
|
717
|
+
case "idle":
|
|
718
|
+
return <button onClick={() => dispatch({ type: "SELECT_FILE" })}>Upload</button>;
|
|
719
|
+
case "selecting":
|
|
720
|
+
return <FileSelector onSelect={() => dispatch({ type: "START_UPLOAD" })} />;
|
|
721
|
+
case "uploading":
|
|
722
|
+
return <ProgressBar progress={state.progress} />;
|
|
723
|
+
case "success":
|
|
724
|
+
return <SuccessMessage url={state.url} onReset={() => dispatch({ type: "RESET" })} />;
|
|
725
|
+
case "error":
|
|
726
|
+
return <ErrorMessage message={state.message} onRetry={() => dispatch({ type: "START_UPLOAD" })} />;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### XState for Complex Machines
|
|
732
|
+
|
|
733
|
+
For multi-step wizards, workflows, or anything with parallel states, use XState.
|
|
734
|
+
|
|
735
|
+
```tsx
|
|
736
|
+
import { createMachine, assign } from "xstate";
|
|
737
|
+
import { useMachine } from "@xstate/react";
|
|
738
|
+
|
|
739
|
+
const checkoutMachine = createMachine({
|
|
740
|
+
id: "checkout",
|
|
741
|
+
initial: "cart",
|
|
742
|
+
context: { items: [], address: null, payment: null },
|
|
743
|
+
states: {
|
|
744
|
+
cart: { on: { PROCEED: "address" } },
|
|
745
|
+
address: {
|
|
746
|
+
on: {
|
|
747
|
+
SET_ADDRESS: { actions: assign({ address: (_, event) => event.data }) },
|
|
748
|
+
PROCEED: { target: "payment", guard: "hasAddress" },
|
|
749
|
+
BACK: "cart",
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
payment: {
|
|
753
|
+
on: {
|
|
754
|
+
SET_PAYMENT: { actions: assign({ payment: (_, event) => event.data }) },
|
|
755
|
+
SUBMIT: "processing",
|
|
756
|
+
BACK: "address",
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
processing: {
|
|
760
|
+
invoke: {
|
|
761
|
+
src: "processOrder",
|
|
762
|
+
onDone: "confirmation",
|
|
763
|
+
onError: "error",
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
confirmation: { type: "final" },
|
|
767
|
+
error: { on: { RETRY: "processing" } },
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## 12. Form Component Patterns
|
|
775
|
+
|
|
776
|
+
### Field Component Pattern
|
|
777
|
+
|
|
778
|
+
Encapsulate label, input, error, and description into a reusable Field component.
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
interface FieldProps {
|
|
782
|
+
label: string;
|
|
783
|
+
error?: string;
|
|
784
|
+
description?: string;
|
|
785
|
+
required?: boolean;
|
|
786
|
+
children: ReactNode;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function Field({ label, error, description, required, children }: FieldProps) {
|
|
790
|
+
const id = useId();
|
|
791
|
+
|
|
792
|
+
return (
|
|
793
|
+
<div className="space-y-1">
|
|
794
|
+
<label htmlFor={id} className="text-sm font-medium">
|
|
795
|
+
{label}
|
|
796
|
+
{required && <span className="text-red-500 ml-1" aria-hidden="true">*</span>}
|
|
797
|
+
</label>
|
|
798
|
+
{cloneElement(children as React.ReactElement, {
|
|
799
|
+
id,
|
|
800
|
+
"aria-describedby": description ? `${id}-desc` : undefined,
|
|
801
|
+
"aria-invalid": !!error,
|
|
802
|
+
})}
|
|
803
|
+
{description && (
|
|
804
|
+
<p id={`${id}-desc`} className="text-xs text-muted-foreground">
|
|
805
|
+
{description}
|
|
806
|
+
</p>
|
|
807
|
+
)}
|
|
808
|
+
{error && (
|
|
809
|
+
<p role="alert" className="text-xs text-red-500">
|
|
810
|
+
{error}
|
|
811
|
+
</p>
|
|
812
|
+
)}
|
|
813
|
+
</div>
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Usage
|
|
818
|
+
<Field label="Email" error={errors.email} required>
|
|
819
|
+
<input type="email" name="email" />
|
|
820
|
+
</Field>
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Multi-Step Form Pattern
|
|
824
|
+
|
|
825
|
+
```tsx
|
|
826
|
+
interface Step {
|
|
827
|
+
id: string;
|
|
828
|
+
title: string;
|
|
829
|
+
component: React.ComponentType<StepProps>;
|
|
830
|
+
validate: (data: FormData) => Record<string, string> | null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function MultiStepForm({ steps }: { steps: Step[] }) {
|
|
834
|
+
const [currentStep, setCurrentStep] = useState(0);
|
|
835
|
+
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
|
836
|
+
|
|
837
|
+
const StepComponent = steps[currentStep].component;
|
|
838
|
+
|
|
839
|
+
function handleNext(stepData: Record<string, unknown>) {
|
|
840
|
+
const errors = steps[currentStep].validate(stepData);
|
|
841
|
+
if (errors) return errors;
|
|
842
|
+
|
|
843
|
+
setFormData((prev) => ({ ...prev, ...stepData }));
|
|
844
|
+
if (currentStep < steps.length - 1) {
|
|
845
|
+
setCurrentStep((prev) => prev + 1);
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return (
|
|
851
|
+
<div>
|
|
852
|
+
<StepIndicator steps={steps} current={currentStep} />
|
|
853
|
+
<StepComponent
|
|
854
|
+
data={formData}
|
|
855
|
+
onNext={handleNext}
|
|
856
|
+
onBack={() => setCurrentStep((prev) => Math.max(0, prev - 1))}
|
|
857
|
+
/>
|
|
858
|
+
</div>
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## 13. Modal and Dialog Management
|
|
866
|
+
|
|
867
|
+
### Portal-Based Modal
|
|
868
|
+
|
|
869
|
+
```tsx
|
|
870
|
+
import { createPortal } from "react-dom";
|
|
871
|
+
import { useEffect, useRef } from "react";
|
|
872
|
+
|
|
873
|
+
function Modal({
|
|
874
|
+
open,
|
|
875
|
+
onClose,
|
|
876
|
+
children,
|
|
877
|
+
}: {
|
|
878
|
+
open: boolean;
|
|
879
|
+
onClose: () => void;
|
|
880
|
+
children: ReactNode;
|
|
881
|
+
}) {
|
|
882
|
+
const previousFocus = useRef<HTMLElement | null>(null);
|
|
883
|
+
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
if (open) {
|
|
886
|
+
previousFocus.current = document.activeElement as HTMLElement;
|
|
887
|
+
// Trap focus inside modal
|
|
888
|
+
} else {
|
|
889
|
+
previousFocus.current?.focus();
|
|
890
|
+
}
|
|
891
|
+
}, [open]);
|
|
892
|
+
|
|
893
|
+
// Close on Escape
|
|
894
|
+
useEffect(() => {
|
|
895
|
+
if (!open) return;
|
|
896
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
897
|
+
if (e.key === "Escape") onClose();
|
|
898
|
+
}
|
|
899
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
900
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
901
|
+
}, [open, onClose]);
|
|
902
|
+
|
|
903
|
+
if (!open) return null;
|
|
904
|
+
|
|
905
|
+
return createPortal(
|
|
906
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
907
|
+
<div className="fixed inset-0 bg-black/50" onClick={onClose} aria-hidden="true" />
|
|
908
|
+
<div role="dialog" aria-modal="true" className="relative bg-white rounded-lg p-6">
|
|
909
|
+
{children}
|
|
910
|
+
</div>
|
|
911
|
+
</div>,
|
|
912
|
+
document.body
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### Declarative Modal Management
|
|
918
|
+
|
|
919
|
+
```tsx
|
|
920
|
+
// hooks/use-modal.ts
|
|
921
|
+
function useModal() {
|
|
922
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
923
|
+
|
|
924
|
+
const modal = useMemo(
|
|
925
|
+
() => ({
|
|
926
|
+
isOpen,
|
|
927
|
+
open: () => setIsOpen(true),
|
|
928
|
+
close: () => setIsOpen(false),
|
|
929
|
+
toggle: () => setIsOpen((prev) => !prev),
|
|
930
|
+
props: { open: isOpen, onClose: () => setIsOpen(false) },
|
|
931
|
+
}),
|
|
932
|
+
[isOpen]
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
return modal;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Usage
|
|
939
|
+
function UserList() {
|
|
940
|
+
const deleteModal = useModal();
|
|
941
|
+
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
942
|
+
|
|
943
|
+
function handleDelete(user: User) {
|
|
944
|
+
setSelectedUser(user);
|
|
945
|
+
deleteModal.open();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return (
|
|
949
|
+
<div>
|
|
950
|
+
{users.map((user) => (
|
|
951
|
+
<UserRow key={user.id} user={user} onDelete={() => handleDelete(user)} />
|
|
952
|
+
))}
|
|
953
|
+
<ConfirmDialog
|
|
954
|
+
{...deleteModal.props}
|
|
955
|
+
title="Delete User"
|
|
956
|
+
description={`Are you sure you want to delete ${selectedUser?.name}?`}
|
|
957
|
+
onConfirm={() => deleteUser(selectedUser!.id)}
|
|
958
|
+
/>
|
|
959
|
+
</div>
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## 14. List and Item Patterns
|
|
967
|
+
|
|
968
|
+
### Consistent List API
|
|
969
|
+
|
|
970
|
+
```tsx
|
|
971
|
+
interface ListProps<T> {
|
|
972
|
+
items: T[];
|
|
973
|
+
renderItem: (item: T, index: number) => ReactNode;
|
|
974
|
+
keyExtractor: (item: T) => string;
|
|
975
|
+
emptyState?: ReactNode;
|
|
976
|
+
loading?: boolean;
|
|
977
|
+
loadingSkeleton?: ReactNode;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function List<T>({
|
|
981
|
+
items,
|
|
982
|
+
renderItem,
|
|
983
|
+
keyExtractor,
|
|
984
|
+
emptyState,
|
|
985
|
+
loading,
|
|
986
|
+
loadingSkeleton,
|
|
987
|
+
}: ListProps<T>) {
|
|
988
|
+
if (loading && loadingSkeleton) return <>{loadingSkeleton}</>;
|
|
989
|
+
if (items.length === 0 && emptyState) return <>{emptyState}</>;
|
|
990
|
+
|
|
991
|
+
return (
|
|
992
|
+
<ul role="list">
|
|
993
|
+
{items.map((item, index) => (
|
|
994
|
+
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
|
|
995
|
+
))}
|
|
996
|
+
</ul>
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Usage
|
|
1001
|
+
<List
|
|
1002
|
+
items={users}
|
|
1003
|
+
keyExtractor={(user) => user.id}
|
|
1004
|
+
renderItem={(user) => <UserCard user={user} />}
|
|
1005
|
+
emptyState={<EmptyState message="No users found" />}
|
|
1006
|
+
loading={isLoading}
|
|
1007
|
+
loadingSkeleton={<UserListSkeleton count={5} />}
|
|
1008
|
+
/>
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Selectable List Pattern
|
|
1012
|
+
|
|
1013
|
+
```tsx
|
|
1014
|
+
function useSelection<T>(
|
|
1015
|
+
items: T[],
|
|
1016
|
+
keyExtractor: (item: T) => string
|
|
1017
|
+
) {
|
|
1018
|
+
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
|
1019
|
+
|
|
1020
|
+
const toggleItem = useCallback((item: T) => {
|
|
1021
|
+
const key = keyExtractor(item);
|
|
1022
|
+
setSelectedKeys((prev) => {
|
|
1023
|
+
const next = new Set(prev);
|
|
1024
|
+
if (next.has(key)) {
|
|
1025
|
+
next.delete(key);
|
|
1026
|
+
} else {
|
|
1027
|
+
next.add(key);
|
|
1028
|
+
}
|
|
1029
|
+
return next;
|
|
1030
|
+
});
|
|
1031
|
+
}, [keyExtractor]);
|
|
1032
|
+
|
|
1033
|
+
const selectAll = useCallback(() => {
|
|
1034
|
+
setSelectedKeys(new Set(items.map(keyExtractor)));
|
|
1035
|
+
}, [items, keyExtractor]);
|
|
1036
|
+
|
|
1037
|
+
const clearSelection = useCallback(() => {
|
|
1038
|
+
setSelectedKeys(new Set());
|
|
1039
|
+
}, []);
|
|
1040
|
+
|
|
1041
|
+
const isSelected = useCallback(
|
|
1042
|
+
(item: T) => selectedKeys.has(keyExtractor(item)),
|
|
1043
|
+
[selectedKeys, keyExtractor]
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
return {
|
|
1047
|
+
selectedKeys,
|
|
1048
|
+
selectedCount: selectedKeys.size,
|
|
1049
|
+
toggleItem,
|
|
1050
|
+
selectAll,
|
|
1051
|
+
clearSelection,
|
|
1052
|
+
isSelected,
|
|
1053
|
+
isAllSelected: selectedKeys.size === items.length && items.length > 0,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
---
|
|
1059
|
+
|
|
1060
|
+
## 15. Anti-Patterns
|
|
1061
|
+
|
|
1062
|
+
### NEVER
|
|
1063
|
+
|
|
1064
|
+
- Use boolean props for mutually exclusive states (`isPrimary` and `isSecondary` on the same component)
|
|
1065
|
+
- Deeply nest context providers when a single provider with composed state would work
|
|
1066
|
+
- Use `cloneElement` to inject props when composition or render props are clearer
|
|
1067
|
+
- Build "God components" with 20+ props -- decompose into compound components
|
|
1068
|
+
- Store UI state in global state management (Redux, Zustand) -- use local state or context
|
|
1069
|
+
- Create wrapper components that just pass all props through to a child
|
|
1070
|
+
- Use `children` for multiple unrelated slots -- use named props instead
|
|
1071
|
+
- Couple component logic to a specific data fetching library
|
|
1072
|
+
|
|
1073
|
+
### ALWAYS
|
|
1074
|
+
|
|
1075
|
+
- Provide TypeScript types for all component props
|
|
1076
|
+
- Include `children` in the type when the component accepts arbitrary content
|
|
1077
|
+
- Use `ReactNode` for slot types (not `JSX.Element` or `React.ReactElement`)
|
|
1078
|
+
- Make components work in both controlled and uncontrolled modes when sensible
|
|
1079
|
+
- Throw descriptive errors when compound component children are used outside their parent
|
|
1080
|
+
- Test components in isolation with different prop combinations
|
|
1081
|
+
- Document which props are required vs optional with TypeScript
|
|
1082
|
+
- Use discriminated unions for variant props instead of string unions when variants have different data shapes
|
|
1083
|
+
|
|
1084
|
+
---
|
|
1085
|
+
|
|
1086
|
+
## 16. Decision Guide
|
|
1087
|
+
|
|
1088
|
+
```
|
|
1089
|
+
+----------------------------+
|
|
1090
|
+
| Need shared implicit state |
|
|
1091
|
+
| across siblings? |
|
|
1092
|
+
+-----------+----------------+
|
|
1093
|
+
YES | NO
|
|
1094
|
+
+-----------v---+ +---v-------------------+
|
|
1095
|
+
| Compound | | Need layout control |
|
|
1096
|
+
| Components | | for consumer? |
|
|
1097
|
+
+---------------+ +---+-------------------+
|
|
1098
|
+
YES | NO
|
|
1099
|
+
+-------v------+ +--v-----------------+
|
|
1100
|
+
| Slot-based | | Need to share |
|
|
1101
|
+
| Composition | | logic, not UI? |
|
|
1102
|
+
+--------------+ +--+-----------------+
|
|
1103
|
+
YES | NO
|
|
1104
|
+
+-------v------+ +--v-----------+
|
|
1105
|
+
| Custom Hook | | Simple Props |
|
|
1106
|
+
+--------------+ +--------------+
|
|
1107
|
+
```
|