diff --git a/.agents/skills/coding-standards/SKILL.md b/.agents/skills/coding-standards/SKILL.md index d9e444a..ca1d043 100644 --- a/.agents/skills/coding-standards/SKILL.md +++ b/.agents/skills/coding-standards/SKILL.md @@ -37,8 +37,8 @@ Standards for this TypeScript/React frontend. Not a NestJS API. ### Naming ```typescript -const searchQuery = 'widget' -const isAuthenticated = true +const searchQuery = 'widget'; +const isAuthenticated = true; async function fetchVehicleType(id: string) {} function isValidCode(code: string): boolean {} @@ -47,8 +47,8 @@ function isValidCode(code: string): boolean {} ### Immutability (CRITICAL) ```typescript -const updated = { ...row, name: 'New' } -const nextItems = [...items, newItem] +const updated = { ...row, name: 'New' }; +const nextItems = [...items, newItem]; ``` Never mutate: no `push`, `splice`, or in-place property assignment on shared state. @@ -71,12 +71,12 @@ No `any`. Prefer entity types in `domain/entities` and DTOs next to transformers ## Imports (this repo) ```ts -import { Button, Text } from '@repo/ui/components' -import { FieldTextInput } from '@repo/ui/form' -import { EnterpriseModuleProvider } from '@repo/ui/foundations' -import { compose, required } from '@repo/ui/validators' -import { createHttpClient } from '@repo/core-api/http-client' -import { CommonRemoteDataServices } from '@repo/core-api/data-services' +import { Button, Text } from '@repo/ui/components'; +import { FieldTextInput } from '@repo/ui/form'; +import { EnterpriseModuleProvider } from '@repo/ui/foundations'; +import { compose, required } from '@repo/ui/validators'; +import { createHttpClient } from '@repo/core-api/http-client'; +import { CommonRemoteDataServices } from '@repo/core-api/data-services'; ``` Do not import `@mantine/core` or axios in `apps/web` feature code. diff --git a/.agents/skills/continuous-learning/SKILL.md b/.agents/skills/continuous-learning/SKILL.md index 11b0fe4..6eca113 100644 --- a/.agents/skills/continuous-learning/SKILL.md +++ b/.agents/skills/continuous-learning/SKILL.md @@ -32,23 +32,19 @@ Edit `config.json` to customize: "debugging_techniques", "project_specific" ], - "ignore_patterns": [ - "simple_typos", - "one_time_fixes", - "external_api_issues" - ] + "ignore_patterns": ["simple_typos", "one_time_fixes", "external_api_issues"] } ``` ## Pattern Types -| Pattern | Description | -|---------|-------------| -| `error_resolution` | How specific errors were resolved | -| `user_corrections` | Patterns from user corrections | -| `workarounds` | Solutions to framework/library quirks | -| `debugging_techniques` | Effective debugging approaches | -| `project_specific` | Project-specific conventions | +| Pattern | Description | +| ---------------------- | ------------------------------------- | +| `error_resolution` | How specific errors were resolved | +| `user_corrections` | Patterns from user corrections | +| `workarounds` | Solutions to framework/library quirks | +| `debugging_techniques` | Effective debugging approaches | +| `project_specific` | Project-specific conventions | ## Hook Setup diff --git a/.agents/skills/detail-layout/SKILL.md b/.agents/skills/detail-layout/SKILL.md index e259b2b..64684dd 100644 --- a/.agents/skills/detail-layout/SKILL.md +++ b/.agents/skills/detail-layout/SKILL.md @@ -32,11 +32,11 @@ Optional **identity / hero** (§3) is an extra content block when the entity ben Look at **how many distinct data categories** the entity has — that decides stacked vs tabbed body: -| Detail type | Body mode | -| --- | --- | -| Few categories (≤3–4 section cards; e.g. Branch, User, simple master data) | **Stacked sections** — vertical stack of section cards under the header | +| Detail type | Body mode | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Few categories (≤3–4 section cards; e.g. Branch, User, simple master data) | **Stacked sections** — vertical stack of section cards under the header | | Many categories (>3–4 distinct groups) **or** the user explicitly asks for tabs | **Tabbed body** — keep a slim always-visible top (identity / summary / stepper), then a horizontal tab bar; each tab owns one category's content | -| Narrow / mobile viewport | Same mode as desktop; only the **internal** key-value and status layouts collapse (§13) | +| Narrow / mobile viewport | Same mode as desktop; only the **internal** key-value and status layouts collapse (§13) | Once a page picks stacked vs tabbed, stay consistent — don't mix an ad-hoc tab region with an ad-hoc long scroll of the same categories. @@ -67,8 +67,8 @@ Use when the entity has a meaningful visual (device photo, avatar, logo). Skip f Single horizontal row inside one section surface: -| Left | Middle (flex grow) | Right | -| --- | --- | --- | +| Left | Middle (flex grow) | Right | +| ----------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | | Leading media (fixed square/circle) | Title stack: primary name (+ id if not already in chrome), then one short subtitle/category line | Primary content action if it belongs to this block (usually omit — Edit lives in module chrome) | - Media left-aligned; title stack left-aligned next to media; any block-level action right-aligned on the same row. @@ -203,25 +203,25 @@ Horizontal gaps inside grids match the medium rhythm. Don't invent a fourth gap ## 13. Responsive collapse -| Wide layout | Narrow collapse | -| --- | --- | -| Identity hero row (media \| title \| action) | Centered vertical stack (media → title → subtitle → full-width action) | -| Key-value label-above grid (§5A) | Label-left / value-right rows (§5B) | -| Status metric horizontal row (§6) | Stacked metric sub-blocks; badge beside value | -| Summary 2–3 columns (§7) | Stacked column groups, same order | -| Tab list | Horizontally scrollable tab list; panels still one-at-a-time | -| Table | Horizontal scroll inside the section **or** stacked label/value per row — pick one strategy per table type and keep it project-wide | +| Wide layout | Narrow collapse | +| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Identity hero row (media \| title \| action) | Centered vertical stack (media → title → subtitle → full-width action) | +| Key-value label-above grid (§5A) | Label-left / value-right rows (§5B) | +| Status metric horizontal row (§6) | Stacked metric sub-blocks; badge beside value | +| Summary 2–3 columns (§7) | Stacked column groups, same order | +| Tab list | Horizontally scrollable tab list; panels still one-at-a-time | +| Table | Horizontal scroll inside the section **or** stacked label/value per row — pick one strategy per table type and keep it project-wide | Never re-pair fields into different logical groups on collapse — only column count / orientation changes. ## 14. Action placement (position only) -| Action scope | Position | -| --- | --- | -| Global entity actions (Edit, Delete, Activate, …) | Module header actions via `EnterpriseDetailPageProvider` — top-right of chrome | -| Section-scoped action (Upload, Add record) | Right side of that section's header band (§4 / §11) | -| Section filters | Right side of the table/section header, before or beside the section primary action | -| Row action | Trailing column of that row only | +| Action scope | Position | +| ------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Global entity actions (Edit, Delete, Activate, …) | Module header actions via `EnterpriseDetailPageProvider` — top-right of chrome | +| Section-scoped action (Upload, Add record) | Right side of that section's header band (§4 / §11) | +| Section filters | Right side of the table/section header, before or beside the section primary action | +| Row action | Trailing column of that row only | Do not duplicate Edit in the body if the provider already exposes it, unless a mobile identity block needs a full-width local affordance (§3 narrow). diff --git a/.agents/skills/eval-harness/SKILL.md b/.agents/skills/eval-harness/SKILL.md index 7f088eb..e79af72 100644 --- a/.agents/skills/eval-harness/SKILL.md +++ b/.agents/skills/eval-harness/SKILL.md @@ -5,6 +5,7 @@ A formal evaluation framework for Claude Code sessions, implementing eval-driven ## Philosophy Eval-Driven Development treats evals as the "unit tests of AI development": + - Define expected behavior BEFORE implementation - Run evals continuously during development - Track regressions with each change @@ -13,33 +14,41 @@ Eval-Driven Development treats evals as the "unit tests of AI development": ## Eval Types ### Capability Evals + Test if Claude can do something it couldn't before: + ```markdown [CAPABILITY EVAL: feature-name] Task: Description of what Claude should accomplish Success Criteria: - - [ ] Criterion 1 - - [ ] Criterion 2 - - [ ] Criterion 3 -Expected Output: Description of expected result + +- [ ] Criterion 1 +- [ ] Criterion 2 +- [ ] Criterion 3 + Expected Output: Description of expected result ``` ### Regression Evals + Ensure changes don't break existing functionality: + ```markdown [REGRESSION EVAL: feature-name] Baseline: SHA or checkpoint name Tests: - - existing-test-1: PASS/FAIL - - existing-test-2: PASS/FAIL - - existing-test-3: PASS/FAIL -Result: X/Y passed (previously Y/Y) + +- existing-test-1: PASS/FAIL +- existing-test-2: PASS/FAIL +- existing-test-3: PASS/FAIL + Result: X/Y passed (previously Y/Y) ``` ## Grader Types ### 1. Code-Based Grader + Deterministic checks using code: + ```bash # Check if file contains expected pattern grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL" @@ -52,10 +61,13 @@ npm run build && echo "PASS" || echo "FAIL" ``` ### 2. Model-Based Grader + Use Claude to evaluate open-ended outputs: + ```markdown [MODEL GRADER PROMPT] Evaluate the following code change: + 1. Does it solve the stated problem? 2. Is it well-structured? 3. Are edge cases handled? @@ -66,7 +78,9 @@ Reasoning: [explanation] ``` ### 3. Human Grader + Flag for manual review: + ```markdown [HUMAN REVIEW REQUIRED] Change: Description of what changed @@ -77,13 +91,17 @@ Risk Level: LOW/MEDIUM/HIGH ## Metrics ### pass@k + "At least one success in k attempts" + - pass@1: First attempt success rate - pass@3: Success within 3 attempts - Typical target: pass@3 > 90% ### pass^k + "All k trials succeed" + - Higher bar for reliability - pass^3: 3 consecutive successes - Use for critical paths @@ -91,28 +109,34 @@ Risk Level: LOW/MEDIUM/HIGH ## Eval Workflow ### 1. Define (Before Coding) + ```markdown ## EVAL DEFINITION: feature-xyz ### Capability Evals + 1. Can create new user account 2. Can validate email format 3. Can hash password securely ### Regression Evals + 1. Existing login still works 2. Session management unchanged 3. Logout flow intact ### Success Metrics + - pass@3 > 90% for capability evals - pass^3 = 100% for regression evals ``` ### 2. Implement + Write code to pass the defined evals. ### 3. Evaluate + ```bash # Run capability evals [Run each capability eval, record PASS/FAIL] @@ -124,25 +148,25 @@ npm test -- --testPathPattern="existing" ``` ### 4. Report + ```markdown -EVAL REPORT: feature-xyz -======================== +# EVAL REPORT: feature-xyz Capability Evals: - create-user: PASS (pass@1) - validate-email: PASS (pass@2) - hash-password: PASS (pass@1) - Overall: 3/3 passed +create-user: PASS (pass@1) +validate-email: PASS (pass@2) +hash-password: PASS (pass@1) +Overall: 3/3 passed Regression Evals: - login-flow: PASS - session-mgmt: PASS - logout-flow: PASS - Overall: 3/3 passed +login-flow: PASS +session-mgmt: PASS +logout-flow: PASS +Overall: 3/3 passed Metrics: - pass@1: 67% (2/3) - pass@3: 100% (3/3) +pass@1: 67% (2/3) +pass@3: 100% (3/3) Status: READY FOR REVIEW ``` @@ -150,26 +174,33 @@ Status: READY FOR REVIEW ## Integration Patterns ### Pre-Implementation + ``` /eval define feature-name ``` + Creates eval definition file at `.cursor/evals/feature-name.md` ### During Implementation + ``` /eval check feature-name ``` + Runs current evals and reports status ### Post-Implementation + ``` /eval report feature-name ``` + Generates full eval report ## Eval Storage Store evals in project: + ``` .cursor/ evals/ @@ -194,7 +225,9 @@ Store evals in project: ## EVAL: add-authentication ### Phase 1: Define (10 min) + Capability Evals: + - [ ] User can register with email/password - [ ] User can login with valid credentials - [ ] Invalid credentials rejected with proper error @@ -202,19 +235,23 @@ Capability Evals: - [ ] Logout clears session Regression Evals: + - [ ] Public routes still accessible - [ ] API responses unchanged - [ ] Database schema compatible ### Phase 2: Implement (varies) + [Write code] ### Phase 3: Evaluate + Run: /eval check add-authentication ### Phase 4: Report -EVAL REPORT: add-authentication -============================== + +# EVAL REPORT: add-authentication + Capability: 5/5 passed (pass@3: 100%) Regression: 3/3 passed (pass^3: 100%) Status: SHIP IT diff --git a/.agents/skills/project-guidelines-example/SKILL.md b/.agents/skills/project-guidelines-example/SKILL.md index a5d6def..87070de 100644 --- a/.agents/skills/project-guidelines-example/SKILL.md +++ b/.agents/skills/project-guidelines-example/SKILL.md @@ -82,7 +82,7 @@ Auth lives under `src/apps/auth/`. Shared-across-modules code lives in `src/core ### Module config + factory ```typescript -import { ModuleConfigEntity } from '@repo/ui/foundations' +import { ModuleConfigEntity } from '@repo/ui/foundations'; export const fullPageModuleConfig: ModuleConfigEntity = { moduleKey: 'EXAMPLE_FULL_PAGE', @@ -91,28 +91,28 @@ export const fullPageModuleConfig: ModuleConfigEntity = { webUrl: '/app/example/full-page', moduleCategory: 'FULL_PAGE', moduleType: 'MASTER_DATA', -} +}; ``` ```tsx -import { EnterpriseModuleProvider } from '@repo/ui/foundations' -import { registerModuleNamespace } from '@repo/core-i18n' -import { apiClient } from '../../../../../../../core/lib/api-client' +import { EnterpriseModuleProvider } from '@repo/ui/foundations'; +import { registerModuleNamespace } from '@repo/core-i18n'; +import { apiClient } from '../../../../../../../core/lib/api-client'; -registerModuleNamespace(fullPageModuleConfig.translationNamespace, { id, en }) +registerModuleNamespace(fullPageModuleConfig.translationNamespace, { id, en }); ``` ### Validators (Zod) ```typescript -import { z } from 'zod' -import { compose, required, rangeLength } from '@repo/ui/validators' +import { z } from 'zod'; +import { compose, required, rangeLength } from '@repo/ui/validators'; export const createFullPageSchema = (t: (key: string) => string) => z.object({ code: compose(z.string(), required(t('common:fields.code'))), name: compose(z.string(), required(t('common:fields.name')), rangeLength(3, 50, t('common:fields.name'))), - }) + }); ``` ### Forms and HTTP diff --git a/.agents/skills/security-review/SKILL.md b/.agents/skills/security-review/SKILL.md index bdd82fc..ca7b0ab 100644 --- a/.agents/skills/security-review/SKILL.md +++ b/.agents/skills/security-review/SKILL.md @@ -21,11 +21,11 @@ Client-side security for this React/Electron frontend. There is no NestJS API or ```typescript // NEVER -const apiKey = "sk-proj-xxxxx" +const apiKey = 'sk-proj-xxxxx'; // ALWAYS -import { ENV } from '../environment' -if (!ENV.API_BASE_URL) throw new Error('VITE_API_BASE_URL is not configured') +import { ENV } from '../environment'; +if (!ENV.API_BASE_URL) throw new Error('VITE_API_BASE_URL is not configured'); ``` - [ ] No hardcoded secrets diff --git a/.agents/skills/strategic-compact/SKILL.md b/.agents/skills/strategic-compact/SKILL.md index 7503d30..a187e15 100644 --- a/.agents/skills/strategic-compact/SKILL.md +++ b/.agents/skills/strategic-compact/SKILL.md @@ -10,11 +10,13 @@ Suggests manual `/compact` at strategic points in your workflow rather than rely ## Why Strategic Compaction? Auto-compaction triggers at arbitrary points: + - Often mid-task, losing important context - No awareness of logical task boundaries - Can interrupt complex multi-step operations Strategic compaction at logical boundaries: + - **After exploration, before execution** - Compact research context, keep implementation plan - **After completing a milestone** - Fresh start for next phase - **Before major context shifts** - Clear exploration context before different task @@ -47,6 +49,7 @@ Already wired in `.cursor/hooks.json` as an `afterFileEdit` command: ## Configuration Environment variables: + - `COMPACT_THRESHOLD` - Tool calls before first suggestion (default: 50) ## Best Practices @@ -54,7 +57,7 @@ Environment variables: 1. **Compact after planning** - Once plan is finalized, compact to start fresh 2. **Compact after debugging** - Clear error-resolution context before continuing 3. **Don't compact mid-implementation** - Preserve context for related changes -4. **Read the suggestion** - The hook tells you *when*, you decide *if* +4. **Read the suggestion** - The hook tells you _when_, you decide _if_ ## Related diff --git a/.agents/skills/tdd-workflow/SKILL.md b/.agents/skills/tdd-workflow/SKILL.md index cf0d300..3b34fe2 100644 --- a/.agents/skills/tdd-workflow/SKILL.md +++ b/.agents/skills/tdd-workflow/SKILL.md @@ -38,31 +38,31 @@ TDD for this frontend monorepo (Vitest, not NestJS/Supertest). ## Unit example ```typescript -import { describe, it, expect } from 'vitest' -import { createFullPageSchema } from './full-page.validator' +import { describe, it, expect } from 'vitest'; +import { createFullPageSchema } from './full-page.validator'; describe('createFullPageSchema', () => { - const t = (key: string) => key + const t = (key: string) => key; it('rejects an empty code', () => { - const result = createFullPageSchema(t).safeParse({ code: '', name: 'Widget' }) - expect(result.success).toBe(false) - }) -}) + const result = createFullPageSchema(t).safeParse({ code: '', name: 'Widget' }); + expect(result.success).toBe(false); + }); +}); ``` ## Component example ```tsx -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; it('calls onClick', async () => { - const onClick = vi.fn() - render() - await userEvent.click(screen.getByRole('button', { name: 'Save' })) - expect(onClick).toHaveBeenCalledTimes(1) -}) + const onClick = vi.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onClick).toHaveBeenCalledTimes(1); +}); ``` ## Mocking @@ -72,7 +72,7 @@ Mock `@repo/core-api` and `apiClient`, not a database. ```ts vi.mock('@repo/core-api/http-client', () => ({ createHttpClient: () => ({ get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() }), -})) +})); ``` ## File organization diff --git a/.agents/skills/verification-loop/SKILL.md b/.agents/skills/verification-loop/SKILL.md index b56bb7e..bb6fd8c 100644 --- a/.agents/skills/verification-loop/SKILL.md +++ b/.agents/skills/verification-loop/SKILL.md @@ -5,6 +5,7 @@ A comprehensive verification system for Claude Code sessions. ## When to Use Invoke this skill: + - After completing a feature or significant code change - Before creating a PR - When you want to ensure quality gates pass @@ -13,6 +14,7 @@ Invoke this skill: ## Verification Phases ### Phase 1: Build Verification + ```bash # Check if project builds npm run build 2>&1 | tail -20 @@ -23,6 +25,7 @@ pnpm build 2>&1 | tail -20 If build fails, STOP and fix before continuing. ### Phase 2: Type Check + ```bash # TypeScript projects npx tsc --noEmit 2>&1 | head -30 @@ -34,6 +37,7 @@ pyright . 2>&1 | head -30 Report all type errors. Fix critical ones before continuing. ### Phase 3: Lint Check + ```bash # JavaScript/TypeScript npm run lint 2>&1 | head -30 @@ -43,6 +47,7 @@ ruff check . 2>&1 | head -30 ``` ### Phase 4: Test Suite + ```bash # Run tests with coverage npm run test -- --coverage 2>&1 | tail -50 @@ -52,12 +57,14 @@ npm run test -- --coverage 2>&1 | tail -50 ``` Report: + - Total tests: X - Passed: X - Failed: X - Coverage: X% ### Phase 5: Security Scan + ```bash # Check for secrets grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10 @@ -68,6 +75,7 @@ grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | hea ``` ### Phase 6: Diff Review + ```bash # Show what changed git diff --stat @@ -75,6 +83,7 @@ git diff HEAD~1 --name-only ``` Review each changed file for: + - Unintended changes - Missing error handling - Potential edge cases @@ -107,6 +116,7 @@ For long sessions, run verification every 15 minutes or after major changes: ```markdown Set a mental checkpoint: + - After completing each function - After finishing a component - Before moving to next task diff --git a/.cursor/agents/architect.md b/.cursor/agents/architect.md index fabfbad..5bef72c 100644 --- a/.cursor/agents/architect.md +++ b/.cursor/agents/architect.md @@ -19,18 +19,21 @@ You are a senior software architect specializing in scalable, maintainable syste ## Architecture Review Process ### 1. Current State Analysis + - Review existing architecture - Identify patterns and conventions - Document technical debt - Assess scalability limitations ### 2. Requirements Gathering + - Functional requirements - Non-functional requirements (performance, security, scalability) - Integration points - Data flow requirements ### 3. Design Proposal + - High-level architecture diagram - Component responsibilities - Data models @@ -38,7 +41,9 @@ You are a senior software architect specializing in scalable, maintainable syste - Integration patterns ### 4. Trade-Off Analysis + For each design decision, document: + - **Pros**: Benefits and advantages - **Cons**: Drawbacks and limitations - **Alternatives**: Other options considered @@ -47,12 +52,14 @@ For each design decision, document: ## Architectural Principles ### 1. Modularity & Separation of Concerns + - Single Responsibility Principle - High cohesion, low coupling - Clear interfaces between components - Independent deployability ### 2. Scalability + - Horizontal scaling capability - Stateless design where possible - Efficient database queries @@ -60,6 +67,7 @@ For each design decision, document: - Load balancing considerations ### 3. Maintainability + - Clear code organization - Consistent patterns - Comprehensive documentation @@ -67,6 +75,7 @@ For each design decision, document: - Simple to understand ### 4. Security + - Defense in depth - Principle of least privilege - Input validation at boundaries @@ -74,6 +83,7 @@ For each design decision, document: - Audit trail ### 5. Performance + - Efficient algorithms - Minimal network requests - Optimized database queries @@ -83,6 +93,7 @@ For each design decision, document: ## Common Patterns ### Frontend Patterns + - **Component Composition**: Build complex UI from simple components - **Container/Presenter**: Separate data logic from presentation - **Custom Hooks**: Reusable stateful logic @@ -90,6 +101,7 @@ For each design decision, document: - **Code Splitting**: Lazy load routes and heavy components ### Backend Patterns + - **Repository Pattern**: Abstract data access - **Service Layer**: Business logic separation - **Middleware Pattern**: Request/response processing @@ -97,6 +109,7 @@ For each design decision, document: - **CQRS**: Separate read and write operations ### Data Patterns + - **Normalized Database**: Reduce redundancy - **Denormalized for Read Performance**: Optimize queries - **Event Sourcing**: Audit trail and replayability @@ -111,25 +124,31 @@ For significant architectural decisions, create ADRs: # ADR-001: Feature modules live in apps/web, shared UI in packages/ui ## Context + Need a default place for product screens vs reusable components. ## Decision + Product modules under `apps/web/src/apps/main/modules/` (copy `example/full-page`). Shared primitives in `packages/ui`. Cross-module app code in `src/core/`. ## Consequences ### Positive + - Clear promotion path: module → core → package - Showcase and docs stay free of product logic ### Negative + - Easy to over-share too early (YAGNI) ### Alternatives Considered + - All UI in apps/web (duplicates landing/showcase) - All features in packages/ui (mixes product with design system) ## Status + Accepted ``` @@ -138,18 +157,21 @@ Accepted When designing a new system or feature: ### Functional Requirements + - [ ] User stories documented - [ ] API contracts defined - [ ] Data models specified - [ ] UI/UX flows mapped ### Non-Functional Requirements + - [ ] Performance targets defined (latency, throughput) - [ ] Scalability requirements specified - [ ] Security requirements identified - [ ] Availability targets set (uptime %) ### Technical Design + - [ ] Architecture diagram created - [ ] Component responsibilities defined - [ ] Data flow documented @@ -158,6 +180,7 @@ When designing a new system or feature: - [ ] Testing strategy planned ### Operations + - [ ] Deployment strategy defined - [ ] Monitoring and alerting planned - [ ] Backup and recovery strategy @@ -166,6 +189,7 @@ When designing a new system or feature: ## Red Flags Watch for these architectural anti-patterns: + - **Big Ball of Mud**: No clear structure - **Golden Hammer**: Using same solution for everything - **Premature Optimization**: Optimizing too early diff --git a/.cursor/agents/code-reviewer.md b/.cursor/agents/code-reviewer.md index 0bba652..796445f 100644 --- a/.cursor/agents/code-reviewer.md +++ b/.cursor/agents/code-reviewer.md @@ -8,11 +8,13 @@ model: opus You are a senior code reviewer ensuring high standards of code quality and security. When invoked: + 1. Run git diff to see recent changes 2. Focus on modified files 3. Begin review immediately Review checklist: + - Code is simple and readable - Functions and variables are well-named - No duplicated code @@ -25,6 +27,7 @@ Review checklist: - Licenses of integrated libraries checked Provide feedback organized by priority: + - Critical issues (must fix) - Warnings (should fix) - Suggestions (consider improving) @@ -74,6 +77,7 @@ Include specific examples of how to fix issues. ## Review Output Format For each issue: + ``` [CRITICAL] Hardcoded API key File: src/core/lib/api-client.ts:42 diff --git a/.cursor/agents/e2e-runner.md b/.cursor/agents/e2e-runner.md index 830fe12..e798dc2 100644 --- a/.cursor/agents/e2e-runner.md +++ b/.cursor/agents/e2e-runner.md @@ -42,14 +42,14 @@ Canonical sample: `apps/web/src/apps/main/modules/example/full-page/`. Copy that ### Package component tests (Testing Library) ```tsx -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { FieldTextInput } from '@repo/ui/form' +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { FieldTextInput } from '@repo/ui/form'; it('renders the field label', () => { - render() - expect(screen.getByLabelText('Code')).toBeInTheDocument() -}) + render(); + expect(screen.getByLabelText('Code')).toBeInTheDocument(); +}); ``` ### Mock remote data services (not a database) @@ -63,7 +63,7 @@ vi.mock('../../domain/factories', () => ({ update: vi.fn(), delete: vi.fn(), }, -})) +})); ``` ## Browser verification (`apps/web`) @@ -92,9 +92,11 @@ Do not add Playwright unless the user explicitly asks. **Command:** pnpm test ## Summary + - Total / passed / failed ## Failed + - File — assertion - Recommended fix ``` diff --git a/.cursor/agents/planner.md b/.cursor/agents/planner.md index 8d4ad52..b5aec1b 100644 --- a/.cursor/agents/planner.md +++ b/.cursor/agents/planner.md @@ -18,19 +18,23 @@ You are an expert planning specialist focused on creating comprehensive, actiona ## Planning Process ### 1. Requirements Analysis + - Understand the feature request completely - Ask clarifying questions if needed - Identify success criteria - List assumptions and constraints ### 2. Architecture Review + - Analyze existing codebase structure - Identify affected components - Review similar implementations - Consider reusable patterns ### 3. Step Breakdown + Create detailed steps with: + - Clear, specific actions - File paths and locations - Dependencies between steps @@ -38,6 +42,7 @@ Create detailed steps with: - Potential risks ### 4. Implementation Order + - Prioritize by dependencies - Group related changes - Minimize context switching @@ -49,20 +54,25 @@ Create detailed steps with: # Implementation Plan: [Feature Name] ## Overview + [2-3 sentence summary] ## Requirements + - [Requirement 1] - [Requirement 2] ## Architecture Changes + - [Change 1: file path and description] - [Change 2: file path and description] ## Implementation Steps ### Phase 1: [Phase Name] + 1. **[Step Name]** (File: path/to/file.ts) + - Action: Specific action to take - Why: Reason for this step - Dependencies: None / Requires step X @@ -72,18 +82,22 @@ Create detailed steps with: ... ### Phase 2: [Phase Name] + ... ## Testing Strategy + - Unit tests: [files to test] - Integration tests: [flows to test] - E2E tests: [user journeys to test] ## Risks & Mitigations + - **Risk**: [Description] - Mitigation: [How to address] ## Success Criteria + - [ ] Criterion 1 - [ ] Criterion 2 ``` diff --git a/.cursor/agents/refactor-cleaner.md b/.cursor/agents/refactor-cleaner.md index f9d5079..401d420 100644 --- a/.cursor/agents/refactor-cleaner.md +++ b/.cursor/agents/refactor-cleaner.md @@ -20,12 +20,14 @@ You are an expert refactoring specialist focused on code cleanup and consolidati ## Tools at Your Disposal ### Detection Tools + - **knip** - Find unused files, exports, dependencies, types - **depcheck** - Identify unused npm dependencies - **ts-prune** - Find unused TypeScript exports - **eslint** - Check for unused disable-directives and variables ### Analysis Commands + ```bash # Run knip for unused exports/files/dependencies npx knip @@ -43,6 +45,7 @@ npx eslint . --report-unused-disable-directives ## Refactoring Workflow ### 1. Analysis Phase + ``` a) Run detection tools in parallel b) Collect all findings @@ -53,6 +56,7 @@ c) Categorize by risk level: ``` ### 2. Risk Assessment + ``` For each item to remove: - Check if it's imported anywhere (grep search) @@ -63,6 +67,7 @@ For each item to remove: ``` ### 3. Safe Removal Process + ``` a) Start with SAFE items only b) Remove one category at a time: @@ -75,6 +80,7 @@ d) Create git commit for each batch ``` ### 4. Duplicate Consolidation + ``` a) Find duplicate components/utilities b) Choose the best implementation: @@ -96,28 +102,34 @@ Create/update `docs/DELETION_LOG.md` with this structure: ## [YYYY-MM-DD] Refactor Session ### Unused Dependencies Removed + - package-name@version - Last used: never, Size: XX KB - another-package@version - Replaced by: better-package ### Unused Files Deleted + - src/old-component.tsx - Replaced by: src/new-component.tsx - lib/deprecated-util.ts - Functionality moved to: lib/utils.ts ### Duplicate Code Consolidated + - src/components/Button1.tsx + Button2.tsx → Button.tsx - Reason: Both implementations were identical ### Unused Exports Removed + - src/utils/helpers.ts - Functions: foo(), bar() - Reason: No references found in codebase ### Impact + - Files deleted: 15 - Dependencies removed: 5 - Lines of code removed: 2,300 - Bundle size reduction: ~45 KB ### Testing + - All unit tests passing: ✓ - All integration tests passing: ✓ - Manual testing completed: ✓ @@ -126,6 +138,7 @@ Create/update `docs/DELETION_LOG.md` with this structure: ## Safety Checklist Before removing ANYTHING: + - [ ] Run detection tools - [ ] Grep for all references - [ ] Check dynamic imports @@ -136,6 +149,7 @@ Before removing ANYTHING: - [ ] Document in DELETION_LOG.md After each removal: + - [ ] Build succeeds - [ ] Tests pass - [ ] No console errors @@ -145,20 +159,22 @@ After each removal: ## Common Patterns to Remove ### 1. Unused Imports + ```typescript // ❌ Remove unused imports -import { useState, useEffect, useMemo } from 'react' // Only useState used +import { useState, useEffect, useMemo } from 'react'; // Only useState used // ✅ Keep only what's used -import { useState } from 'react' +import { useState } from 'react'; ``` ### 2. Dead Code Branches + ```typescript // ❌ Remove unreachable code if (false) { // This never executes - doSomething() + doSomething(); } // ❌ Remove unused functions @@ -168,6 +184,7 @@ export function unusedHelper() { ``` ### 3. Duplicate Components + ```typescript // ❌ Multiple similar components components/Button.tsx @@ -179,12 +196,13 @@ components/Button.tsx (with variant prop) ``` ### 4. Unused Dependencies + ```json // ❌ Package installed but not imported { "dependencies": { - "lodash": "^4.17.21", // Not used anywhere - "moment": "^2.29.4" // Replaced by date-fns + "lodash": "^4.17.21", // Not used anywhere + "moment": "^2.29.4" // Replaced by date-fns } } ``` @@ -192,6 +210,7 @@ components/Button.tsx (with variant prop) ## Example Project-Specific Rules **CRITICAL - NEVER REMOVE:** + - `apiClient` / `createHttpClient` wiring - `terminateAuthSession` / auth interceptors - `EnterpriseModuleProvider` and FULL_PAGE page providers @@ -199,6 +218,7 @@ components/Button.tsx (with variant prop) - Electron preload / IPC bridge **SAFE TO REMOVE:** + - Old unused components in components/ folder - Deprecated utility functions - Test files for deleted features @@ -206,6 +226,7 @@ components/Button.tsx (with variant prop) - Unused TypeScript types/interfaces **ALWAYS VERIFY:** + - Auth login + `terminateAuthSession` - `example/full-page` still routes and loads - `@repo/ui` exports used by web/showcase @@ -219,26 +240,31 @@ When opening PR with deletions: ## Refactor: Code Cleanup ### Summary + Dead code cleanup removing unused exports, dependencies, and duplicates. ### Changes + - Removed X unused files - Removed Y unused dependencies - Consolidated Z duplicate components - See docs/DELETION_LOG.md for details ### Testing + - [x] Build passes - [x] All tests pass - [x] Manual testing completed - [x] No console errors ### Impact + - Bundle size: -XX KB - Lines of code: -XXXX - Dependencies: -X packages ### Risk Level + 🟢 LOW - Only removed verifiably unused code See DELETION_LOG.md for complete details. @@ -249,6 +275,7 @@ See DELETION_LOG.md for complete details. If something breaks after removal: 1. **Immediate rollback:** + ```bash git revert HEAD pnpm install @@ -257,11 +284,13 @@ If something breaks after removal: ``` 2. **Investigate:** + - What failed? - Was it a dynamic import? - Was it used in a way detection tools missed? 3. **Fix forward:** + - Mark item as "DO NOT REMOVE" in notes - Document why detection tools missed it - Add explicit type annotations if needed @@ -293,6 +322,7 @@ If something breaks after removal: ## Success Metrics After cleanup session: + - ✅ All tests passing - ✅ Build succeeds - ✅ No console errors diff --git a/.cursor/agents/security-reviewer.md b/.cursor/agents/security-reviewer.md index c627381..6b51915 100644 --- a/.cursor/agents/security-reviewer.md +++ b/.cursor/agents/security-reviewer.md @@ -65,7 +65,10 @@ If CRITICAL: stop, fix, rotate any leaked secret, scan for the same pattern. ```markdown # Security Review + **Status:** CLEAR / ISSUES FOUND + ## Critical / High / Medium + - File:line — issue — fix ``` diff --git a/.cursor/agents/tdd-guide.md b/.cursor/agents/tdd-guide.md index c8909c2..71cf3fc 100644 --- a/.cursor/agents/tdd-guide.md +++ b/.cursor/agents/tdd-guide.md @@ -19,18 +19,18 @@ You are a Test-Driven Development (TDD) specialist. This repo uses **Vitest** (a ### Step 1: Write the test first (RED) ```typescript -import { describe, it, expect } from 'vitest' -import { createFullPageSchema } from './full-page.validator' +import { describe, it, expect } from 'vitest'; +import { createFullPageSchema } from './full-page.validator'; describe('createFullPageSchema', () => { - const t = (key: string) => key + const t = (key: string) => key; it('rejects an empty code', () => { - const schema = createFullPageSchema(t) - const result = schema.safeParse({ code: '', name: 'Widget' }) - expect(result.success).toBe(false) - }) -}) + const schema = createFullPageSchema(t); + const result = schema.safeParse({ code: '', name: 'Widget' }); + expect(result.success).toBe(false); + }); +}); ``` ### Step 2: Run it (must FAIL) @@ -47,7 +47,7 @@ export const createFullPageSchema = (t: (key: string) => string) => z.object({ code: compose(z.string(), required(t('common:fields.code'))), name: compose(z.string(), required(t('common:fields.name')), rangeLength(3, 50, t('common:fields.name'))), - }) + }); ``` ### Step 4: Run until green, then refactor. Coverage via `pnpm test` / `pnpm check:all`. @@ -65,7 +65,7 @@ Mock `@repo/core-api` and `apiClient` — not a database. ```ts vi.mock('@repo/core-api/http-client', () => ({ createHttpClient: () => ({ get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() }), -})) +})); ``` ## Edge cases you MUST test diff --git a/.cursor/commands/build-fix.md b/.cursor/commands/build-fix.md index 2045c5d..ae0a460 100644 --- a/.cursor/commands/build-fix.md +++ b/.cursor/commands/build-fix.md @@ -5,16 +5,19 @@ Incrementally fix TypeScript and build errors: 1. Run `pnpm typecheck` or `pnpm build` (this repo uses pnpm + Turbo + Vite) 2. Parse error output: + - Group by file - Sort by severity 3. For each error: + - Show 5 lines of context - Explain the issue - Apply a minimal fix - Re-run the failing command 4. Stop if: + - The fix introduces new errors - The same error persists after 3 attempts - The user asks to pause diff --git a/.cursor/commands/checkpoint.md b/.cursor/commands/checkpoint.md index b835a75..c69cd8d 100644 --- a/.cursor/commands/checkpoint.md +++ b/.cursor/commands/checkpoint.md @@ -26,12 +26,14 @@ When verifying against a checkpoint: 1. Read checkpoint from log 2. Compare current state to checkpoint: + - Files added since checkpoint - Files modified since checkpoint - Test pass rate now vs then - Coverage now vs then 3. Report: + ``` CHECKPOINT COMPARISON: $NAME ============================ @@ -44,6 +46,7 @@ Build: [PASS/FAIL] ## List Checkpoints Show all checkpoints with: + - Name - Timestamp - Git SHA @@ -68,6 +71,7 @@ Typical checkpoint flow: ## Arguments $ARGUMENTS: + - `create ` - Create named checkpoint - `verify ` - Verify against named checkpoint - `list` - Show all checkpoints diff --git a/.cursor/commands/code-review.md b/.cursor/commands/code-review.md index 4e5ef01..31bc66f 100644 --- a/.cursor/commands/code-review.md +++ b/.cursor/commands/code-review.md @@ -7,14 +7,16 @@ Comprehensive security and quality review of uncommitted changes: 2. For each changed file, check for: **Security Issues (CRITICAL):** + - Hardcoded credentials, API keys, tokens - SQL injection vulnerabilities -- XSS vulnerabilities +- XSS vulnerabilities - Missing input validation - Insecure dependencies - Path traversal risks **Code Quality (HIGH):** + - Functions > 50 lines - Files > 800 lines - Nesting depth > 4 levels @@ -24,12 +26,14 @@ Comprehensive security and quality review of uncommitted changes: - Missing JSDoc for public APIs **Best Practices (MEDIUM):** + - Mutation patterns (use immutable instead) - Emoji usage in code/comments - Missing tests for new code - Accessibility issues (a11y) 3. Generate report with: + - Severity: CRITICAL, HIGH, MEDIUM, LOW - File location and line numbers - Issue description diff --git a/.cursor/commands/eval.md b/.cursor/commands/eval.md index 1c788e9..b93d4d6 100644 --- a/.cursor/commands/eval.md +++ b/.cursor/commands/eval.md @@ -16,17 +16,21 @@ Create a new eval definition: ```markdown ## EVAL: feature-name + Created: $(date) ### Capability Evals + - [ ] [Description of capability 1] - [ ] [Description of capability 2] ### Regression Evals + - [ ] [Existing behavior 1 still works] - [ ] [Existing behavior 2 still works] ### Success Criteria + - pass@3 > 90% for capability evals - pass^3 = 100% for regression evals ``` @@ -113,6 +117,7 @@ feature-export [0/4 passing] NOT STARTED ## Arguments $ARGUMENTS: + - `define ` - Create new eval definition - `check ` - Run and check evals - `report ` - Generate full report diff --git a/.cursor/commands/learn.md b/.cursor/commands/learn.md index f563a2b..c70217b 100644 --- a/.cursor/commands/learn.md +++ b/.cursor/commands/learn.md @@ -11,17 +11,20 @@ Run `/learn` at any point during a session when you've solved a non-trivial prob Look for: 1. **Error Resolution Patterns** + - What error occurred? - What was the root cause? - What fixed it? - Is this reusable for similar errors? 2. **Debugging Techniques** + - Non-obvious debugging steps - Tool combinations that worked - Diagnostic patterns 3. **Workarounds** + - Library quirks - API limitations - Version-specific fixes @@ -42,15 +45,19 @@ Create a skill file at `.agents/skills/learned/[pattern-name].md`: **Context:** [Brief description of when this applies] ## Problem + [What problem this solves - be specific] ## Solution + [The pattern/technique/workaround] ## Example + [Code example if applicable] ## When to Use + [Trigger conditions - what should activate this skill] ``` diff --git a/.cursor/commands/orchestrate.md b/.cursor/commands/orchestrate.md index 30ac2b8..6a14cd2 100644 --- a/.cursor/commands/orchestrate.md +++ b/.cursor/commands/orchestrate.md @@ -9,25 +9,33 @@ Sequential agent workflow for complex tasks. ## Workflow Types ### feature + Full feature implementation workflow: + ``` planner -> tdd-guide -> code-reviewer -> security-reviewer ``` ### bugfix + Bug investigation and fix workflow: + ``` explorer -> tdd-guide -> code-reviewer ``` ### refactor + Safe refactoring workflow: + ``` architect -> code-reviewer -> tdd-guide ``` ### security + Security-focused review: + ``` security-reviewer -> code-reviewer -> architect ``` @@ -49,18 +57,23 @@ Between agents, create handoff document: ## HANDOFF: [previous-agent] -> [next-agent] ### Context + [Summary of what was done] ### Findings + [Key discoveries or decisions] ### Files Modified + [List of files touched] ### Open Questions + [Unresolved items for next agent] ### Recommendations + [Suggested next steps] ``` @@ -73,18 +86,21 @@ Between agents, create handoff document: Executes: 1. **Planner Agent** + - Analyzes requirements - Creates implementation plan - Identifies dependencies - Output: `HANDOFF: planner -> tdd-guide` 2. **TDD Guide Agent** + - Reads planner handoff - Writes tests first - Implements to pass tests - Output: `HANDOFF: tdd-guide -> code-reviewer` 3. **Code Reviewer Agent** + - Reviews implementation - Checks for issues - Suggests improvements @@ -139,18 +155,22 @@ For independent checks, run agents in parallel: ```markdown ### Parallel Phase + Run simultaneously: + - code-reviewer (quality) - security-reviewer (security) - architect (design) ### Merge Results + Combine outputs into single report ``` ## Arguments $ARGUMENTS: + - `feature ` - Full feature workflow - `bugfix ` - Bug fix workflow - `refactor ` - Refactoring workflow diff --git a/.cursor/commands/plan.md b/.cursor/commands/plan.md index 8cf076e..da9456b 100644 --- a/.cursor/commands/plan.md +++ b/.cursor/commands/plan.md @@ -16,6 +16,7 @@ This command invokes the **planner** agent to create a comprehensive implementat ## When to Use Use `/plan` when: + - Starting a new feature - Making significant architectural changes - Working on complex refactoring @@ -82,6 +83,7 @@ Agent (planner): **CRITICAL**: The planner agent will **NOT** write any code until you explicitly confirm the plan with "yes" or "proceed" or similar affirmative response. If you want changes, respond with: + - "modify: [your changes]" - "different approach: [alternative]" - "skip phase 2 and do phase 3 first" @@ -89,6 +91,7 @@ If you want changes, respond with: ## Integration with Other Commands After planning: + - Use `/tdd` to implement with test-driven development - Use `/build-and-fix` if build errors occur - Use `/code-review` to review completed implementation diff --git a/.cursor/commands/refactor-clean.md b/.cursor/commands/refactor-clean.md index 6f5e250..5b104a5 100644 --- a/.cursor/commands/refactor-clean.md +++ b/.cursor/commands/refactor-clean.md @@ -3,6 +3,7 @@ Safely identify and remove dead code with test verification: 1. Run dead code analysis tools: + - knip: Find unused exports and files - depcheck: Find unused dependencies - ts-prune: Find unused TypeScript exports @@ -10,6 +11,7 @@ Safely identify and remove dead code with test verification: 2. Generate comprehensive report in .reports/dead-code-analysis.md 3. Categorize findings by severity: + - SAFE: Test files, unused utilities - CAUTION: API routes, components - DANGER: Config files, main entry points @@ -17,6 +19,7 @@ Safely identify and remove dead code with test verification: 4. Propose safe deletions only 5. Before each deletion: + - Run full test suite - Verify tests pass - Apply change diff --git a/.cursor/commands/setup-pm.md b/.cursor/commands/setup-pm.md index fc40f01..1ca4e3a 100644 --- a/.cursor/commands/setup-pm.md +++ b/.cursor/commands/setup-pm.md @@ -37,6 +37,7 @@ When determining which package manager to use, the following order is checked: ## Configuration Files ### Global Configuration + ```json // ~/.cursor/package-manager.json { @@ -45,6 +46,7 @@ When determining which package manager to use, the following order is checked: ``` ### Project Configuration + ```json // .cursor/package-manager.json { @@ -53,6 +55,7 @@ When determining which package manager to use, the following order is checked: ``` ### package.json + ```json { "packageManager": "pnpm@8.6.0" diff --git a/.cursor/commands/tdd.md b/.cursor/commands/tdd.md index 8bb8a66..c645074 100644 --- a/.cursor/commands/tdd.md +++ b/.cursor/commands/tdd.md @@ -34,12 +34,12 @@ User: /tdd Add validation for the full-page name field ```typescript // full-page.validator.test.ts -import { createFullPageSchema } from './full-page.validator' +import { createFullPageSchema } from './full-page.validator'; it('rejects an empty name', () => { - const schema = createFullPageSchema((k) => k) - expect(schema.safeParse({ code: 'ABC', name: '' }).success).toBe(false) -}) + const schema = createFullPageSchema((k) => k); + expect(schema.safeParse({ code: 'ABC', name: '' }).success).toBe(false); +}); ``` ```bash diff --git a/.cursor/commands/test-coverage.md b/.cursor/commands/test-coverage.md index 2d91d48..df3b26c 100644 --- a/.cursor/commands/test-coverage.md +++ b/.cursor/commands/test-coverage.md @@ -7,6 +7,7 @@ Analyze Vitest coverage and add missing tests: 2. Identify files below 80% 3. For each under-covered file: + - Unit tests for validators, transformers, utils, stores - Testing Library tests for `@repo/ui` components - Journey tests for critical `apps/web` flows (mock data services) @@ -16,6 +17,7 @@ Analyze Vitest coverage and add missing tests: 5. Show before/after coverage Focus on: + - Happy path - Error handling - Edge cases (null, undefined, empty) diff --git a/.cursor/commands/update-docs.md b/.cursor/commands/update-docs.md index 79863ed..eb9dd00 100644 --- a/.cursor/commands/update-docs.md +++ b/.cursor/commands/update-docs.md @@ -3,12 +3,15 @@ Sync docs with this frontend monorepo. Source of truth: `package.json`, `apps/web/.env.example`, and `apps/docs-dev` (VitePress). 1. Read root `package.json` scripts + - Table of `pnpm dev:web`, `pnpm dev:showcase`, `pnpm dev:docs-dev`, `pnpm typecheck:web`, `pnpm test`, `pnpm check:all` 2. Read `apps/web/.env.example` + - Document each `VITE_*` var (they are public to the client) 3. Update `apps/docs-dev` (VitePress) and the root README + - Where to work (`apps/web` vs `showcase`) - Module layout (`example/full-page`) - Preferred `@repo/*` imports diff --git a/.cursor/commands/verify.md b/.cursor/commands/verify.md index 5f628b1..d8f0286 100644 --- a/.cursor/commands/verify.md +++ b/.cursor/commands/verify.md @@ -7,23 +7,28 @@ Run comprehensive verification on current codebase state. Execute verification in this exact order: 1. **Build Check** + - Run the build command for this project - If it fails, report errors and STOP 2. **Type Check** + - Run TypeScript/type checker - Report all errors with file:line 3. **Lint Check** + - Run linter - Report warnings and errors 4. **Test Suite** + - Run all tests - Report pass/fail count - Report coverage percentage 5. **Console.log Audit** + - Search for console.log in source files - Report locations @@ -53,6 +58,7 @@ If any critical issues, list them with fix suggestions. ## Arguments $ARGUMENTS can be: + - `quick` - Only build + types - `full` - All checks (default) - `pre-commit` - Checks relevant for commits diff --git a/.cursor/contexts/dev.md b/.cursor/contexts/dev.md index 28b64ab..73c6fc4 100644 --- a/.cursor/contexts/dev.md +++ b/.cursor/contexts/dev.md @@ -4,17 +4,20 @@ Mode: Active development Focus: Implementation, coding, building features ## Behavior + - Write code first, explain after - Prefer working solutions over perfect solutions - Run tests after changes - Keep commits atomic ## Priorities + 1. Get it working 2. Get it right 3. Get it clean ## Tools to favor + - Edit, Write for code changes - Bash for running tests/builds - Grep, Glob for finding code diff --git a/.cursor/contexts/research.md b/.cursor/contexts/research.md index a298194..2aa03d1 100644 --- a/.cursor/contexts/research.md +++ b/.cursor/contexts/research.md @@ -4,12 +4,14 @@ Mode: Exploration, investigation, learning Focus: Understanding before acting ## Behavior + - Read widely before concluding - Ask clarifying questions - Document findings as you go - Don't write code until understanding is clear ## Research Process + 1. Understand the question 2. Explore relevant code/docs 3. Form hypothesis @@ -17,10 +19,12 @@ Focus: Understanding before acting 5. Summarize findings ## Tools to favor + - Read for understanding code - Grep, Glob for finding patterns - WebSearch, WebFetch for external docs - Task with Explore agent for codebase questions ## Output + Findings first, recommendations second diff --git a/.cursor/contexts/review.md b/.cursor/contexts/review.md index fce643d..9c819d9 100644 --- a/.cursor/contexts/review.md +++ b/.cursor/contexts/review.md @@ -4,12 +4,14 @@ Mode: PR review, code analysis Focus: Quality, security, maintainability ## Behavior + - Read thoroughly before commenting - Prioritize issues by severity (critical > high > medium > low) - Suggest fixes, don't just point out problems - Check for security vulnerabilities ## Review Checklist + - [ ] Logic errors - [ ] Edge cases - [ ] Error handling @@ -19,4 +21,5 @@ Focus: Quality, security, maintainability - [ ] Test coverage ## Output Format + Group findings by file, severity first diff --git a/.gitignore b/.gitignore index 5e0724f..1f484b1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # Dependencies node_modules .pnpm-store +.cursor/sessions/* # Turborepo & Cache .turbo diff --git a/apps/docs-dev/src/apps/desktop/AUTO_UPDATER.md b/apps/docs-dev/src/apps/desktop/AUTO_UPDATER.md index a7ee902..9a605e4 100644 --- a/apps/docs-dev/src/apps/desktop/AUTO_UPDATER.md +++ b/apps/docs-dev/src/apps/desktop/AUTO_UPDATER.md @@ -1,4 +1,3 @@ - # Desktop Auto-Update System > **Architectural Foundation:** [electron-updater](https://www.npmjs.com/package/electron-updater) · [electron-builder](https://www.electron.build/) · [GitHub Actions](https://docs.github.com/en/actions) @@ -113,8 +112,7 @@ pnpm run prebuild GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml ``` -> [!CAUTION] -> **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault. +> [!CAUTION] > **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault. ### Automated Release (GitHub Actions) @@ -232,8 +230,7 @@ publish: Your server must host the same directory structure as the S3 layout above. -> [!IMPORTANT] -> **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures. +> [!IMPORTANT] > **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures. **Nginx reference:** @@ -260,8 +257,7 @@ server { ## 🛡️ Code Signing: The Trust Boundary -> [!WARNING] -> **Code signing is not merely a requirement — it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**. +> [!WARNING] > **Code signing is not merely a requirement — it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**. ### macOS diff --git a/apps/docs-dev/src/apps/desktop/CONFIGURATION.md b/apps/docs-dev/src/apps/desktop/CONFIGURATION.md index 2bb648a..e70bd20 100644 --- a/apps/docs-dev/src/apps/desktop/CONFIGURATION.md +++ b/apps/docs-dev/src/apps/desktop/CONFIGURATION.md @@ -1,4 +1,3 @@ - # Desktop Configuration Guide > **Architectural Foundation:** [Electron Protocol API](https://www.electronjs.org/docs/latest/api/protocol) · [electron-builder](https://www.electron.build/) · [React Router](https://reactrouter.com/) diff --git a/apps/docs-dev/src/apps/desktop/IPC_ARCHITECTURE.md b/apps/docs-dev/src/apps/desktop/IPC_ARCHITECTURE.md index b857148..623626d 100644 --- a/apps/docs-dev/src/apps/desktop/IPC_ARCHITECTURE.md +++ b/apps/docs-dev/src/apps/desktop/IPC_ARCHITECTURE.md @@ -8,7 +8,6 @@ outline: [2, 3] > > **Description:** Hardened IPC security model enforcing privilege separation via contextBridge, defining the Three-Step Bridge SOP for native feature exposure, the verified channel manifest, and critical anti-pattern audit checklist. - > **Scope**: [Electron](https://www.electronjs.org/) Main ↔ Renderer process communication > > **Enforcement Level**: Mandatory — deviations constitute security violations @@ -17,10 +16,10 @@ This document defines the **hardened security perimeter** and communication topo The architecture operates on three invariants: -| Invariant | Guarantee | -|---|---| -| **Context Encapsulation** | The Preload Script executes in a hermetically sealed V8 context, isolated from both Main Process globals and the Renderer DOM. | -| **Interface Narrowing** | Only explicitly declared, type-safe API surfaces are exposed via `contextBridge`. No wildcard access patterns exist. | +| Invariant | Guarantee | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Context Encapsulation** | The Preload Script executes in a hermetically sealed V8 context, isolated from both Main Process globals and the Renderer DOM. | +| **Interface Narrowing** | Only explicitly declared, type-safe API surfaces are exposed via `contextBridge`. No wildcard access patterns exist. | | **Deterministic Lifecycle** | All IPC subscriptions are paired with unsubscribe functions, tying native event listeners to React's component lifecycle to prevent memory leaks. | --- @@ -100,12 +99,12 @@ The Preload Script functions as a **Secure Gateway** that performs **Interface N These settings are declared in `BrowserWindow.webPreferences` and are **non-negotiable**: -| Setting | Value | Enforcement | -| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. | -| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. | -| `sandbox` | `true` | The renderer process runs inside a **[Chromium OS-level sandbox](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/design/sandbox.md)**, restricting system calls and file access at the kernel level. | -| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. | +| Setting | Value | Enforcement | +| ------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. | +| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. | +| `sandbox` | `true` | The renderer process runs inside a **[Chromium OS-level sandbox](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/design/sandbox.md)**, restricting system calls and file access at the kernel level. | +| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. | --- @@ -113,8 +112,7 @@ These settings are declared in `BrowserWindow.webPreferences` and are **non-nego Every native feature in this architecture **must** follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface. -> [!IMPORTANT] -> **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production). +> [!IMPORTANT] > **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production). ### Step 1: Register the Handler — Main Process diff --git a/apps/docs-dev/src/apps/desktop/index.md b/apps/docs-dev/src/apps/desktop/index.md index c976948..b8de77c 100644 --- a/apps/docs-dev/src/apps/desktop/index.md +++ b/apps/docs-dev/src/apps/desktop/index.md @@ -1,4 +1,5 @@ # Desktop + > **Architectural Foundation:** [Electron](https://www.electronjs.org/) · [electron-vite](https://electron-vite.org/) · [electron-builder](https://www.electron.build/) · [electron-updater](https://www.npmjs.com/package/electron-updater) > > **Description:** Secure Electron desktop wrapper that embeds monorepo web applications, providing custom app:// protocol routing, hardware IPC bridge, auto-updates, and CORS bypass proxy with hardened security defaults. @@ -31,10 +32,10 @@ pnpm package:desktop ## How It Works -| Environment | Operational Logic | -|---|---| -| **Development** | Bridges the Electron shell with the Vite Dev Server, enabling Hot Module Replacement (HMR) and real-time UI synchronization at `http://localhost:5173`. | -| **Production** | Orchestrates a Secure Custom Protocol (`app://`) to serve optimized static assets, ensuring seamless SPA client-side routing via an intelligent `index.html` fallback mechanism. | +| Environment | Operational Logic | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Development** | Bridges the Electron shell with the Vite Dev Server, enabling Hot Module Replacement (HMR) and real-time UI synchronization at `http://localhost:5173`. | +| **Production** | Orchestrates a Secure Custom Protocol (`app://`) to serve optimized static assets, ensuring seamless SPA client-side routing via an intelligent `index.html` fallback mechanism. | --- @@ -71,33 +72,32 @@ apps/desktop/ ### Development & Build -| Script | Description | -|---|---| -| `pnpm dev` | Launch the electron-vite development server with live reload | -| `pnpm build` | Compile main, preload, and renderer TypeScript modules → `out/` | +| Script | Description | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pnpm dev` | Launch the electron-vite development server with live reload | +| `pnpm build` | Compile main, preload, and renderer TypeScript modules → `out/` | | `pnpm prebuild` | Synchronize the target web app's build output via `scripts/copy-web-dist.ts` — copies `apps//dist/` → `web-dist/`. Invoked automatically before `pnpm build`. | -| `pnpm preview` | Preview the compiled Electron app locally without generating a distributable | +| `pnpm preview` | Preview the compiled Electron app locally without generating a distributable | ### 🚀 Packaging & Distribution To generate a production-ready installer, execute from **within `apps/desktop/`** or use the root-level `pnpm package:*` commands, which orchestrate the full pipeline automatically: -| Command | Platform | Output Artifact | -|---|---|---| -| `pnpm package` | Current OS | Detects host OS and builds accordingly | -| `pnpm package:mac` | macOS | `.dmg` and `.zip` (supports x64 & arm64) | -| `pnpm package:win` | Windows | `.exe` (NSIS Installer) | -| `pnpm package:linux` | Linux | `.AppImage` | +| Command | Platform | Output Artifact | +| -------------------- | ---------- | ---------------------------------------- | +| `pnpm package` | Current OS | Detects host OS and builds accordingly | +| `pnpm package:mac` | macOS | `.dmg` and `.zip` (supports x64 & arm64) | +| `pnpm package:win` | Windows | `.exe` (NSIS Installer) | +| `pnpm package:linux` | Linux | `.AppImage` | All artifacts are emitted to the `release/` directory. -> [!IMPORTANT] -> **Deterministic Build Pipeline**: All `package:*` commands strictly enforce a deterministic build pipeline: compiling web assets via Turborepo, synchronizing the output via the `prebuild` bridge (`node --import tsx scripts/copy-web-dist.ts`), and finally generating the native binary through `electron-builder`. +> [!IMPORTANT] > **Deterministic Build Pipeline**: All `package:*` commands strictly enforce a deterministic build pipeline: compiling web assets via Turborepo, synchronizing the output via the `prebuild` bridge (`node --import tsx scripts/copy-web-dist.ts`), and finally generating the native binary through `electron-builder`. > > **Running locally within `apps/desktop/`**: These scripts assume the web app has already been compiled. Either run `pnpm build --filter=web` beforehand, or use the root-level `pnpm package:*` commands which handle the complete orchestration. -> [!WARNING] -> **macOS Code Signing**: Distributable macOS builds with Auto-Update capability **require** an Apple Developer Certificate. Provide the following environment variables: +> [!WARNING] > **macOS Code Signing**: Distributable macOS builds with Auto-Update capability **require** an Apple Developer Certificate. Provide the following environment variables: +> > ```bash > CSC_LINK= > CSC_KEY_PASSWORD= @@ -105,10 +105,11 @@ All artifacts are emitted to the `release/` directory. > APPLE_APP_SPECIFIC_PASSWORD= > APPLE_TEAM_ID= > ``` +> > Without valid code signing, macOS Gatekeeper will quarantine the application and `electron-updater` will reject update payloads. See [AUTO_UPDATER.md](./AUTO_UPDATER.md) for the complete requirements. -> [!NOTE] -> **Cross-Compilation Advisory**: It is strongly recommended to build for each platform on its native OS. Cross-compilation (e.g., producing `.dmg` on Linux) may fail due to platform-specific toolchain dependencies. For CI, leverage a matrix strategy: +> [!NOTE] > **Cross-Compilation Advisory**: It is strongly recommended to build for each platform on its native OS. Cross-compilation (e.g., producing `.dmg` on Linux) may fail due to platform-specific toolchain dependencies. For CI, leverage a matrix strategy: +> > ```yaml > strategy: > matrix: @@ -162,12 +163,12 @@ The application enforces a **single running instance** via `app.requestSingleIns The Desktop Wrapper enforces a **hardened security perimeter**, strictly isolating the Node.js Main Process from the Renderer Context. Our architecture is built upon the principle of **Least Privilege**, ensuring that the web application only interacts with system hardware through a verified, secure IPC bridge. -| Setting | Value | Purpose | -|---|---|---| -| `contextIsolation` | `true` | Preload executes in a hermetically sealed JavaScript context | -| `nodeIntegration` | `false` | Zero Node.js API surface exposed to the renderer | -| `sandbox` | `true` | Chromium OS-level sandbox enforced | -| `webSecurity` | `true` | Same-origin policy strictly upheld | +| Setting | Value | Purpose | +| ------------------ | ------- | ------------------------------------------------------------ | +| `contextIsolation` | `true` | Preload executes in a hermetically sealed JavaScript context | +| `nodeIntegration` | `false` | Zero Node.js API surface exposed to the renderer | +| `sandbox` | `true` | Chromium OS-level sandbox enforced | +| `webSecurity` | `true` | Same-origin policy strictly upheld | **Defense-in-depth protections in `src/main/index.ts`**: @@ -182,8 +183,8 @@ See [IPC_ARCHITECTURE.md](./IPC_ARCHITECTURE.md) for the full security model, th ## Documentation -| Document | Scope | -|---|---| -| [CONFIGURATION.md](./CONFIGURATION.md) | Target app switching, `app://` protocol internals, HashRouter fallback procedure | -| [AUTO_UPDATER.md](./AUTO_UPDATER.md) | Release lifecycle, CI/CD variables, provider switching, code signing | +| Document | Scope | +| -------------------------------------------- | ------------------------------------------------------------------------------------- | +| [CONFIGURATION.md](./CONFIGURATION.md) | Target app switching, `app://` protocol internals, HashRouter fallback procedure | +| [AUTO_UPDATER.md](./AUTO_UPDATER.md) | Release lifecycle, CI/CD variables, provider switching, code signing | | [IPC_ARCHITECTURE.md](./IPC_ARCHITECTURE.md) | Security model, Three-Step Bridge pattern, existing IPC channels, extensibility guide | diff --git a/apps/docs-dev/src/index.md b/apps/docs-dev/src/index.md index fbb548b..fa968d1 100644 --- a/apps/docs-dev/src/index.md +++ b/apps/docs-dev/src/index.md @@ -2,9 +2,9 @@ layout: home hero: - name: "Frontend Architecture" - text: "Enterprise Monorepo" - tagline: "A scalable, standardized foundation for Web & Desktop applications. Built for performance, consistency, and velocity." + name: 'Frontend Architecture' + text: 'Enterprise Monorepo' + tagline: 'A scalable, standardized foundation for Web & Desktop applications. Built for performance, consistency, and velocity.' actions: - theme: brand text: Get Started @@ -31,6 +31,7 @@ features: link: /packages/core-api/ linkText: Explore Core --- +
@@ -262,4 +263,4 @@ features: padding: 24px; } } - \ No newline at end of file + diff --git a/apps/docs-dev/src/overview.md b/apps/docs-dev/src/overview.md index 0c7b0bd..fb4866c 100644 --- a/apps/docs-dev/src/overview.md +++ b/apps/docs-dev/src/overview.md @@ -34,140 +34,155 @@ The monorepo is organized into **Apps** (deployable applications) and **Packages ## 📦 Packages Overview ### 1. `apps/web` + The main consumer-facing application. -* Imports business logic from `@repo/utils` -* Uses shared UI components from `@repo/ui` +- Imports business logic from `@repo/utils` +- Uses shared UI components from `@repo/ui` **Tech Stack**: -* [React](https://react.dev/) -* [Vite](https://vite.dev/) -* [TypeScript](https://www.typescriptlang.org/) -* [Tailwind CSS](https://tailwindcss.com/) +- [React](https://react.dev/) +- [Vite](https://vite.dev/) +- [TypeScript](https://www.typescriptlang.org/) +- [Tailwind CSS](https://tailwindcss.com/) ### 2. `apps/desktop` + The **Electron desktop wrapper** that embeds `apps/web` for native desktop experiences. -* In **development**: loads the Vite dev server with full hot reload -* In **production**: serves the static web build via a secure custom `app://` protocol -* Configurable target app via `.env` (can wrap `apps/web`, `apps/docs-dev`, or any future app) +- In **development**: loads the Vite dev server with full hot reload +- In **production**: serves the static web build via a secure custom `app://` protocol +- Configurable target app via `.env` (can wrap `apps/web`, `apps/docs-dev`, or any future app) **Tech Stack**: -* [Electron](https://www.electronjs.org/) 33.x -* [electron-vite](https://electron-vite.org/) -* [electron-builder](https://www.electron.build/) -* [electron-updater](https://www.npmjs.com/package/electron-updater) +- [Electron](https://www.electronjs.org/) 33.x +- [electron-vite](https://electron-vite.org/) +- [electron-builder](https://www.electron.build/) +- [electron-updater](https://www.npmjs.com/package/electron-updater) **Key Capabilities**: -| Feature | Description | -|---|---| -| 🖨️ Native Printing | Silent and direct printing via secure IPC bridge | -| 🔄 Auto-Updates | Background downloads via GitHub Releases (switchable to S3) | -| 🔒 Secure IPC Bridge | `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true` | -| 🌐 Custom Protocol | `app://` serves static files with SPA routing fallback to `index.html` | -| 🛡️ CORS Bypass | Transparent Origin header rewriting for cloud API calls | +| Feature | Description | +| -------------------- | ---------------------------------------------------------------------- | +| 🖨️ Native Printing | Silent and direct printing via secure IPC bridge | +| 🔄 Auto-Updates | Background downloads via GitHub Releases (switchable to S3) | +| 🔒 Secure IPC Bridge | `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true` | +| 🌐 Custom Protocol | `app://` serves static files with SPA routing fallback to `index.html` | +| 🛡️ CORS Bypass | Transparent Origin header rewriting for cloud API calls | ### 3. `apps/landing` + The **public promotional website** — a standalone SPA for the company profile and marketing pages. -* Deployed independently to the web (e.g., [Vercel](https://vercel.com/)) — no interaction with Electron -* Consumes shared UI components from `@repo/ui` and utilities from `@repo/utils` -* Locked to port **3000** (`strictPort: true`) — evacuated from the `517x` range to avoid `electron-vite` port collisions +- Deployed independently to the web (e.g., [Vercel](https://vercel.com/)) — no interaction with Electron +- Consumes shared UI components from `@repo/ui` and utilities from `@repo/utils` +- Locked to port **3000** (`strictPort: true`) — evacuated from the `517x` range to avoid `electron-vite` port collisions **Tech Stack**: -* [React](https://react.dev/) -* [Vite](https://vite.dev/) -* [TypeScript](https://www.typescriptlang.org/) -* [Tailwind CSS](https://tailwindcss.com/) v4 +- [React](https://react.dev/) +- [Vite](https://vite.dev/) +- [TypeScript](https://www.typescriptlang.org/) +- [Tailwind CSS](https://tailwindcss.com/) v4 ### 4. `apps/docs-dev` + An isolated environment for developing and documenting UI components. -* Ensures components in `@repo/ui` are built and tested independently -* Acts as a living design system and playground -* Built with **[VitePress](https://vitepress.dev/)** + +- Ensures components in `@repo/ui` are built and tested independently +- Acts as a living design system and playground +- Built with **[VitePress](https://vitepress.dev/)** ### 5. `packages/core-api` + The **platform-agnostic API engine** for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline ([Grafana Faro](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/faro-web-sdk/) + [OpenTelemetry](https://opentelemetry.io/)), and a generic data services engine. -* Consumed by `apps/web`, `apps/landing`, and any future workspace -* Centralizes all `@grafana/faro-*` and `@opentelemetry/*` dependencies -* Provides plug-and-play telemetry via `initTelemetry()` + `faroAdapter` +- Consumed by `apps/web`, `apps/landing`, and any future workspace +- Centralizes all `@grafana/faro-*` and `@opentelemetry/*` dependencies +- Provides plug-and-play telemetry via `initTelemetry()` + `faroAdapter` **Tech Stack**: -* [Axios](https://axios-http.com/) (isolated instances, zero singleton pollution) -* [Grafana Faro](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/faro-web-sdk/) (RUM, Logs, Error tracking) -* [OpenTelemetry](https://opentelemetry.io/) (custom spans, distributed tracing) -* [TypeScript](https://www.typescriptlang.org/) (strict types, module augmentation) +- [Axios](https://axios-http.com/) (isolated instances, zero singleton pollution) +- [Grafana Faro](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/faro-web-sdk/) (RUM, Logs, Error tracking) +- [OpenTelemetry](https://opentelemetry.io/) (custom spans, distributed tracing) +- [TypeScript](https://www.typescriptlang.org/) (strict types, module augmentation) **Key Capabilities**: -| Feature | Description | -|---|---| -| 🏭 HTTP Client Factory | `createHttpClient()` — per-app isolated Axios instances with interceptor hooks | -| 📡 Faro/Loki Baseline | Every request automatically pushes structured logs with `module.key` and `module.action` | -| 🎯 Custom Spans (Opt-In) | `telemetryContext.customSpanName` creates explicit OTel spans visible in Grafana Tempo | -| 🛡️ Error Normalization | `ApiError.fromAxiosError()` — structured, serializable error codes for all failure modes | -| 📦 Data Services Engine | `CommonRemoteDataServices` — full CRUD + lifecycle operations with zero boilerplate | +| Feature | Description | +| ------------------------ | ---------------------------------------------------------------------------------------- | +| 🏭 HTTP Client Factory | `createHttpClient()` — per-app isolated Axios instances with interceptor hooks | +| 📡 Faro/Loki Baseline | Every request automatically pushes structured logs with `module.key` and `module.action` | +| 🎯 Custom Spans (Opt-In) | `telemetryContext.customSpanName` creates explicit OTel spans visible in Grafana Tempo | +| 🛡️ Error Normalization | `ApiError.fromAxiosError()` — structured, serializable error codes for all failure modes | +| 📦 Data Services Engine | `CommonRemoteDataServices` — full CRUD + lifecycle operations with zero boilerplate | ### 6. `packages/core-storage` + The **Enterprise-grade storage engine** for the monorepo. Provides a unified, Promise-based interface for interacting with browser storage (`localStorage` and `IndexedDB`). Enforces strict type safety, prevents key collisions via a centralized registry, and automatically provides **AES encryption at rest** for sensitive payloads using `@repo/utils`. ### 7. `packages/core-i18n` + The **Enterprise Internationalization Architecture** for the monorepo. Provides a Hybrid Namespace Architecture combining a centralized i18n engine with decentralized, lazy-loaded feature dictionaries. Features strict TypeScript typings (including nested keys), optional backend synchronization with automatic error rollbacks, and a deep-merge mechanism for dynamic tenant-specific vocabulary overrides. **Key Capabilities**: -| Feature | Description | -|---|---| -| 🌐 Hybrid Namespaces | Centralized `common` corpus + lazy-loaded feature dictionaries. | -| 🛡️ Strict Typings | Native TS autocomplete for nested paths (e.g., `header.title`) via module augmentation. | -| 🔄 Safe Backend Sync | `changeLanguage` accepts a `syncCallback` with built-in rollback if the API fails. | -| 🏢 Tenant Overrides | `applyTenantOverrides` performs a partial deep-merge to selectively override terminology. | +| Feature | Description | +| -------------------- | ----------------------------------------------------------------------------------------- | +| 🌐 Hybrid Namespaces | Centralized `common` corpus + lazy-loaded feature dictionaries. | +| 🛡️ Strict Typings | Native TS autocomplete for nested paths (e.g., `header.title`) via module augmentation. | +| 🔄 Safe Backend Sync | `changeLanguage` accepts a `syncCallback` with built-in rollback if the API fails. | +| 🏢 Tenant Overrides | `applyTenantOverrides` performs a partial deep-merge to selectively override terminology. | ### 8. `packages/core-events` -The **decoupled Nervous System** for the monorepo. + +The **decoupled Nervous System** for the monorepo. Provides a highly performant, strictly typed Event Bus (Pub/Sub) powered by [`mitt`](https://www.npmjs.com/package/mitt). It allows independent modules to communicate seamlessly without tightly coupling their codebases or triggering expensive global React tree re-renders. **Key Capabilities**: -| Feature | Description | -|---|---| -| 🧩 Zero Coupling | Publishers and subscribers interact via blind events, eliminating direct module imports and circular dependencies. | +| Feature | Description | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| 🧩 Zero Coupling | Publishers and subscribers interact via blind events, eliminating direct module imports and circular dependencies. | | ⚡ Extreme Performance | Enables targeted DOM updates for high-frequency data streams (e.g., WebSockets) without re-rendering parent components. | -| 🧹 Memory Safety | Native `useAppEvent` hook automatically unsubscribes on component unmount, preventing SPA memory leaks. | -| 🛡️ Strict Contracts | Centralized `events.registry.ts` enforces payload shapes via TypeScript, ensuring cross-module data safety. | +| 🧹 Memory Safety | Native `useAppEvent` hook automatically unsubscribes on component unmount, preventing SPA memory leaks. | +| 🛡️ Strict Contracts | Centralized `events.registry.ts` enforces payload shapes via TypeScript, ensuring cross-module data safety. | ### 9. `packages/utils` + Shared business logic and reusable utility modules that can be consumed across multiple applications. Fully tested using [Vitest](https://vitest.dev/). This package is intended to hold non-UI, cross-cutting logic such as date/time handling, security helpers, and other common utilities. It is designed to be framework-agnostic, predictable, and easy to extend as the system evolves. ### 10. `packages/ui` + Shared UI component library (Buttons, Inputs, Cards, Layouts) with a comprehensive **Form UI Library**. -* Ensures consistent design across all applications -* Designed to be consumed by both web apps and Storybook -* **Form UI Library**: 22 RHF-connected [Mantine](https://mantine.dev/) form components with [Zod](https://zod.dev/) validation and i18n error translation, built via a `withRHF()` HOC factory with `useController` micro-subscriptions and `React.memo` optimization for ERP-scale forms +- Ensures consistent design across all applications +- Designed to be consumed by both web apps and Storybook +- **Form UI Library**: 22 RHF-connected [Mantine](https://mantine.dev/) form components with [Zod](https://zod.dev/) validation and i18n error translation, built via a `withRHF()` HOC factory with `useController` micro-subscriptions and `React.memo` optimization for ERP-scale forms ### 11. `packages/configs` + Single source of truth for tooling configuration. -* **eslint-config**: Shared [ESLint](https://eslint.org/) rules -* **typescript-config**: Shared `tsconfig.json` base configurations + +- **eslint-config**: Shared [ESLint](https://eslint.org/) rules +- **typescript-config**: Shared `tsconfig.json` base configurations ## ⚙️ Configuration & Environment ### Turborepo Caching + This repository uses **[Turborepo](https://turbo.build/repo) caching** for builds, tests, and other artifacts. To fully clean the workspace (dependencies, build outputs, and Turbo cache): + ```bash rm -rf node_modules **/*/node_modules .turbo **/*/.turbo dist **/*/dist out **/*/out web-dist **/*/web-dist release **/*/release ``` diff --git a/apps/docs-dev/src/packages/core-api/index.md b/apps/docs-dev/src/packages/core-api/index.md index 2590665..98b7e9c 100644 --- a/apps/docs-dev/src/packages/core-api/index.md +++ b/apps/docs-dev/src/packages/core-api/index.md @@ -11,6 +11,7 @@ The platform-agnostic API engine for the monorepo. Provides an isolated HTTP cli --- ## Architecture Overview + ```mermaid graph TD %% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ─── @@ -46,7 +47,7 @@ graph TD %% ─── Flow & Relationships ─── WEB & LAND & DESK ===>|instantiates| FACTORY WEB & LAND & DESK ===>|extends| COMMON - + COMMON --->|executes via| FACTORY FACTORY -.->|reports via| FARO FACTORY -.->|throws| API_ERR @@ -61,13 +62,13 @@ graph TD %% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ─── style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5 style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5 - + %% Nested subgraphs also need transparent backgrounds to prevent glaring white boxes in dark mode style HTTP fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5 style OBS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5 style DATA fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5 style ERRORS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5 - ``` +``` ### Data Flow Lifecycle @@ -102,7 +103,7 @@ sequenceDiagram H->>F: onRequestStart() (Log + Span) H->>A: hooks.onRequest() (Inject Token) A->>N: fetch/XHR - + alt Success (2xx) N-->>A: return Response A->>F: onRequestEnd() (Close Span) @@ -156,20 +157,20 @@ export const apiClient = createHttpClient( ### Configuration -| Property | Type | Default | Description | -|---|---|---|---| -| `baseURL` | `string` | *required* | Base URL for all requests | -| `timeout` | `number` | `15000` | Default request timeout (ms) | -| `defaultHeaders` | `Record` | `{}` | Headers applied to every request | -| `observability` | `IObservabilityAdapter` | `noopAdapter` | Observability adapter (Faro or no-op) | +| Property | Type | Default | Description | +| ---------------- | ------------------------ | ------------- | ------------------------------------- | +| `baseURL` | `string` | _required_ | Base URL for all requests | +| `timeout` | `number` | `15000` | Default request timeout (ms) | +| `defaultHeaders` | `Record` | `{}` | Headers applied to every request | +| `observability` | `IObservabilityAdapter` | `noopAdapter` | Observability adapter (Faro or no-op) | ### Interceptor Hooks -| Hook | Signature | Purpose | -|---|---|---| -| `onRequest` | `(config) => config` | Inject auth tokens, tenant headers | -| `onResponse` | `(response) => response` | Transform response shapes | -| `onResponseError` | `(error) => never` | App-specific error handling (e.g., 401 redirect) | +| Hook | Signature | Purpose | +| ----------------- | ------------------------ | ------------------------------------------------ | +| `onRequest` | `(config) => config` | Inject auth tokens, tenant headers | +| `onResponse` | `(response) => response` | Transform response shapes | +| `onResponseError` | `(error) => never` | App-specific error handling (e.g., 401 redirect) | --- @@ -179,13 +180,12 @@ export const apiClient = createHttpClient( The observability layer operates in two complementary modes: -| Mode | Activation | What it does | -|---|---|---| -| **Baseline** (always on) | Automatic | Pushes structured logs to Faro/Loki on every request with `module.key`, `module.action`, HTTP method, and URL | -| **Custom Span** (opt-in) | Via `telemetryContext.customSpanName` | Creates an explicit OTel span with custom tags, visible in [Grafana Tempo](https://grafana.com/oss/tempo/) | +| Mode | Activation | What it does | +| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| **Baseline** (always on) | Automatic | Pushes structured logs to Faro/Loki on every request with `module.key`, `module.action`, HTTP method, and URL | +| **Custom Span** (opt-in) | Via `telemetryContext.customSpanName` | Creates an explicit OTel span with custom tags, visible in [Grafana Tempo](https://grafana.com/oss/tempo/) | -> [!NOTE] -> `trace.getActiveSpan()` returns `undefined` inside Axios interceptors due to browser XHR/Fetch lifecycle race conditions with Faro's `TracingInstrumentation`. The adapter does **not** attempt to enrich auto-instrumented spans. HTTP span capture is handled entirely by `TracingInstrumentation` auto-instrumentation. +> [!NOTE] > `trace.getActiveSpan()` returns `undefined` inside Axios interceptors due to browser XHR/Fetch lifecycle race conditions with Faro's `TracingInstrumentation`. The adapter does **not** attempt to enrich auto-instrumented spans. HTTP span capture is handled entirely by `TracingInstrumentation` auto-instrumentation. ### Initialization @@ -206,34 +206,34 @@ initTelemetry({ ### `TelemetryConfig` -| Property | Type | Required | Description | -|---|---|---|---| -| `appName` | `string` | ✅ | Application name for Faro + OTel resource attributes | -| `appVersion` | `string` | ✅ | SemVer version | -| `telemetryUrl` | `string` | ✅ | Grafana Faro collector URL | -| `environment` | `string` | ✅ | Deployment environment (`production`, `staging`, `development`) | -| `otlpTraceUrl` | `string` | — | Separate OTLP trace endpoint for direct Tempo ingestion | -| `propagateTraceHeaderCorsUrls` | `Array` | — | CORS patterns for W3C trace context propagation (default: `[/.*/]`) | +| Property | Type | Required | Description | +| ------------------------------ | ------------------------- | -------- | ------------------------------------------------------------------- | +| `appName` | `string` | ✅ | Application name for Faro + OTel resource attributes | +| `appVersion` | `string` | ✅ | SemVer version | +| `telemetryUrl` | `string` | ✅ | Grafana Faro collector URL | +| `environment` | `string` | ✅ | Deployment environment (`production`, `staging`, `development`) | +| `otlpTraceUrl` | `string` | — | Separate OTLP trace endpoint for direct Tempo ingestion | +| `propagateTraceHeaderCorsUrls` | `Array` | — | CORS patterns for W3C trace context propagation (default: `[/.*/]`) | ### Audit Headers Every request dispatched through `BaseRemoteDataServices` automatically attaches two business audit headers: -| Header | Source | Purpose | -|---|---|---| -| `ex-module-key` | `DataServicesConfig.moduleKey` | Identifies the business module (e.g., `BOOKING`) | -| `ex-module-action` | `RequestDescriptor.action` | Identifies the operation (e.g., `READ`, `CREATE`) | +| Header | Source | Purpose | +| ------------------ | ------------------------------ | ------------------------------------------------- | +| `ex-module-key` | `DataServicesConfig.moduleKey` | Identifies the business module (e.g., `BOOKING`) | +| `ex-module-action` | `RequestDescriptor.action` | Identifies the operation (e.g., `READ`, `CREATE`) | These headers are extracted by the `faroAdapter` and included in all Faro `pushLog`, `pushError`, and `pushEvent` calls as top-level context — making them directly queryable in **LogQL ([Loki](https://grafana.com/oss/loki/))**. ### Span Safety Guarantees -| Guarantee | Mechanism | -|---|---| -| **No span leaks** | `safeEndSpan()` always closes the span and detaches the reference from config | -| **No double-close on retry** | Span reference is deleted from config after `span.end()` | -| **No error swallowing** | All adapter calls are wrapped in try-catch in `create-http-client.ts` | -| **No crash on timeout** | `null`/`undefined` config guards on all `error.config` access | +| Guarantee | Mechanism | +| ---------------------------- | ----------------------------------------------------------------------------- | +| **No span leaks** | `safeEndSpan()` always closes the span and detaches the reference from config | +| **No double-close on retry** | Span reference is deleted from config after `span.end()` | +| **No error swallowing** | All adapter calls are wrapped in try-catch in `create-http-client.ts` | +| **No crash on timeout** | `null`/`undefined` config guards on all `error.config` access | --- @@ -254,32 +254,29 @@ interface BookingEntity extends BaseEntity { status: 'pending' | 'confirmed' | 'cancelled'; } -export const bookingServices = new CommonRemoteDataServices( - apiClient, - { - apiUrl: '/bookings', - moduleKey: 'BOOKING', - }, -); +export const bookingServices = new CommonRemoteDataServices(apiClient, { + apiUrl: '/bookings', + moduleKey: 'BOOKING', +}); ``` ### Available Operations -| Method | HTTP | URL Template | Description | -|---|---|---|---| -| `getMany(config?)` | GET | `/bookings` | Fetch paginated list | -| `getOne(id, config?)` | GET | `/bookings/:id` | Fetch single entity | -| `create(data, config?)` | POST | `/bookings` | Create new entity | -| `edit(id, data, config?)` | PUT | `/bookings/:id` | Update entity | -| `delete(id, config?)` | DELETE | `/bookings/:id` | Delete entity | -| `batchDelete(ids, config?)` | DELETE | `/bookings/batch` | Delete multiple | -| `activate(id)` | PATCH | `/bookings/:id/activate` | Activate entity | -| `deactivate(id)` | PATCH | `/bookings/:id/deactivate` | Deactivate entity | -| `confirmProcessData(id)` | PATCH | `/bookings/:id/confirm-process-data` | Confirm data processing | -| `confirmProcessTransaction(id)` | PATCH | `/bookings/:id/confirm-process-transaction` | Confirm transaction | -| `cancelProcessTransaction(id)` | PATCH | `/bookings/:id/cancel-process-transaction` | Cancel transaction | -| `rollbackProcessTransaction(id)` | PATCH | `/bookings/:id/rollback-process-transaction` | Rollback transaction | -| `holdProcessTransaction(id)` | PATCH | `/bookings/:id/hold-process-transaction` | Hold transaction | +| Method | HTTP | URL Template | Description | +| -------------------------------- | ------ | -------------------------------------------- | ----------------------- | +| `getMany(config?)` | GET | `/bookings` | Fetch paginated list | +| `getOne(id, config?)` | GET | `/bookings/:id` | Fetch single entity | +| `create(data, config?)` | POST | `/bookings` | Create new entity | +| `edit(id, data, config?)` | PUT | `/bookings/:id` | Update entity | +| `delete(id, config?)` | DELETE | `/bookings/:id` | Delete entity | +| `batchDelete(ids, config?)` | DELETE | `/bookings/batch` | Delete multiple | +| `activate(id)` | PATCH | `/bookings/:id/activate` | Activate entity | +| `deactivate(id)` | PATCH | `/bookings/:id/deactivate` | Deactivate entity | +| `confirmProcessData(id)` | PATCH | `/bookings/:id/confirm-process-data` | Confirm data processing | +| `confirmProcessTransaction(id)` | PATCH | `/bookings/:id/confirm-process-transaction` | Confirm transaction | +| `cancelProcessTransaction(id)` | PATCH | `/bookings/:id/cancel-process-transaction` | Cancel transaction | +| `rollbackProcessTransaction(id)` | PATCH | `/bookings/:id/rollback-process-transaction` | Rollback transaction | +| `holdProcessTransaction(id)` | PATCH | `/bookings/:id/hold-process-transaction` | Hold transaction | All batch variants (`batchActivate`, `batchDeactivate`, etc.) are also available. @@ -308,8 +305,11 @@ import { initTelemetry } from '@repo/core-api/observability/setup'; initTelemetry({ appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web', appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0', - telemetryUrl: import.meta.env.VITE_FARO_URL || '[https://telemetry.eigen.co.id/collect](https://telemetry.eigen.co.id/collect)', - otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || '[https://telemetry.eigen.co.id/v1/traces](https://telemetry.eigen.co.id/v1/traces)', + telemetryUrl: + import.meta.env.VITE_FARO_URL || '[https://telemetry.eigen.co.id/collect](https://telemetry.eigen.co.id/collect)', + otlpTraceUrl: + import.meta.env.VITE_OTLP_TRACE_URL || + '[https://telemetry.eigen.co.id/v1/traces](https://telemetry.eigen.co.id/v1/traces)', environment: import.meta.env.VITE_ENV || 'development', }); @@ -345,10 +345,10 @@ export interface BookingEntity extends BaseEntity { totalAmount: number; } -export const bookingServices = new CommonRemoteDataServices( - apiClient, - { apiUrl: '/bookings', moduleKey: 'BOOKING' }, -); +export const bookingServices = new CommonRemoteDataServices(apiClient, { + apiUrl: '/bookings', + moduleKey: 'BOOKING', +}); ``` ### 4. Consume in a React Component @@ -425,11 +425,11 @@ await bookingServices.getMany({ ### What Happens at Each Stage -| Stage | Baseline (no telemetryContext) | With `customSpanName` | -|---|---|---| -| **Request Start** | Faro `pushLog` (DEBUG) with `module.key`, `module.action`, URL | + Creates OTel span with `http.method`, `http.url`, `custom.*` tags | -| **Request Success** | — | Closes span (OK). If `pushEventOnSuccess`, pushes Faro event | -| **Request Error** | Faro `pushError` + `pushLog` (ERROR) | + Closes span (ERROR), records exception | +| Stage | Baseline (no telemetryContext) | With `customSpanName` | +| ------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------- | +| **Request Start** | Faro `pushLog` (DEBUG) with `module.key`, `module.action`, URL | + Creates OTel span with `http.method`, `http.url`, `custom.*` tags | +| **Request Success** | — | Closes span (OK). If `pushEventOnSuccess`, pushes Faro event | +| **Request Error** | Faro `pushError` + `pushLog` (ERROR) | + Closes span (ERROR), records exception | --- @@ -444,10 +444,10 @@ try { await bookingServices.getOne('42'); } catch (err) { if (err instanceof ApiError) { - err.code; // ApiErrorCode.NOT_FOUND - err.status; // 404 + err.code; // ApiErrorCode.NOT_FOUND + err.status; // 404 err.message; // "Booking not found" - err.data; // Raw server response body + err.data; // Raw server response body err.toJSON(); // Serializable for logging } } @@ -455,25 +455,25 @@ try { ### Error Codes -| Code | HTTP Status | Description | -|---|---|---| -| `BAD_REQUEST` | 400 | Invalid request parameters | -| `UNAUTHORIZED` | 401 | Missing or expired token | -| `FORBIDDEN` | 403 | Insufficient permissions | -| `NOT_FOUND` | 404 | Resource not found | -| `TIMEOUT` | — | Request timed out (`ECONNABORTED`) | -| `CANCELLED` | — | Request was cancelled (`ERR_CANCELED`) | -| `NETWORK_ERROR` | — | No response received | -| `SERVER_ERROR` | 500+ | Internal server error | +| Code | HTTP Status | Description | +| --------------- | ----------- | -------------------------------------- | +| `BAD_REQUEST` | 400 | Invalid request parameters | +| `UNAUTHORIZED` | 401 | Missing or expired token | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `NOT_FOUND` | 404 | Resource not found | +| `TIMEOUT` | — | Request timed out (`ECONNABORTED`) | +| `CANCELLED` | — | Request was cancelled (`ERR_CANCELED`) | +| `NETWORK_ERROR` | — | No response received | +| `SERVER_ERROR` | 500+ | Internal server error | --- ## Package Exports -| Import Path | Contents | -|---|---| -| `@repo/core-api/http-client` | `createHttpClient`, `ApiResponse`, `TelemetryContext`, Axios type re-exports | -| `@repo/core-api/observability` | `faroAdapter`, `noopObservabilityAdapter`, `IObservabilityAdapter`, `initTelemetry`, `getFaro`, `TelemetryConfig` | -| `@repo/core-api/observability/setup` | `initTelemetry`, `getFaro`, `TelemetryConfig` | -| `@repo/core-api/data-services` | `BaseRemoteDataServices`, `CommonRemoteDataServices`, types, constants | -| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` | \ No newline at end of file +| Import Path | Contents | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | +| `@repo/core-api/http-client` | `createHttpClient`, `ApiResponse`, `TelemetryContext`, Axios type re-exports | +| `@repo/core-api/observability` | `faroAdapter`, `noopObservabilityAdapter`, `IObservabilityAdapter`, `initTelemetry`, `getFaro`, `TelemetryConfig` | +| `@repo/core-api/observability/setup` | `initTelemetry`, `getFaro`, `TelemetryConfig` | +| `@repo/core-api/data-services` | `BaseRemoteDataServices`, `CommonRemoteDataServices`, types, constants | +| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` | diff --git a/apps/docs-dev/src/packages/core-api/transformers.md b/apps/docs-dev/src/packages/core-api/transformers.md index 9a1b1a7..4d0a95e 100644 --- a/apps/docs-dev/src/packages/core-api/transformers.md +++ b/apps/docs-dev/src/packages/core-api/transformers.md @@ -10,13 +10,13 @@ In enterprise applications, the shape of data returned by the API (DTOs) often differs from the shape used in the frontend (Domain Entities). Common differences include: -| API (DTO) | Frontend (Entity) | -| ------------------------------ | ---------------------------- | -| `snake_case` field names | `camelCase` field names | -| Deeply nested structures | Flattened/normalized shapes | -| Raw ISO date strings | Parsed `Date` objects | -| No computed fields | Derived/computed properties | -| Backend-specific enums | Frontend-friendly enums | +| API (DTO) | Frontend (Entity) | +| ------------------------ | --------------------------- | +| `snake_case` field names | `camelCase` field names | +| Deeply nested structures | Flattened/normalized shapes | +| Raw ISO date strings | Parsed `Date` objects | +| No computed fields | Derived/computed properties | +| Backend-specific enums | Frontend-friendly enums | Without transformers, this mapping logic leaks into components, hooks, and services — violating the **Single Responsibility Principle** and making the codebase harder to test and maintain. @@ -54,6 +54,7 @@ graph LR ``` **Data flows:** + - **API → Frontend:** Response DTO → `transformToEntity()` → Domain Entity - **Frontend → API:** Domain Entity → `transformToDTO()` → Request DTO @@ -153,14 +154,14 @@ interface IDataTransformer { Abstract class implementing `IDataTransformer` with sensible defaults. -| Method | Default Behavior | Override When | -| ------------------------- | ---------------------------------------- | ------------------------------------------ | -| `transformToEntity` | Identity cast (passthrough) | Always — this is the core mapping | -| `transformToDTO` | Identity cast (passthrough) | Always — this is the core mapping | -| `transformGetOneResponse` | Delegates to `transformToEntity` | `getOne` needs computed/derived fields | -| `transformGetManyResponse`| Maps each item via `transformToEntity` | List responses need bulk transformations | -| `transformCreatePayload` | Delegates to `transformToDTO` | Create payloads need special handling (e.g., strip IDs) | -| `transformEditPayload` | Delegates to `transformToDTO` | Edit payloads differ from create | +| Method | Default Behavior | Override When | +| -------------------------- | -------------------------------------- | ------------------------------------------------------- | +| `transformToEntity` | Identity cast (passthrough) | Always — this is the core mapping | +| `transformToDTO` | Identity cast (passthrough) | Always — this is the core mapping | +| `transformGetOneResponse` | Delegates to `transformToEntity` | `getOne` needs computed/derived fields | +| `transformGetManyResponse` | Maps each item via `transformToEntity` | List responses need bulk transformations | +| `transformCreatePayload` | Delegates to `transformToDTO` | Create payloads need special handling (e.g., strip IDs) | +| `transformEditPayload` | Delegates to `transformToDTO` | Edit payloads differ from create | --- @@ -168,14 +169,14 @@ Abstract class implementing `IDataTransformer` with sensible defaults. When a transformer is injected via `DataServicesConfig.transformer`, the base service methods automatically apply transformations: -| Service Method | Transformer Hook Used | Direction | -| -------------- | -------------------------------- | --------------- | -| `getOne()` | `transformGetOneResponse()` | Response → Entity | -| `getMany()` | `transformGetManyResponse()` | Response → Entity | -| `create()` | `transformCreatePayload()` | Entity → DTO | -| `edit()` | `transformEditPayload()` | Entity → DTO | -| `delete()` | None (no data transformation) | — | -| `customRequest()` | None (manual transformation) | — | +| Service Method | Transformer Hook Used | Direction | +| ----------------- | ----------------------------- | ----------------- | +| `getOne()` | `transformGetOneResponse()` | Response → Entity | +| `getMany()` | `transformGetManyResponse()` | Response → Entity | +| `create()` | `transformCreatePayload()` | Entity → DTO | +| `edit()` | `transformEditPayload()` | Entity → DTO | +| `delete()` | None (no data transformation) | — | +| `customRequest()` | None (manual transformation) | — | > **Important:** If no transformer is injected, all methods behave exactly as before — data passes through unchanged. This ensures 100% backward compatibility. @@ -277,8 +278,12 @@ Adding transformers to existing services requires **zero breaking changes**: ```typescript class MyTransformer extends BaseDataTransformer { - transformToEntity(dto: MyDTO): MyEntity { /* ... */ } - transformToDTO(entity: MyEntity): MyDTO { /* ... */ } + transformToEntity(dto: MyDTO): MyEntity { + /* ... */ + } + transformToDTO(entity: MyEntity): MyDTO { + /* ... */ + } } ``` @@ -296,8 +301,12 @@ class MyTransformer extends BaseDataTransformer { ```typescript class MyTransformer extends BaseDataTransformer { - transformToEntity(dto: MyDTO): MyEntity { /* ... */ } - transformToDTO(entity: MyEntity): MyDTO { /* ... */ } + transformToEntity(dto: MyDTO): MyEntity { + /* ... */ + } + transformToDTO(entity: MyEntity): MyDTO { + /* ... */ + } // Only override if getOne needs special handling override transformGetOneResponse(dto: MyDTO): MyEntity { @@ -315,9 +324,9 @@ class MyTransformer extends BaseDataTransformer { A full working example is available in the showcase booking feature: -| File | Description | -| ---- | ----------- | -| `apps/showcase/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping | -| `apps/showcase/.../booking/data/booking.data-services.ts` | Data service with injected transformer | -| `apps/showcase/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method | -| `apps/showcase/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` | +| File | Description | +| ------------------------------------------------------------------ | ------------------------------------------------------ | +| `apps/showcase/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping | +| `apps/showcase/.../booking/data/booking.data-services.ts` | Data service with injected transformer | +| `apps/showcase/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method | +| `apps/showcase/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` | diff --git a/apps/docs-dev/src/packages/core-events/index.md b/apps/docs-dev/src/packages/core-events/index.md index 881c711..452c9b9 100644 --- a/apps/docs-dev/src/packages/core-events/index.md +++ b/apps/docs-dev/src/packages/core-events/index.md @@ -19,6 +19,7 @@ The Global Pub/Sub & Hardware Integration Blueprint. ### Architectural Topology ### 1. Conceptual Topology: The Pub/Sub Data Flow + This diagram illustrates the high-level concept of our decoupled architecture, demonstrating how application-specific types merge into the core bus. ```mermaid @@ -45,6 +46,7 @@ graph LR ``` ### 2. System Architecture: Core Engine vs. App Autonomy + This detailed diagram shows the exact boundaries between the @repo/core-events engine and the consuming application, highlighting real-world publishers (e.g., Cashier UI) and subscribers. ```mermaid @@ -65,12 +67,12 @@ graph TD subgraph Apps ["apps/web (App Autonomy)"] D[[events.d.ts Declaration Merging]] - + %% Publishers A([Cashier UI]) B([Profile Settings]) C([WebSocket Client]) - + %% Subscribers X([Electron IPC Bridge]) Y([IndexedDB Sync]) @@ -107,18 +109,16 @@ graph TD By routing communication through this centralized event bus, we achieve: -* **App Autonomy**: The core defines the engine. The app defines the contract. There is zero circular dependency. -* **Zero Coupling**: Publishers and subscribers do not need to import, reference, or know about each other's existence. -* **Extreme Performance**: Components can subscribe to high-frequency data streams (like WebSockets or hardware signals) and update their own local state *without* triggering massive React tree re-renders. -* **Memory Safety**: The provided `useAppEvent` hook automatically handles subscription cleanup on component unmount, proactively preventing the most common source of memory leaks in Single Page Architectures (SPAs). - +- **App Autonomy**: The core defines the engine. The app defines the contract. There is zero circular dependency. +- **Zero Coupling**: Publishers and subscribers do not need to import, reference, or know about each other's existence. +- **Extreme Performance**: Components can subscribe to high-frequency data streams (like WebSockets or hardware signals) and update their own local state _without_ triggering massive React tree re-renders. +- **Memory Safety**: The provided `useAppEvent` hook automatically handles subscription cleanup on component unmount, proactively preventing the most common source of memory leaks in Single Page Architectures (SPAs). --- ## Defining Events (Module Augmentation) -> [!IMPORTANT] -> **Do NOT add application events to `packages/core-events/src/events.registry.ts`.** +> [!IMPORTANT] > **Do NOT add application events to `packages/core-events/src/events.registry.ts`.** > The core registry is intentionally empty. Each app owns its own event contract. The core exports an open `AppEventRegistry` interface. Apps extend it using TypeScript's `declare module` syntax — the same pattern used for `@types/*` across the JS ecosystem. @@ -148,7 +148,7 @@ declare module '@repo/core-events' { 'STORE:ORDER_PLACED': OrderPayload; 'STORE:ORDER_CANCELLED': { orderId: string; reason: string }; 'UI:SIDEBAR_TOGGLED': { collapsed: boolean }; - + // Explicit payloads for the examples below: 'DEVICE:PRINT_RECEIPT': { receiptId: string; items: any[]; total: number; cashierName: string; timestamp: number }; 'WS:STOCK_UPDATE': { id: string; price: number }; @@ -180,12 +180,12 @@ function OrderTracker() { ### Why this pattern? -| Concern | Old (Hardcoded) | New (Module Augmentation) | -|---|---|---| -| Core knows about app events? | ❌ Yes — violates IoC | ✅ No — core is a pure tool | -| Adding events requires editing core? | ❌ Yes | ✅ No — edit your app's `.d.ts` only | -| Multiple apps share the same registry? | ❌ Collision risk | ✅ Each app has its own `.d.ts` | -| Type safety / autocomplete | ✅ Works | ✅ Works identically | +| Concern | Old (Hardcoded) | New (Module Augmentation) | +| -------------------------------------- | --------------------- | ------------------------------------ | +| Core knows about app events? | ❌ Yes — violates IoC | ✅ No — core is a pure tool | +| Adding events requires editing core? | ❌ Yes | ✅ No — edit your app's `.d.ts` only | +| Multiple apps share the same registry? | ❌ Collision risk | ✅ Each app has its own `.d.ts` | +| Type safety / autocomplete | ✅ Works | ✅ Works identically | --- @@ -223,6 +223,7 @@ Here are three real-world architectural patterns powered by the Event Bus. All e **Solution**: The UI publishes a blind event. A headless listener handles the platform routing. **Publisher (Cashier UI)**: + ```tsx import { usePublishEvent } from '@repo/core-events'; @@ -234,7 +235,7 @@ export function CashierUI() { publish('DEVICE:PRINT_RECEIPT', { receiptId: 'RCP-123', items: [], - total: 45.00, + total: 45.0, cashierName: 'Firman', timestamp: Date.now(), }); @@ -245,6 +246,7 @@ export function CashierUI() { ``` **Subscriber (Headless Listener)**: + ```tsx import { useAppEvent } from '@repo/core-events'; @@ -274,10 +276,11 @@ export function PrinterListener() { **Solution**: The parent grid renders empty rows. Each row subscribes to the event bus and filters updates so it only re-renders when its specific data changes. **Parent Grid (Never re-renders)**: + ```tsx export function LiveStockGrid() { // Generates 1000 IDs once. No stock data is stored here! - const stockIds = generateStockIds(1000); + const stockIds = generateStockIds(1000); return ( @@ -292,6 +295,7 @@ export function LiveStockGrid() { ``` **Child Row (Targeted Updates)**: + ```tsx import { memo, useState } from 'react'; import { useAppEvent } from '@repo/core-events'; @@ -303,7 +307,7 @@ export const StockRow = memo(function StockRow({ stockId }) { // CRITICAL: Filter out events for other rows. // 999 out of 1000 rows will exit here instantly without causing a re-render. if (payload.id !== stockId) return; - + // Only the targeted row updates its local state setData(payload); }); @@ -326,6 +330,7 @@ export const StockRow = memo(function StockRow({ stockId }) { **Solution**: The UI form announces the profile update. A dedicated storage listener persists it in the background, properly escalating errors if the storage fails. **Publisher (Profile UI)**: + ```tsx import { usePublishEvent } from '@repo/core-events'; @@ -347,6 +352,7 @@ export function ProfileSettingsUI() { ``` **Subscriber (Storage Sync Listener)**: + ```tsx import { useAppEvent, usePublishEvent } from '@repo/core-events'; import { secureIndexedDB } from '@repo/core-storage'; @@ -355,7 +361,7 @@ export function StorageSyncListener() { const publish = usePublishEvent(); useAppEvent('AUTH:PROFILE_UPDATED', (payload) => { - // Automatically encrypted at rest because 'user_profile' + // Automatically encrypted at rest because 'user_profile' // is defined in ENCRYPTED_KEYS in @repo/core-storage secureIndexedDB.setItem('user_profile', payload).catch((error) => { // Escalate to global error handler instead of swallowing it @@ -365,4 +371,4 @@ export function StorageSyncListener() { return null; } -``` \ No newline at end of file +``` diff --git a/apps/docs-dev/src/packages/core-storage/index.md b/apps/docs-dev/src/packages/core-storage/index.md index afd15c9..908e664 100644 --- a/apps/docs-dev/src/packages/core-storage/index.md +++ b/apps/docs-dev/src/packages/core-storage/index.md @@ -4,11 +4,12 @@ > > **Description:** Enterprise storage engine providing AES-encrypted LocalStorage, strict-gatekeeper IndexedDB, and offline-first PouchDB with bi-directional CouchDB cloud synchronization. -`@repo/core-storage` is the **Enterprise-grade, multi-tool storage engine** for the Eigen Monorepo. +`@repo/core-storage` is the **Enterprise-grade, multi-tool storage engine** for the Eigen Monorepo. It provides a unified set of strictly-typed, secure, and fault-tolerant storage mechanisms tailored for React applications. It enforces strict **Inversion of Control (IoC)**—the core engine knows absolutely nothing about your application's business domains; instead, the consuming apps inject their own configurations and types. This package provides three primary storage solutions: + 1. **Secure Local Storage** (Strict Key-Gatekeeping & AES encryption) 2. **Secure [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)** (For larger key-value payloads) 3. **Offline-First [PouchDB](https://pouchdb.com/)** (For document-oriented, bi-directional sync data) @@ -20,6 +21,7 @@ This package provides three primary storage solutions: Browser storage is notoriously vulnerable to XSS attacks and pollution. The `LocalStorageService` and `IndexedDBService` implement a strict **Gatekeeper** pattern to solve this. By forcing developers to register every key explicitly into either `plainTextKeys` or `encryptedKeys`, the engine guarantees: + 1. No unapproved or rogue keys can ever be written or read (throws a `Security Exception`). 2. Highly sensitive tokens (e.g., JWTs) are automatically routed through the `@repo/utils` AES Encryption pipeline before touching the disk. @@ -56,19 +58,19 @@ graph TD %% 1. Initialization Flow REG -.->|Injects Keys & Config| FAC FAC -.->|Returns| INST - + %% 2. Runtime Execution Flow UI ===>|getItem / setItem| INST INST ---> API API ---> VAL - + %% 3. Gatekeeper Decision Tree VAL -.->|Invalid Key| ERR VAL ===>|Sensitive Key| ENC - + VAL --->|Plain-text Key| LOCAL VAL --->|Plain-text Key| IDB - + %% 4. Post-Encryption Storage ENC ===>|Encrypted Data| LOCAL ENC ===>|Encrypted Data| IDB @@ -107,10 +109,10 @@ const theme = await appStorage.getItem('THEME'); // Plaintext on disk ### ✅ Do's and ❌ Don'ts -* **✅ DO use TypeScript Literal Types** for your storage keys (`type Keys = 'A' | 'B'`) to get full IntelliSense. -* **✅ DO place Session/Auth tokens** exclusively inside the `encryptedKeys` Set. -* **❌ DON'T use native `window.localStorage` directly** anywhere in your React components. It bypasses our encryption and gatekeeper logic. -* **❌ DON'T mix domain data.** Keep UI preferences (Theme, Sidebar state) in LocalStorage, and large datasets (Offline Caches) in IndexedDB. +- **✅ DO use TypeScript Literal Types** for your storage keys (`type Keys = 'A' | 'B'`) to get full IntelliSense. +- **✅ DO place Session/Auth tokens** exclusively inside the `encryptedKeys` Set. +- **❌ DON'T use native `window.localStorage` directly** anywhere in your React components. It bypasses our encryption and gatekeeper logic. +- **❌ DON'T mix domain data.** Keep UI preferences (Theme, Sidebar state) in LocalStorage, and large datasets (Offline Caches) in IndexedDB. --- @@ -147,7 +149,7 @@ graph TD %% ─── Flow & Relationships ─── COMP ===>|Read / Write| L_SALES COMP ===>|Read / Write| L_INV - + MGR -.->|Instantiates Multi-DB| L_SALES MGR -.->|Instantiates Multi-DB| L_INV @@ -178,7 +180,7 @@ export const dbManager = new PouchDBManager(); export const itemDB = dbManager.register({ localName: 'items_db', - remoteUrl: 'http://admin:password@localhost:5984/items_db' + remoteUrl: 'http://admin:password@localhost:5984/items_db', }); ``` @@ -186,24 +188,24 @@ export const itemDB = dbManager.register({ The registered database returns a `PouchService` instance. This wrapper fully abstracts the raw PouchDB API into clean, Promise-based helpers, auto-handling `_rev` conflicts. -| Method | Description | -|---|---| -| `create(data)` | Inserts a new document. Auto-generates `_id` if omitted. | -| `update(id, data)` | Auto-fetches the latest `_rev` to merge payloads cleanly. | -| `delete(id)` | Auto-fetches the latest `_rev` to safely remove the document. | -| `getAll()` | Retrieves all documents (filters out internal `_design/` docs). | -| `find(options)` | Queries using MongoDB-style selectors (via `pouchdb-find`). | +| Method | Description | +| ------------------ | --------------------------------------------------------------- | +| `create(data)` | Inserts a new document. Auto-generates `_id` if omitted. | +| `update(id, data)` | Auto-fetches the latest `_rev` to merge payloads cleanly. | +| `delete(id)` | Auto-fetches the latest `_rev` to safely remove the document. | +| `getAll()` | Retrieves all documents (filters out internal `_design/` docs). | +| `find(options)` | Queries using MongoDB-style selectors (via `pouchdb-find`). | ```typescript // Example: Querying data using selectors const expensiveItems = await itemDB.find({ - selector: { price: { $gt: 100 }, category: 'electronics' } + selector: { price: { $gt: 100 }, category: 'electronics' }, }); ``` ### 3. Real-Time Reactivity (`onChange` Pub/Sub) -We implemented a **Publisher-Subscriber (Pub/Sub)** pattern inside the wrapper to handle real-time data changes efficiently. The wrapper maintains a *single* background connection to the changes feed and broadcasts events to all React subscribers. +We implemented a **Publisher-Subscriber (Pub/Sub)** pattern inside the wrapper to handle real-time data changes efficiently. The wrapper maintains a _single_ background connection to the changes feed and broadcasts events to all React subscribers. ```tsx import { useEffect, useCallback, useState } from 'react'; @@ -233,7 +235,7 @@ export function InventoryList() { ### 4. Envelope Pattern (`PouchEnvelopeDBManager`) -If you want to store multiple types of entities (e.g. `items`, `bookings`, `activities`) in a single CouchDB/PouchDB database to simplify sync setup, use the **Envelope Pattern**. +If you want to store multiple types of entities (e.g. `items`, `bookings`, `activities`) in a single CouchDB/PouchDB database to simplify sync setup, use the **Envelope Pattern**. Instead of `PouchDBManager`, instantiate a `PouchEnvelopeDBManager`. It provides the exact same `PouchService` API (CRUD + Find), but automatically wraps documents into an envelope format internally: `{ _id: "entityName:businessId", entity: "entityName", data: { ... } }`. @@ -244,16 +246,22 @@ import type { ItemEntity, BookingEntity } from './types'; export const envelopeDbManager = new PouchEnvelopeDBManager(); // Registers to the SAME database 'master_db', but scoped to 'item' -export const itemDB = envelopeDbManager.register({ - localName: 'master_db', - remoteUrl: 'http://admin:pass@localhost:5984/master_db' -}, 'item'); +export const itemDB = envelopeDbManager.register( + { + localName: 'master_db', + remoteUrl: 'http://admin:pass@localhost:5984/master_db', + }, + 'item', +); // Registers to the SAME database 'master_db', but scoped to 'booking' -export const bookingDB = envelopeDbManager.register({ - localName: 'master_db', - remoteUrl: 'http://admin:pass@localhost:5984/master_db' -}, 'booking'); +export const bookingDB = envelopeDbManager.register( + { + localName: 'master_db', + remoteUrl: 'http://admin:pass@localhost:5984/master_db', + }, + 'booking', +); // API usage remains identical! await itemDB.create({ _id: '123', name: 'Widget' }); // Stored as "item:123" @@ -265,19 +273,20 @@ const results = await itemDB.search('widget keyword', ['data.name', 'data.sku']) ### ✅ Do's and ❌ Don'ts for PouchDB -* **✅ DO use `.onChange()`** to make your UI reactive to background cloud syncs. -* **✅ DO return the `unsubscribe` function** in your `useEffect` cleanup block to prevent severe memory leaks. -* **❌ DON'T use `db.raw.changes()`** inside your React components. It creates zombie WebSocket connections and tightly couples your UI to PouchDB's specific API. -* **❌ DON'T pass the `_rev` property** manually when updating or deleting. The wrapper's `update()` and `delete()` methods handle revision fetching automatically. +- **✅ DO use `.onChange()`** to make your UI reactive to background cloud syncs. +- **✅ DO return the `unsubscribe` function** in your `useEffect` cleanup block to prevent severe memory leaks. +- **❌ DON'T use `db.raw.changes()`** inside your React components. It creates zombie WebSocket connections and tightly couples your UI to PouchDB's specific API. +- **❌ DON'T pass the `_rev` property** manually when updating or deleting. The wrapper's `update()` and `delete()` methods handle revision fetching automatically. --- ## ⚠️ Troubleshooting ### CouchDB CORS Infinite Retries + By providing a `remoteUrl`, the engine runs bi-directional sync in the background (`live: true, retry: true`). Fault tolerance is guaranteed: if CouchDB crashes, local reads/writes continue uninterrupted. However, if your browser blocks CouchDB sync with a **CORS error**, PouchDB will misinterpret this as a network failure and enter an infinite retry loop, flooding your Network tab. > **DO NOT try to fix this in the frontend Vite config or proxy!** -> This is strictly a CouchDB server policy issue. You must enable CORS directly on the CouchDB cluster (editing its `local.ini` or via its dashboard) to allow `origins`, `credentials`, and `headers`. \ No newline at end of file +> This is strictly a CouchDB server policy issue. You must enable CORS directly on the CouchDB cluster (editing its `local.ini` or via its dashboard) to allow `origins`, `credentials`, and `headers`. diff --git a/apps/docs-dev/src/packages/ui/ACTION-TOOLS.md b/apps/docs-dev/src/packages/ui/ACTION-TOOLS.md index df3c367..f6f3008 100644 --- a/apps/docs-dev/src/packages/ui/ACTION-TOOLS.md +++ b/apps/docs-dev/src/packages/ui/ACTION-TOOLS.md @@ -12,12 +12,7 @@ These components automatically adapt to screen sizes, handle tooltip generation, ## Import Statement ```tsx -import { - PageActions, - RowActions, - type PageAction, - type RowAction -} from '@repo/ui/components'; +import { PageActions, RowActions, type PageAction, type RowAction } from '@repo/ui/components'; ``` ## Usage Examples @@ -52,11 +47,11 @@ function PageHeader() { icon: , onClick: (k) => console.log(k), }, - { - key: 'print-copy', - label: 'Print Copy', - icon: , - onClick: (k) => console.log(k) + { + key: 'print-copy', + label: 'Print Copy', + icon: , + onClick: (k) => console.log(k), }, ], }, @@ -132,17 +127,17 @@ function DataTable() { ### PageActions Props -| Prop | Type | Default | Description | -|---|---|---|---| -| `actions` | `PageAction[]` | Required | Array of configured page-level actions. | -| `onClose` | `() => void` | `undefined` | Optional callback triggered when the close (X) button is clicked. | +| Prop | Type | Default | Description | +| --------- | -------------- | ----------- | ----------------------------------------------------------------- | +| `actions` | `PageAction[]` | Required | Array of configured page-level actions. | +| `onClose` | `() => void` | `undefined` | Optional callback triggered when the close (X) button is clicked. | ### RowActions Props -| Prop | Type | Default | Description | -|---|---|---|---| -| `actions` | `RowAction[]` | `[]` | Array of configured row-level actions. | -| `showLabels` | `boolean` | `false` | If true, renders the text label alongside the icon for top-level buttons. | +| Prop | Type | Default | Description | +| ------------ | ------------- | ------- | ------------------------------------------------------------------------- | +| `actions` | `RowAction[]` | `[]` | Array of configured row-level actions. | +| `showLabels` | `boolean` | `false` | If true, renders the text label alongside the icon for top-level buttons. | ### Action Definitions @@ -150,29 +145,29 @@ Both `PageAction` and `RowAction` share a common base interface. **Base Action Properties (`BaseAction`)** -| Property | Type | Description | -|---|---|---| -| `key` | `string` | Unique identifier. Required for 'action', optional for 'divider'. | -| `type` | `'action'` \| `'divider'` | Type of action. Defaults to 'action'. | -| `icon` | `ReactNode` | Visual representation of the action. | -| `disabled` | `boolean` | Disables interaction if set to true. | -| `intent` | `'default'` \| `'success'` \| `'warning'` \| `'destructive'` \| `'primary'` | Semantic context to determine visual emphasis (color mapping). | -| `onClick` | `(key: string) => void` | Callback triggered upon execution. | +| Property | Type | Description | +| ---------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `key` | `string` | Unique identifier. Required for 'action', optional for 'divider'. | +| `type` | `'action'` \| `'divider'` | Type of action. Defaults to 'action'. | +| `icon` | `ReactNode` | Visual representation of the action. | +| `disabled` | `boolean` | Disables interaction if set to true. | +| `intent` | `'default'` \| `'success'` \| `'warning'` \| `'destructive'` \| `'primary'` | Semantic context to determine visual emphasis (color mapping). | +| `onClick` | `(key: string) => void` | Callback triggered upon execution. | **`PageAction` Specific Properties** -| Property | Type | Description | -|---|---|---| -| `label` | `string` | Text label displayed on the button. Required for 'action' type. | -| `variant` | `'filled'` \| `'light'` \| `'outline'` \| `'default'` \| `'subtle'` \| `'transparent'` | Specifies the Mantine button variant. Defaults to 'transparent' internally. | -| `children` | `PageAction[]` | Nested actions rendered as a dropdown menu below the main button. | +| Property | Type | Description | +| ---------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `label` | `string` | Text label displayed on the button. Required for 'action' type. | +| `variant` | `'filled'` \| `'light'` \| `'outline'` \| `'default'` \| `'subtle'` \| `'transparent'` | Specifies the Mantine button variant. Defaults to 'transparent' internally. | +| `children` | `PageAction[]` | Nested actions rendered as a dropdown menu below the main button. | **`RowAction` Specific Properties** -| Property | Type | Description | -|---|---|---| -| `label` | `string` | Text primarily used when rendered inside a nested menu item. | -| `tooltip` | `string` | Optional text displayed on hover over the standalone icon. | +| Property | Type | Description | +| ---------- | ------------- | ------------------------------------------------------------ | +| `label` | `string` | Text primarily used when rendered inside a nested menu item. | +| `tooltip` | `string` | Optional text displayed on hover over the standalone icon. | | `children` | `RowAction[]` | Nested actions that will be rendered inside a dropdown menu. | ## Best Practices diff --git a/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md b/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md index ca9c16b..77a3330 100644 --- a/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md +++ b/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md @@ -8,8 +8,7 @@ outline: [2, 3] > > **Description:** Configuration-driven layout engine wrapping Mantine's AppShell, providing three layout variants (header-first, sidebar-first, top-nav), double sidebar support, responsive mobile drawers, and state persistence via Context API. -> **Package**: `@repo/ui` · **Module Path**: `@repo/ui/components` -> **Dependencies**: React 18+, [Mantine v8](https://mantine.dev/) (`AppShell`), [`@mantine/hooks`](https://mantine.dev/hooks/package/) +> **Package**: `@repo/ui` · **Module Path**: `@repo/ui/components` > **Dependencies**: React 18+, [Mantine v8](https://mantine.dev/) (`AppShell`), [`@mantine/hooks`](https://mantine.dev/hooks/package/) --- @@ -100,11 +99,11 @@ interface CoreAppShellConfig { } ``` -| Property | Type | Required | Description | -|---|---|---|---| -| `variant` | `LayoutVariant` | ✅ | Determines the structural layout mode | -| `dimensions` | `CoreAppShellDimensions` | — | Override default pixel dimensions | -| `features` | `CoreAppShellFeatures` | — | Toggle optional layout regions and behaviors | +| Property | Type | Required | Description | +| ------------ | ------------------------ | -------- | -------------------------------------------- | +| `variant` | `LayoutVariant` | ✅ | Determines the structural layout mode | +| `dimensions` | `CoreAppShellDimensions` | — | Override default pixel dimensions | +| `features` | `CoreAppShellFeatures` | — | Toggle optional layout regions and behaviors | --- @@ -114,11 +113,11 @@ interface CoreAppShellConfig { type LayoutVariant = 'header-first' | 'sidebar-first' | 'top-nav'; ``` -| Variant | Mantine `layout` | Visual Description | -|---|---|---| -| `header-first` | `default` | Header spans the full viewport width. Sidebar and aside sit **below** the header, stretching to the bottom of the screen. Footer is inset between the sidebar and aside. This is the most common enterprise/dashboard pattern (e.g., Azure Portal, Jira). | -| `sidebar-first` | `alt` | Sidebar spans the full viewport height. Header sits **to the right** of the sidebar. Produces a "desktop application" feel (e.g., VS Code, Slack). Footer spans full width beneath the sidebar. | -| `top-nav` | `default` | Header-only layout with **no visible desktop sidebar**. The sidebar is hidden on desktop but remains accessible as a mobile drawer on small screens. Ideal for documentation sites or marketing pages. | +| Variant | Mantine `layout` | Visual Description | +| --------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `header-first` | `default` | Header spans the full viewport width. Sidebar and aside sit **below** the header, stretching to the bottom of the screen. Footer is inset between the sidebar and aside. This is the most common enterprise/dashboard pattern (e.g., Azure Portal, Jira). | +| `sidebar-first` | `alt` | Sidebar spans the full viewport height. Header sits **to the right** of the sidebar. Produces a "desktop application" feel (e.g., VS Code, Slack). Footer spans full width beneath the sidebar. | +| `top-nav` | `default` | Header-only layout with **no visible desktop sidebar**. The sidebar is hidden on desktop but remains accessible as a mobile drawer on small screens. Ideal for documentation sites or marketing pages. | > [!IMPORTANT] > When `variant` is set to `top-nav`, the desktop navbar is visually hidden via `collapsed.desktop: true` and width `0`. However, the `` DOM element remains mounted with responsive width props so the mobile drawer continues to function. This is an intentional design choice to avoid conditional DOM removal. @@ -140,19 +139,18 @@ interface CoreAppShellFeatures { } ``` -| Property | Type | Default | Description | -|---|---|---|---| -| `desktopCollapseVariant` | `'hide' \| 'mini'` | `'hide'` | **`hide`**: Sidebar slides out completely (collapsed width = 0). **`mini`**: Sidebar shrinks to `sidebarMiniWidth` showing only icons. | -| `withUtilityBar` | `boolean` | Auto-detected | Show the utility bar above the header. If omitted, the bar renders when a `utilityBar` slot is provided. Set explicitly to `false` to suppress. | -| `withAside` | `boolean` | Auto-detected | Show the right-hand aside panel. Same auto-detection logic as `withUtilityBar`. | -| `withFooter` | `boolean` | Auto-detected | Show the bottom footer. Same auto-detection logic. | -| `withDoubleSidebar` | `boolean` | `false` | Enable the **Rail + Panel** double sidebar mode. When `true`, the navbar renders `sidebarRail` and `sidebarPanel` slots instead of the single `sidebar` slot. | -| `persistState` | `boolean` | `true` (implied) | Persist sidebar variant (`expanded`/`mini`/`hidden`) to `localStorage` via `useLocalStorage`. Set to `false` for demos or ephemeral layouts. | -| `zIndex` | `number` | `200` | Base z-index passed to Mantine's `AppShell`. | -| `disabled` | `boolean` | `false` | Disables the AppShell layout entirely (renders children without structural chrome). | +| Property | Type | Default | Description | +| ------------------------ | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `desktopCollapseVariant` | `'hide' \| 'mini'` | `'hide'` | **`hide`**: Sidebar slides out completely (collapsed width = 0). **`mini`**: Sidebar shrinks to `sidebarMiniWidth` showing only icons. | +| `withUtilityBar` | `boolean` | Auto-detected | Show the utility bar above the header. If omitted, the bar renders when a `utilityBar` slot is provided. Set explicitly to `false` to suppress. | +| `withAside` | `boolean` | Auto-detected | Show the right-hand aside panel. Same auto-detection logic as `withUtilityBar`. | +| `withFooter` | `boolean` | Auto-detected | Show the bottom footer. Same auto-detection logic. | +| `withDoubleSidebar` | `boolean` | `false` | Enable the **Rail + Panel** double sidebar mode. When `true`, the navbar renders `sidebarRail` and `sidebarPanel` slots instead of the single `sidebar` slot. | +| `persistState` | `boolean` | `true` (implied) | Persist sidebar variant (`expanded`/`mini`/`hidden`) to `localStorage` via `useLocalStorage`. Set to `false` for demos or ephemeral layouts. | +| `zIndex` | `number` | `200` | Base z-index passed to Mantine's `AppShell`. | +| `disabled` | `boolean` | `false` | Disables the AppShell layout entirely (renders children without structural chrome). | -> [!TIP] -> **Smart defaults**: You rarely need to set `withUtilityBar`, `withAside`, or `withFooter` explicitly. The engine auto-detects presence by checking if the corresponding slot is provided and truthy. Only set them to `false` when you want to **suppress** a slot that is being passed. +> [!TIP] > **Smart defaults**: You rarely need to set `withUtilityBar`, `withAside`, or `withFooter` explicitly. The engine auto-detects presence by checking if the corresponding slot is provided and truthy. Only set them to `false` when you want to **suppress** a slot that is being passed. --- @@ -169,14 +167,14 @@ interface CoreAppShellDimensions { } ``` -| Property | Type | Default | Description | -|---|---|---|---| -| `utilityBarHeight` | `number \| string` | `32` | Height of the utility bar strip above the header | -| `headerHeight` | `number \| string` | `60` | Height of the main header | -| `sidebarWidth` | `number \| string` | `260` | Width of the expanded sidebar | -| `sidebarMiniWidth` | `number \| string` | `80` | Width of the sidebar in `mini` collapse mode | -| `sidebarRailWidth` | `number \| string` | `54` | Width of the icon rail in double-sidebar mode | -| `asideWidth` | `number \| string` | `260` | Width of the right-hand aside panel | +| Property | Type | Default | Description | +| ------------------ | ------------------ | ------- | ------------------------------------------------ | +| `utilityBarHeight` | `number \| string` | `32` | Height of the utility bar strip above the header | +| `headerHeight` | `number \| string` | `60` | Height of the main header | +| `sidebarWidth` | `number \| string` | `260` | Width of the expanded sidebar | +| `sidebarMiniWidth` | `number \| string` | `80` | Width of the sidebar in `mini` collapse mode | +| `sidebarRailWidth` | `number \| string` | `54` | Width of the icon rail in double-sidebar mode | +| `asideWidth` | `number \| string` | `260` | Width of the right-hand aside panel | > [!NOTE] > All dimension values accept both pixel numbers (e.g., `260`) and CSS strings (e.g., `'20rem'`). When both `headerHeight` and `utilityBarHeight` are numbers, they are summed directly. When either is a string, the engine wraps them in a `calc()` expression automatically. @@ -200,16 +198,16 @@ interface CoreAppShellSlots { } ``` -| Slot | Location | Notes | -|---|---|---| -| `utilityBar` | Above the header, hidden on mobile (`display: none` below `sm`) | Typically used for environment banners, announcements, or top-level links. | -| `header` | Main application header | Must contain its own `` for mobile toggle (use `useCoreAppShell()` context). | -| `sidebar` | Desktop navbar body (single-sidebar mode) | Ignored when `withDoubleSidebar` is `true` — use `sidebarRail` + `sidebarPanel` instead. | -| `sidebarMobile` | Mobile drawer content | Falls back to `sidebar` if not provided. Use this to render a simplified mobile-specific navigation. | -| `sidebarRail` | Narrow icon rail (double-sidebar mode) | Only rendered when `withDoubleSidebar` is `true`. Separated from `sidebarPanel` by a 1px border. | -| `sidebarPanel` | Contextual panel beside the rail (double-sidebar mode) | Collapsible via `toggleNavbarPanel()`. Only rendered when `withDoubleSidebar` is `true` and `navbarPanelOpened` is `true`. | -| `aside` | Right-hand panel | Collapsible via `toggleAside()`. Only rendered when `withAside` is enabled. | -| `footer` | Bottom application footer | In `header-first` mode, the footer is inset between sidebar and aside. In `sidebar-first` mode, it spans the full width. | +| Slot | Location | Notes | +| --------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `utilityBar` | Above the header, hidden on mobile (`display: none` below `sm`) | Typically used for environment banners, announcements, or top-level links. | +| `header` | Main application header | Must contain its own `` for mobile toggle (use `useCoreAppShell()` context). | +| `sidebar` | Desktop navbar body (single-sidebar mode) | Ignored when `withDoubleSidebar` is `true` — use `sidebarRail` + `sidebarPanel` instead. | +| `sidebarMobile` | Mobile drawer content | Falls back to `sidebar` if not provided. Use this to render a simplified mobile-specific navigation. | +| `sidebarRail` | Narrow icon rail (double-sidebar mode) | Only rendered when `withDoubleSidebar` is `true`. Separated from `sidebarPanel` by a 1px border. | +| `sidebarPanel` | Contextual panel beside the rail (double-sidebar mode) | Collapsible via `toggleNavbarPanel()`. Only rendered when `withDoubleSidebar` is `true` and `navbarPanelOpened` is `true`. | +| `aside` | Right-hand panel | Collapsible via `toggleAside()`. Only rendered when `withAside` is enabled. | +| `footer` | Bottom application footer | In `header-first` mode, the footer is inset between sidebar and aside. In `sidebar-first` mode, it spans the full width. | --- @@ -221,22 +219,21 @@ The `useCoreAppShell()` hook provides access to all layout state and toggle meth import { useCoreAppShell } from '@repo/ui/components'; ``` -| Property / Method | Type | Description | -|---|---|---| -| `mobileOpened` | `boolean` | Whether the mobile drawer is currently open | -| `desktopOpened` | `boolean` | Whether the desktop sidebar is expanded (only applies when `desktopCollapseVariant` is `'hide'`) | -| `sidebarVariant` | `SidebarVariant` | Current sidebar mode: `'expanded'` \| `'mini'` \| `'hidden'` | -| `asideOpened` | `boolean` | Whether the aside panel is currently visible | -| `navbarPanelOpened` | `boolean` | Whether the secondary panel in double-sidebar mode is expanded | -| `config` | `CoreAppShellConfig` | Read-only access to the current layout configuration | -| `toggleMobile()` | `() => void` | Toggle the mobile drawer open/closed | -| `toggleDesktop()` | `() => void` | Toggle the desktop sidebar open/closed | -| `toggleAside()` | `() => void` | Toggle the aside panel visibility | -| `toggleNavbarPanel()` | `() => void` | Toggle the double-sidebar panel open/closed | -| `setSidebarVariant()` | `(variant: SidebarVariant) => void` | Programmatically set the sidebar to `'expanded'`, `'mini'`, or `'hidden'` | +| Property / Method | Type | Description | +| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------ | +| `mobileOpened` | `boolean` | Whether the mobile drawer is currently open | +| `desktopOpened` | `boolean` | Whether the desktop sidebar is expanded (only applies when `desktopCollapseVariant` is `'hide'`) | +| `sidebarVariant` | `SidebarVariant` | Current sidebar mode: `'expanded'` \| `'mini'` \| `'hidden'` | +| `asideOpened` | `boolean` | Whether the aside panel is currently visible | +| `navbarPanelOpened` | `boolean` | Whether the secondary panel in double-sidebar mode is expanded | +| `config` | `CoreAppShellConfig` | Read-only access to the current layout configuration | +| `toggleMobile()` | `() => void` | Toggle the mobile drawer open/closed | +| `toggleDesktop()` | `() => void` | Toggle the desktop sidebar open/closed | +| `toggleAside()` | `() => void` | Toggle the aside panel visibility | +| `toggleNavbarPanel()` | `() => void` | Toggle the double-sidebar panel open/closed | +| `setSidebarVariant()` | `(variant: SidebarVariant) => void` | Programmatically set the sidebar to `'expanded'`, `'mini'`, or `'hidden'` | -> [!WARNING] -> `useCoreAppShell()` **must** be called from within a `` subtree. Calling it outside the provider will throw: `"useCoreAppShell must be used within CoreAppShellProvider"`. If you need context access in the header slot, pass a component (not inline JSX) so it mounts inside the provider tree. +> [!WARNING] > `useCoreAppShell()` **must** be called from within a `` subtree. Calling it outside the provider will throw: `"useCoreAppShell must be used within CoreAppShellProvider"`. If you need context access in the header slot, pass a component (not inline JSX) so it mounts inside the provider tree. --- @@ -272,8 +269,12 @@ function App() { header: , sidebar: ( - - + + ), }} @@ -300,7 +301,9 @@ function AppHeader() { - Enterprise Dashboard + + Enterprise Dashboard + ); @@ -393,7 +396,9 @@ function App() { ), sidebarPanel: ( - Navigation + + Navigation + {/* Contextual links based on active rail icon */} ), @@ -429,14 +434,17 @@ function ShellDemo() { const [collapseVariant, setCollapseVariant] = useState('hide'); const [withDoubleSidebar, setWithDoubleSidebar] = useState(false); - const config: CoreAppShellConfig = useMemo(() => ({ - variant: layoutVariant, - features: { - desktopCollapseVariant: collapseVariant, - withDoubleSidebar, - persistState: false, - }, - }), [layoutVariant, collapseVariant, withDoubleSidebar]); + const config: CoreAppShellConfig = useMemo( + () => ({ + variant: layoutVariant, + features: { + desktopCollapseVariant: collapseVariant, + withDoubleSidebar, + persistState: false, + }, + }), + [layoutVariant, collapseVariant, withDoubleSidebar], + ); return ( , sidebar: }}> @@ -466,13 +474,13 @@ interface CorePageContainerProps extends ContainerProps { } ``` -| Prop | Type | Default | Description | -|---|---|---|---| -| `headerSlot` | `ReactNode` | — | Page-level header content (title, breadcrumbs, action buttons). Rendered above the main content with a bottom border. | -| `stickyHeader` | `boolean` | `false` | When `true`, the page header sticks to the top of the scroll area, offset by the AppShell header height via `var(--app-shell-header-offset)`. | -| `px` | `MantineSpacing` | `'md'` | Horizontal padding for both the header and content areas | -| `py` | `MantineSpacing` | `'md'` | Vertical padding for both the header and content areas | -| _...rest_ | `ContainerProps` | — | All other Mantine `Container` props are forwarded to the content region | +| Prop | Type | Default | Description | +| -------------- | ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `headerSlot` | `ReactNode` | — | Page-level header content (title, breadcrumbs, action buttons). Rendered above the main content with a bottom border. | +| `stickyHeader` | `boolean` | `false` | When `true`, the page header sticks to the top of the scroll area, offset by the AppShell header height via `var(--app-shell-header-offset)`. | +| `px` | `MantineSpacing` | `'md'` | Horizontal padding for both the header and content areas | +| `py` | `MantineSpacing` | `'md'` | Vertical padding for both the header and content areas | +| _...rest_ | `ContainerProps` | — | All other Mantine `Container` props are forwarded to the content region | ### Usage @@ -482,7 +490,9 @@ interface CorePageContainerProps extends ContainerProps { stickyHeader headerSlot={ - Users + + Users + } @@ -513,12 +523,12 @@ In `sidebar-first` mode, the footer spans the full viewport width (`left: 0; rig ### Z-Index Strategy -| Element | `header-first` | `sidebar-first` | -|---|---|---| +| Element | `header-first` | `sidebar-first` | +| --------------- | --------------- | --------------- | | AppShell (base) | `200` (default) | `200` (default) | -| Navbar | `105` | `100` | -| Aside | `105` | `100` | -| Footer | `100` | `100` | +| Navbar | `105` | `100` | +| Aside | `105` | `100` | +| Footer | `100` | `100` | The elevated `105` z-index for navbar/aside in `header-first` mode ensures they render above the footer, which is positioned at `100`. diff --git a/apps/docs-dev/src/packages/ui/FORM-COMPONENTS.md b/apps/docs-dev/src/packages/ui/FORM-COMPONENTS.md index 057e182..a9961b8 100644 --- a/apps/docs-dev/src/packages/ui/FORM-COMPONENTS.md +++ b/apps/docs-dev/src/packages/ui/FORM-COMPONENTS.md @@ -8,8 +8,7 @@ outline: [2, 3] > > **Description:** 22 pre-built form field components generated via a withRHF() HOC factory, integrating Mantine inputs with React Hook Form micro-subscriptions, Zod validation, and i18n error translation for ERP-scale performance. -> **Package**: `@repo/ui` · **Module Path**: `@repo/ui/form` -> **Dependencies**: [React Hook Form](https://react-hook-form.com/) v7, [Zod](https://zod.dev/) v3, [Mantine](https://mantine.dev/) v8, `@repo/core-i18n` +> **Package**: `@repo/ui` · **Module Path**: `@repo/ui/form` > **Dependencies**: [React Hook Form](https://react-hook-form.com/) v7, [Zod](https://zod.dev/) v3, [Mantine](https://mantine.dev/) v8, `@repo/core-i18n` --- @@ -68,17 +67,17 @@ withRHF(displayName, MantineComponent, options?) The factory accepts three arguments: -| Argument | Type | Description | -|---|---|---| -| `displayName` | `string` | React DevTools name (e.g., `"FieldTextInput"`) | -| `MantineComponent` | `ComponentType` | The raw Mantine component | -| `options` | `WithRHFOptions` | Optional config for special components | +| Argument | Type | Description | +| ------------------ | ---------------- | ---------------------------------------------- | +| `displayName` | `string` | React DevTools name (e.g., `"FieldTextInput"`) | +| `MantineComponent` | `ComponentType` | The raw Mantine component | +| `options` | `WithRHFOptions` | Optional config for special components | #### Options -| Option | Default | Description | -|---|---|---| -| `isCheckType` | `false` | Use `checked` instead of `value` (for Checkbox, Switch) | +| Option | Default | Description | +| ----------------- | ------- | ----------------------------------------------------------------------------------------- | +| `isCheckType` | `false` | Use `checked` instead of `value` (for Checkbox, Switch) | | `requiresWrapper` | `false` | Wrap in `Input.Wrapper` for error display (for ColorPicker, SegmentedControl, Chip.Group) | ### Naming Conventions @@ -144,7 +143,6 @@ import { withRHF } from '../withRHF'; export const FieldTextInput = withRHF('FieldTextInput', TextInput); ``` - --- ## Performance & Memoization @@ -153,10 +151,10 @@ export const FieldTextInput = withRHF('FieldTextInput', TextInpu In enterprise ERP forms with **1500+ fields**, performance is critical: -| Technique | What it prevents | Cost | -|---|---|---| -| **`useController`** | Global form state re-renders — each field subscribes only to its own slice | ~0 (hook-level isolation) | -| **`React.memo`** | Parent-driven re-renders (e.g., grid layout changes, tab switches) | O(n) shallow prop comparison (typically n < 10) | +| Technique | What it prevents | Cost | +| ------------------- | -------------------------------------------------------------------------- | ----------------------------------------------- | +| **`useController`** | Global form state re-renders — each field subscribes only to its own slice | ~0 (hook-level isolation) | +| **`React.memo`** | Parent-driven re-renders (e.g., grid layout changes, tab switches) | O(n) shallow prop comparison (typically n < 10) | Together, they achieve **O(1) render cost per keystroke** regardless of form size. @@ -185,10 +183,13 @@ Encode Zod errors as JSON with a translation key: ```tsx const schema = z.object({ - name: z.string().min(3, JSON.stringify({ - key: 'validation:min_length', - values: { min: 3 }, - })), + name: z.string().min( + 3, + JSON.stringify({ + key: 'validation:min_length', + values: { min: 3 }, + }), + ), }); // Error displayed: t('validation:min_length', { min: 3 }) // → "Minimum 3 characters" (from validation namespace) @@ -311,23 +312,23 @@ function ExampleForm() { ## Validator Bank Reference -The `registry.validator.ts` provides a set of pre-configured atomic validators returning modified Zod schemas that automatically emit translated JSON payloads. +The `registry.validator.ts` provides a set of pre-configured atomic validators returning modified Zod schemas that automatically emit translated JSON payloads. ### Available Atomic Validators -| Category | Validator | Target Type | Description | -|---|---|---|---| -| **Numeric** | `minValue(min, field?)` | `ZodNumber` | Minimum numeric value | -| **Numeric** | `maxValue(max, field?)` | `ZodNumber` | Maximum numeric value | -| **Numeric** | `rangeValue(min, max, field?)` | `ZodNumber` | Restricts value between `min` and `max` limits | -| **Numeric** | `positiveNumber(field?)` | `ZodNumber` | Restricts to positive numbers | -| **String** | `minLength(len, field?)` | `ZodString` | Minimum string character length | -| **String** | `maxLength(len, field?)` | `ZodString` | Maximum string character length | -| **String** | `rangeLength(min, max, field?)` | `ZodString` | Restricts string length between `min` and `max` bounds | -| **Security** | `simplePassword(min)` | `ZodString` | Checks password string length bounds only | -| **Security** | `complexPassword(min)` | `ZodString` | Enforces length, 1 uppercase, 1 lowercase, 1 number, and 1 special char | -| **Technical** | `emailValidator()` | `ZodString` | Standard email format | -| **Technical** | `phoneValidator()` | `ZodString` | Enforces Indonesian (+62) phone number format | +| Category | Validator | Target Type | Description | +| ------------- | ------------------------------- | ----------- | ----------------------------------------------------------------------- | +| **Numeric** | `minValue(min, field?)` | `ZodNumber` | Minimum numeric value | +| **Numeric** | `maxValue(max, field?)` | `ZodNumber` | Maximum numeric value | +| **Numeric** | `rangeValue(min, max, field?)` | `ZodNumber` | Restricts value between `min` and `max` limits | +| **Numeric** | `positiveNumber(field?)` | `ZodNumber` | Restricts to positive numbers | +| **String** | `minLength(len, field?)` | `ZodString` | Minimum string character length | +| **String** | `maxLength(len, field?)` | `ZodString` | Maximum string character length | +| **String** | `rangeLength(min, max, field?)` | `ZodString` | Restricts string length between `min` and `max` bounds | +| **Security** | `simplePassword(min)` | `ZodString` | Checks password string length bounds only | +| **Security** | `complexPassword(min)` | `ZodString` | Enforces length, 1 uppercase, 1 lowercase, 1 number, and 1 special char | +| **Technical** | `emailValidator()` | `ZodString` | Standard email format | +| **Technical** | `phoneValidator()` | `ZodString` | Enforces Indonesian (+62) phone number format | > [!WARNING] > Always distinguish between `rangeValue` (which bounds the actual numeric integer/float) and `rangeLength` (which bounds the amount of characters in a string). @@ -343,11 +344,7 @@ import { z } from 'zod'; import { compose, required, minLength, complexPassword } from '@repo/ui/validators'; export const userRegistrationSchema = z.object({ - password: compose( - z.string(), - required('Password'), - complexPassword(8) - ) + password: compose(z.string(), required('Password'), complexPassword(8)), }); ``` @@ -361,10 +358,10 @@ Tests must explicitly verify the JSON stringified i18n payload: it('minValue() should enforce min', () => { const schema = compose(z.number(), minValue(10, 'Age')); const res = schema.safeParse(5); - + expect(res.success).toBe(false); expect(res.error?.issues[0].message).toBe( - JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } }) + JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } }), ); }); ``` @@ -382,10 +379,10 @@ To decouple complex rendering side-effects from your component's root render fun The hook supports two cleanup strategies defined by the `mode` parameter: -| Mode | Behavior | Use Case | -|---|---|---| -| `unregister` | Completely unmounts the field. Value is wiped. Key is removed from submission payload. | Hidden fields (e.g. Spouse Name if "Single" is checked). | -| `reset` | Field stays active/disabled. Value is wiped. Error state is cleared. Key is sent in payload as empty/default. | Disabled or Cascading fields (e.g. Email Input if "Subscribe" is false, or resetting City when Province changes). | +| Mode | Behavior | Use Case | +| ------------ | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `unregister` | Completely unmounts the field. Value is wiped. Key is removed from submission payload. | Hidden fields (e.g. Spouse Name if "Single" is checked). | +| `reset` | Field stays active/disabled. Value is wiped. Error state is cleared. Key is sent in payload as empty/default. | Disabled or Cascading fields (e.g. Email Input if "Subscribe" is false, or resetting City when Province changes). | ### Hook Configuration @@ -395,7 +392,7 @@ import { useConditionalField } from '@repo/ui/hooks'; export function ExampleForm() { const { control, setValue, unregister, clearErrors } = useForm(); - + const userType = useWatch({ control, name: 'userType' }); const newsletter = useWatch({ control, name: 'newsletter' }); @@ -405,7 +402,7 @@ export function ExampleForm() { name: 'corporateTaxId', setValue, unregister, - mode: 'unregister' + mode: 'unregister', }); // 2. Reset Mode (Visible but Disabled) @@ -414,7 +411,7 @@ export function ExampleForm() { name: 'newsletterEmail', setValue, clearErrors, - mode: 'reset' + mode: 'reset', }); return
...; @@ -423,13 +420,12 @@ export function ExampleForm() { ### Cascading Dropdowns & Reactivity -When dealing with cascading dependencies (e.g., Department -> Role), changing the parent dropdown should invalidate and reset the child dropdown. +When dealing with cascading dependencies (e.g., Department -> Role), changing the parent dropdown should invalidate and reset the child dropdown. You can accomplish this easily by supplying `mode: 'reset'` to `useConditionalField`. However, there is a **critical rendering caveat** with Mantine's `Select` (and similar complex visual inputs): -> [!WARNING] -> **The Dynamic Key Trick:** Mantine components aggressively cache their internal visual text state. Even if `useConditionalField` perfectly resets the React Hook Form payload state to `''`, Mantine may still visually display the old, stale text on the screen. -> +> [!WARNING] > **The Dynamic Key Trick:** Mantine components aggressively cache their internal visual text state. Even if `useConditionalField` perfectly resets the React Hook Form payload state to `''`, Mantine may still visually display the old, stale text on the screen. +> > To fix this UI desync, you **must bind the parent dependency to the child component's `key` prop**. This forces React's reconciliation engine to completely unmount and remount the child DOM node, flushing Mantine's internal cache and guaranteeing perfect UI synchronization. #### Master Example: Department to Role Cascade @@ -441,17 +437,21 @@ import { FieldSelect } from '@repo/ui/form'; export function DepartmentForm() { const { control, setValue, clearErrors } = useForm(); - + const department = useWatch({ control, name: 'department' }); const role = useWatch({ control, name: 'role' }); // Derive available options based on the parent state - const currentRoleOptions = department === 'IT' - ? [{ value: 'FRONTEND', label: 'Frontend' }, { value: 'BACKEND', label: 'Backend' }] - : []; + const currentRoleOptions = + department === 'IT' + ? [ + { value: 'FRONTEND', label: 'Frontend' }, + { value: 'BACKEND', label: 'Backend' }, + ] + : []; // Determine if the currently selected role is still mathematically valid - const isRoleValid = !role || (!!department && currentRoleOptions.some(opt => opt.value === role)); + const isRoleValid = !role || (!!department && currentRoleOptions.some((opt) => opt.value === role)); // 3. Reset Mode: Automatically wipes the field value in the RHF Payload if it becomes invalid useConditionalField({ @@ -460,16 +460,16 @@ export function DepartmentForm() { setValue, clearErrors, mode: 'reset', - defaultValue: '' + defaultValue: '', }); return (
- {/* CRITICAL: We bind the department string to the key prop to force remounts on change */} @@ -493,6 +493,7 @@ export function DepartmentForm() { Mantine's native `Select` and `MultiSelect` are string-based: they store `string | null` and `string[]` respectively. In enterprise applications, we often need to store **full objects** (`T | null` or `T[]`) in RHF state — for example, a user object `{ id: '1', name: 'Alice', email: 'alice@co.com' }` rather than just `'1'`. The **LocalSelect** and **AsyncSelect** engines bridge this gap by: + 1. Mapping `T[]` → `ComboboxItem[]` for Mantine rendering (via `valueKey` + `labelKey`/`renderLabel`) 2. Building an O(1) reverse lookup map (`Map`) for resolving string changes back to full objects 3. Intercepting `onChange` to pass resolved objects to RHF @@ -502,10 +503,10 @@ The **LocalSelect** and **AsyncSelect** engines bridge this gap by: ### Single vs. Multi-Select Data Mapping -| Mode | Mantine Component | RHF Value | Mantine `value` Prop | `onChange` Payload | -|---|---|---|---|---| -| `multiple={false}` (default) | `` | `T \| null` | `string \| null` | `T \| null` | +| `multiple={true}` | `` | `T[]` | `string[]` | `T[]` | ### FieldLocalSelect — Local Object Select @@ -513,18 +514,18 @@ Accepts a static `data` array of objects. No async fetching. #### Props -| Prop | Type | Required | Description | -|---|---|---|---| -| `options` | `T[]` | ✅ | Array of objects to select from | -| `valueKey` | `keyof T & string` | ✅ | Property used as the unique identifier | -| `labelKey` | `keyof T & string` | — | Property used as the display label | -| `renderLabel` | `(item: T) => string` | — | Custom label renderer (overrides `labelKey`) | -| `multiple` | `boolean` | — | Enable multi-select mode | -| `filterOption` | `(item: T, ctx) => boolean` | — | Custom filter/exclusion logic | -| `onSelect` | `(value: T \| T[] \| null) => void` | — | Side-effect callback on selection change | -| `name` | `FieldPath` | ✅ | RHF field path | -| `control` | `Control` | ✅ | RHF control object | -| _...all Mantine Select/MultiSelect props_ | | | Passed through to the underlying component | +| Prop | Type | Required | Description | +| ----------------------------------------- | ----------------------------------- | -------- | -------------------------------------------- | +| `options` | `T[]` | ✅ | Array of objects to select from | +| `valueKey` | `keyof T & string` | ✅ | Property used as the unique identifier | +| `labelKey` | `keyof T & string` | — | Property used as the display label | +| `renderLabel` | `(item: T) => string` | — | Custom label renderer (overrides `labelKey`) | +| `multiple` | `boolean` | — | Enable multi-select mode | +| `filterOption` | `(item: T, ctx) => boolean` | — | Custom filter/exclusion logic | +| `onSelect` | `(value: T \| T[] \| null) => void` | — | Side-effect callback on selection change | +| `name` | `FieldPath` | ✅ | RHF field path | +| `control` | `Control` | ✅ | RHF control object | +| _...all Mantine Select/MultiSelect props_ | | | Passed through to the underlying component | #### Usage Example @@ -573,18 +574,18 @@ Uses **Inversion of Control**: the component does NOT handle API calls directly. #### Props -| Prop | Type | Required | Description | -|---|---|---|---| -| `loadOptions` | `LoadOptionsFn` | ✅ | Async callback: `(search, page, prevOptions) => Promise<{ options: T[], hasMore?: boolean }>` | -| `defaultOptions` | `T[]` | — | Pre-loaded objects always present in dropdown (for edit forms) | -| `debounceMs` | `number` | — | Search debounce delay (default: 300) | -| `valueKey` | `keyof T & string` | ✅ | Property used as the unique identifier | -| `labelKey` | `keyof T & string` | — | Property used as the display label | -| `renderLabel` | `(item: T) => string` | — | Custom label renderer | -| `multiple` | `boolean` | — | Enable multi-select mode | -| `name` | `FieldPath` | ✅ | RHF field path | -| `control` | `Control` | ✅ | RHF control object | -| _...all Mantine Select/MultiSelect props_ | | | Passed through to the underlying component | +| Prop | Type | Required | Description | +| ----------------------------------------- | --------------------- | -------- | --------------------------------------------------------------------------------------------- | +| `loadOptions` | `LoadOptionsFn` | ✅ | Async callback: `(search, page, prevOptions) => Promise<{ options: T[], hasMore?: boolean }>` | +| `defaultOptions` | `T[]` | — | Pre-loaded objects always present in dropdown (for edit forms) | +| `debounceMs` | `number` | — | Search debounce delay (default: 300) | +| `valueKey` | `keyof T & string` | ✅ | Property used as the unique identifier | +| `labelKey` | `keyof T & string` | — | Property used as the display label | +| `renderLabel` | `(item: T) => string` | — | Custom label renderer | +| `multiple` | `boolean` | — | Enable multi-select mode | +| `name` | `FieldPath` | ✅ | RHF field path | +| `control` | `Control` | ✅ | RHF control object | +| _...all Mantine Select/MultiSelect props_ | | | Passed through to the underlying component | #### Paginated Example @@ -719,12 +720,12 @@ useEffect(() => { const targetValue = defaultValueRef.current !== undefined ? defaultValueRef.current : ''; setValue(name, targetValue); } -}, [condition, name, setValue]); +}, [condition, name, setValue]); ``` ### Zod Schema Performance: Avoid superRefine for Conditionals -For complex dynamic forms, developers often default to `.superRefine` or `.refine` to handle conditional validation (e.g., "Require Tax ID only if userType is Corporate"). +For complex dynamic forms, developers often default to `.superRefine` or `.refine` to handle conditional validation (e.g., "Require Tax ID only if userType is Corporate"). **The Problem:** `superRefine` acts as an opaque callback. Zod cannot optimize it. In large forms, doing manual `.safeParse` inside a `superRefine` loop forces Zod to parse the entire tree continuously on every keystroke, leading to severe O(n) CPU spikes. @@ -733,30 +734,34 @@ For complex dynamic forms, developers often default to `.superRefine` or `.refin #### ❌ Bad: Manual Parsing (O(n) CPU Spike) ```tsx -const badSchema = z.object({ - userType: z.enum(['PERSONAL', 'CORPORATE']), - corporateTaxId: z.string().optional() -}).superRefine((data, ctx) => { - if (data.userType === 'CORPORATE') { - // ⚠️ INCREDIBLY SLOW: Manual parsing inside refine loop - const res = taxIdValidator.safeParse(data.corporateTaxId); - if (!res.success) ctx.addIssue({ ...res.error.issues[0], path: ['corporateTaxId'] }); - } -}); +const badSchema = z + .object({ + userType: z.enum(['PERSONAL', 'CORPORATE']), + corporateTaxId: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.userType === 'CORPORATE') { + // ⚠️ INCREDIBLY SLOW: Manual parsing inside refine loop + const res = taxIdValidator.safeParse(data.corporateTaxId); + if (!res.success) ctx.addIssue({ ...res.error.issues[0], path: ['corporateTaxId'] }); + } + }); ``` #### ✅ Good: Declarative Unions (O(1) Evaluation) ```tsx -const goodSchema = z.object({ - userType: z.enum(['PERSONAL', 'CORPORATE']), - corporateTaxId: z.string().optional() -}).and( - z.discriminatedUnion('userType', [ - z.object({ userType: z.literal('PERSONAL') }), - z.object({ userType: z.literal('CORPORATE'), corporateTaxId: taxIdValidator }) - ]) -); +const goodSchema = z + .object({ + userType: z.enum(['PERSONAL', 'CORPORATE']), + corporateTaxId: z.string().optional(), + }) + .and( + z.discriminatedUnion('userType', [ + z.object({ userType: z.literal('PERSONAL') }), + z.object({ userType: z.literal('CORPORATE'), corporateTaxId: taxIdValidator }), + ]), + ); ``` By stacking `.and(z.union([...]))` for independent conditionals (like `hasSpouse`, `newsletter`, etc.), you achieve lightning-fast, type-safe conditional validation without writing a single `superRefine` loop. @@ -798,25 +803,20 @@ function LoginForm() { import { z } from 'zod'; import { useForm, type SubmitHandler } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { - FieldTextInput, - FieldNumberInput, - FieldSelect, - FieldCheckbox, -} from '@repo/ui/form'; +import { FieldTextInput, FieldNumberInput, FieldSelect, FieldCheckbox } from '@repo/ui/form'; const productSchema = z.object({ name: z.string().min(1, { - message: JSON.stringify({ key: 'validation:required', values: { field: 'Product Name' } }) + message: JSON.stringify({ key: 'validation:required', values: { field: 'Product Name' } }), }), sku: z.string().regex(/^[A-Z]{3}-\d{4}$/, { - message: JSON.stringify({ key: 'validation:invalid_format', values: { format: 'AAA-0000' } }) + message: JSON.stringify({ key: 'validation:invalid_format', values: { format: 'AAA-0000' } }), }), price: z.number().min(0, { - message: JSON.stringify({ key: 'validation:min_value', values: { min: 0 } }) + message: JSON.stringify({ key: 'validation:min_value', values: { min: 0 } }), }), category: z.string().min(1, { - message: JSON.stringify({ key: 'validation:required', values: { field: 'Category' } }) + message: JSON.stringify({ key: 'validation:required', values: { field: 'Category' } }), }), isActive: z.boolean(), }); @@ -842,12 +842,7 @@ function ProductEditor() { - + @@ -863,45 +858,43 @@ Use `withRHF` directly to wrap any Mantine component not included in the library import { DatePickerInput, type DatePickerInputProps } from '@mantine/dates'; import { withRHF } from '@repo/ui/form'; -export const FieldDatePicker = withRHF( - 'FieldDatePicker', - DatePickerInput, -); +export const FieldDatePicker = withRHF('FieldDatePicker', DatePickerInput); ``` --- ## Component Reference -| Component | Mantine Source | Type | Notes | -|---|---|---|---| -| `FieldTextInput` | `TextInput` | Text | Standard text input | -| `FieldPasswordInput` | `PasswordInput` | Text | Password with visibility toggle | -| `FieldTextarea` | `Textarea` | Text | Multi-line text | -| `FieldNumberInput` | `NumberInput` | Text | Numeric with increment/decrement | -| `FieldJsonInput` | `JsonInput` | Text | JSON-formatted text | -| `FieldPinInput` | `PinInput` | Text | PIN/OTP code input | -| `FieldAutocomplete` | `Autocomplete` | Text | Text input with suggestions | -| `FieldSelect` | `Select` | Selection | Single-value dropdown | -| `FieldMultiSelect` | `MultiSelect` | Selection | Multi-value dropdown | -| `FieldNativeSelect` | `NativeSelect` | Selection | Native `` element | +| `FieldTagsInput` | `TagsInput` | Selection | Free-form tag entry | +| `FieldCheckbox` | `Checkbox` | Toggle | Boolean checkbox (uses `checked`) | +| `FieldRadioGroup` | `Radio.Group` | Toggle | Radio button group | +| `FieldSwitch` | `Switch` | Toggle | Boolean switch (uses `checked`) | +| `FieldChipGroup` | `Chip.Group` | Toggle | Chip selection group (uses `Input.Wrapper`) | +| `FieldSegmentedControl` | `SegmentedControl` | Toggle | Segmented control (uses `Input.Wrapper`) | +| `FieldSlider` | `Slider` | Range | Single-value slider | +| `FieldRangeSlider` | `RangeSlider` | Range | Dual-handle range slider | +| `FieldRating` | `Rating` | Range | Star rating | +| `FieldColorInput` | `ColorInput` | Color | Color picker with text input | +| `FieldColorPicker` | `ColorPicker` | Color | Color picker only (uses `Input.Wrapper`) | +| `FieldLocalSelect` | `Select / MultiSelect` | Selection | Stores full `T` or `T[]` object in RHF instead of string ID. Accepts static `options` array with `valueKey`/`labelKey` mapping. | +| `FieldAsyncSelect` | `Select / MultiSelect` | Selection | Async paginated object select with IoC `loadOptions` callback. Supports search-keyed caching, `defaultOptions` for edit forms, and automatic pagination detection. | +| `FieldFileInput` | `` | `File | File[] | null` | +| `FieldRichTextEditor` | `@mantine/tiptap` | `string` (HTML) | ### Rich Text Editor (TipTap) + The `FieldRichTextEditor` component integrates [`@mantine/tiptap`](https://mantine.dev/x/tiptap/) directly with React Hook Form. It safely stores the Editor's HTML output directly into the RHF state as a `string`. Because TipTap is an uncontrolled editor natively, this field uses a specialized `useController` wrapper that automatically syncs bidirectional updates (e.g., calling `editor.commands.setContent(field.value)` when the form is reset or async default values arrive). --- diff --git a/apps/docs-dev/src/packages/ui/index.md b/apps/docs-dev/src/packages/ui/index.md index 4e87df0..7bdd8a5 100644 --- a/apps/docs-dev/src/packages/ui/index.md +++ b/apps/docs-dev/src/packages/ui/index.md @@ -16,13 +16,13 @@ The centralized UI component library for the monorepo. Provides consistent desig ## Exports -| Entry Point | Path | Description | -|---|---|---| -| `@repo/ui/components` | `./src/components/index.ts` | All components (Mantine re-exports + system pages + Form fields) | -| `@repo/ui/form` | `./src/components/Form/index.ts` | Form field components, `withRHF` factory, RHF/Zod re-exports | -| `@repo/ui/hooks` | `./src/hooks/index.ts` | Mantine hooks re-export | -| `@repo/ui/provider` | `./src/provider/index.ts` | `ThemeProvider` with color scheme and density controls | -| `@repo/ui/theme.css` | `./src/theme.css` | Base CSS with Mantine → Tailwind token mapping | +| Entry Point | Path | Description | +| --------------------- | -------------------------------- | ---------------------------------------------------------------- | +| `@repo/ui/components` | `./src/components/index.ts` | All components (Mantine re-exports + system pages + Form fields) | +| `@repo/ui/form` | `./src/components/Form/index.ts` | Form field components, `withRHF` factory, RHF/Zod re-exports | +| `@repo/ui/hooks` | `./src/hooks/index.ts` | Mantine hooks re-export | +| `@repo/ui/provider` | `./src/provider/index.ts` | `ThemeProvider` with color scheme and density controls | +| `@repo/ui/theme.css` | `./src/theme.css` | Base CSS with Mantine → Tailwind token mapping | ## 📋 Form UI Library @@ -56,12 +56,7 @@ function UserForm() { return (
- + ); @@ -76,11 +71,11 @@ The `ActionTools` suite provides flexible, responsive, and semantic action menus ## Scripts -| Command | Description | -|---|---| -| `pnpm test` | Run unit tests (Vitest) | +| Command | Description | +| ----------------- | ----------------------- | +| `pnpm test` | Run unit tests (Vitest) | | `pnpm test:watch` | Run tests in watch mode | -| `pnpm lint` | Run ESLint | +| `pnpm lint` | Run ESLint | ## Dependencies diff --git a/apps/docs-dev/src/setup.md b/apps/docs-dev/src/setup.md index a82e3e4..a3ba504 100644 --- a/apps/docs-dev/src/setup.md +++ b/apps/docs-dev/src/setup.md @@ -10,8 +10,8 @@ Ensure your local environment matches the following versions to avoid compatibility issues: -* **[Node.js](https://nodejs.org/)**: `v20+` (tested with `v24.11.1`) — required for the `--import tsx` flag used by the desktop prebuild script -* **[pnpm](https://pnpm.io/)**: `v8.15.6` +- **[Node.js](https://nodejs.org/)**: `v20+` (tested with `v24.11.1`) — required for the `--import tsx` flag used by the desktop prebuild script +- **[pnpm](https://pnpm.io/)**: `v8.15.6` (Enforced via the `packageManager` field in `package.json`) ### Installation @@ -28,43 +28,41 @@ This repository uses **[Turborepo](https://turbo.build/repo)** to orchestrate ta ### Development -| Command | Description | -| -------------------- | ---------------------------------------------------------------------------------- | -| `pnpm dev` | Start **all applications** (`web` and `docs-dev`) in parallel | -| `pnpm dev:web` | Start only the **Main Web App** (strictly at `http://localhost:5173`) | -| `pnpm dev:landing` | Start the **Public Landing App** (strictly at `http://localhost:3000`) | -| `pnpm dev:docs-dev` | Start **[VitePress](https://vitepress.dev/)** for documentation development (strictly at `http://localhost:6060`) | -| `pnpm dev:desktop` | Start the **Web App + Electron** in parallel for desktop development | +| Command | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `pnpm dev` | Start **all applications** (`web` and `docs-dev`) in parallel | +| `pnpm dev:web` | Start only the **Main Web App** (strictly at `http://localhost:5173`) | +| `pnpm dev:landing` | Start the **Public Landing App** (strictly at `http://localhost:3000`) | +| `pnpm dev:docs-dev` | Start **[VitePress](https://vitepress.dev/)** for documentation development (strictly at `http://localhost:6060`) | +| `pnpm dev:desktop` | Start the **Web App + Electron** in parallel for desktop development | -> [!NOTE] -> **Port Topology**: `electron-vite` dynamically allocates a background port (usually `5174`) for its internal renderer shell during `pnpm dev:desktop`. We strictly isolate `web` (`5173`) and `landing` (`3000`) onto separate port ranges to prevent race conditions during parallel execution. +> [!NOTE] > **Port Topology**: `electron-vite` dynamically allocates a background port (usually `5174`) for its internal renderer shell during `pnpm dev:desktop`. We strictly isolate `web` (`5173`) and `landing` (`3000`) onto separate port ranges to prevent race conditions during parallel execution. ### Building & Quality -| Command | Description | -| --------------------- | ------------------------------------------------------- | -| `pnpm build` | Build all apps and packages using Turbo cache | -| `pnpm build:web` | Build only the web application | -| `pnpm build:landing` | Build only the landing page | -| `pnpm build:docs-dev` | Build only the docs-dev application | -| `pnpm build:desktop` | Build the web app, then compile the Electron app | -| `pnpm test` | Run unit tests ([Vitest](https://vitest.dev/)) across all packages | -| `pnpm lint` | Run [ESLint](https://eslint.org/) across the workspace | -| `pnpm format` | Format code using [Prettier](https://prettier.io/) | +| Command | Description | +| --------------------- | ------------------------------------------------------------------ | +| `pnpm build` | Build all apps and packages using Turbo cache | +| `pnpm build:web` | Build only the web application | +| `pnpm build:landing` | Build only the landing page | +| `pnpm build:docs-dev` | Build only the docs-dev application | +| `pnpm build:desktop` | Build the web app, then compile the Electron app | +| `pnpm test` | Run unit tests ([Vitest](https://vitest.dev/)) across all packages | +| `pnpm lint` | Run [ESLint](https://eslint.org/) across the workspace | +| `pnpm format` | Format code using [Prettier](https://prettier.io/) | ### 🚀 Desktop Packaging & Distribution To package the application into a production-ready installer, use the following commands from the **root directory**: -| Command | Platform | Output Artifact | -| ---------------------- | ----------- | ------------------------------------------ | -| `pnpm package:desktop` | Current OS | Detects host OS and builds accordingly | -| `pnpm package:mac` | macOS | `.dmg` and `.zip` (supports x64 & arm64) | -| `pnpm package:win` | Windows | `.exe` (NSIS Installer) | -| `pnpm package:linux` | Linux | `.AppImage` | +| Command | Platform | Output Artifact | +| ---------------------- | ---------- | ---------------------------------------- | +| `pnpm package:desktop` | Current OS | Detects host OS and builds accordingly | +| `pnpm package:mac` | macOS | `.dmg` and `.zip` (supports x64 & arm64) | +| `pnpm package:win` | Windows | `.exe` (NSIS Installer) | +| `pnpm package:linux` | Linux | `.AppImage` | -> [!IMPORTANT] -> **Build Sequence**: All `package:*` commands execute the following pipeline automatically: +> [!IMPORTANT] > **Build Sequence**: All `package:*` commands execute the following pipeline automatically: > > 1. **`turbo run build --filter=web`** — Compiles the React SPA into `apps/web/dist/`. > 2. **`prebuild` hook** — Runs `node --import tsx scripts/copy-web-dist.ts`, which copies `apps/web/dist/` → `apps/desktop/web-dist/`. @@ -72,8 +70,6 @@ To package the application into a production-ready installer, use the following > > You do not need to run these steps manually — they are chained via npm scripts. -> [!WARNING] -> **macOS Code Signing**: To build a distributable macOS app with Auto-Update support, you **must** have an Apple Developer Certificate and provide `CSC_LINK` and `CSC_KEY_PASSWORD` in your environment. Without code signing, macOS Gatekeeper will block the app and auto-updates will fail. See the Desktop documentation for details. +> [!WARNING] > **macOS Code Signing**: To build a distributable macOS app with Auto-Update support, you **must** have an Apple Developer Certificate and provide `CSC_LINK` and `CSC_KEY_PASSWORD` in your environment. Without code signing, macOS Gatekeeper will block the app and auto-updates will fail. See the Desktop documentation for details. -> [!NOTE] -> **Cross-Compilation**: It is highly recommended to build for Windows on a Windows machine and for macOS on a Mac. Cross-compilation (e.g., building `.dmg` on Linux) may fail due to platform-specific toolchain dependencies. Use a CI matrix strategy (e.g., GitHub Actions with `runs-on: [macos-latest, windows-latest, ubuntu-latest]`) for multi-platform releases. +> [!NOTE] > **Cross-Compilation**: It is highly recommended to build for Windows on a Windows machine and for macOS on a Mac. Cross-compilation (e.g., building `.dmg` on Linux) may fail due to platform-specific toolchain dependencies. Use a CI matrix strategy (e.g., GitHub Actions with `runs-on: [macos-latest, windows-latest, ubuntu-latest]`) for multi-platform releases.