chore: update .gitignore and improve coding standards documentation
- Added .cursor/sessions/* to .gitignore to prevent session files from being tracked. - Enhanced coding standards in SKILL.md by adding semicolons to TypeScript examples for consistency. - Improved formatting in continuous learning, detail layout, and other SKILL.md files for better readability. These changes aim to streamline development processes and maintain code quality across the project.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(<Button onClick={onClick}>Save</Button>)
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
const onClick = vi.fn();
|
||||
render(<Button onClick={onClick}>Save</Button>);
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(<FieldTextInput name="code" label="Code" />)
|
||||
expect(screen.getByLabelText('Code')).toBeInTheDocument()
|
||||
})
|
||||
render(<FieldTextInput name="code" label="Code" />);
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <name>` - Create named checkpoint
|
||||
- `verify <name>` - Verify against named checkpoint
|
||||
- `list` - Show all checkpoints
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <name>` - Create new eval definition
|
||||
- `check <name>` - Run and check evals
|
||||
- `report <name>` - Generate full report
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
|
||||
@@ -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 <description>` - Full feature workflow
|
||||
- `bugfix <description>` - Bug fix workflow
|
||||
- `refactor <description>` - Refactoring workflow
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
.cursor/sessions/*
|
||||
|
||||
# Turborepo & Cache
|
||||
.turbo
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/<DESKTOP_TARGET_APP>/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=<base64-encoded .p12 certificate>
|
||||
> CSC_KEY_PASSWORD=<certificate password>
|
||||
@@ -105,10 +105,11 @@ All artifacts are emitted to the `release/` directory.
|
||||
> APPLE_APP_SPECIFIC_PASSWORD=<app-specific password>
|
||||
> APPLE_TEAM_ID=<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 |
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
<div class="custom-divider"></div>
|
||||
<div class="bento-container">
|
||||
<div class="bento-header">
|
||||
@@ -262,4 +263,4 @@ features:
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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<string, string>` | `{}` | 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<string, string>` | `{}` | 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<string \| RegExp>` | — | 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<string \| RegExp>` | — | 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<BookingEntity>(
|
||||
apiClient,
|
||||
{
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
},
|
||||
);
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(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<BookingEntity>(
|
||||
apiClient,
|
||||
{ apiUrl: '/bookings', moduleKey: 'BOOKING' },
|
||||
);
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(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` |
|
||||
| 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` |
|
||||
|
||||
@@ -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<TEntity, TDTO> {
|
||||
|
||||
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<MyEntity, MyDTO> {
|
||||
transformToEntity(dto: MyDTO): MyEntity { /* ... */ }
|
||||
transformToDTO(entity: MyEntity): MyDTO { /* ... */ }
|
||||
transformToEntity(dto: MyDTO): MyEntity {
|
||||
/* ... */
|
||||
}
|
||||
transformToDTO(entity: MyEntity): MyDTO {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -296,8 +301,12 @@ class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
|
||||
|
||||
```typescript
|
||||
class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
|
||||
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<MyEntity, MyDTO> {
|
||||
|
||||
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()` |
|
||||
|
||||
@@ -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 (
|
||||
<table>
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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<Item>({
|
||||
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<Item>({
|
||||
|
||||
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<ItemEntity>({
|
||||
localName: 'master_db',
|
||||
remoteUrl: 'http://admin:pass@localhost:5984/master_db'
|
||||
}, 'item');
|
||||
export const itemDB = envelopeDbManager.register<ItemEntity>(
|
||||
{
|
||||
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<BookingEntity>({
|
||||
localName: 'master_db',
|
||||
remoteUrl: 'http://admin:pass@localhost:5984/master_db'
|
||||
}, 'booking');
|
||||
export const bookingDB = envelopeDbManager.register<BookingEntity>(
|
||||
{
|
||||
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`.
|
||||
> 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`.
|
||||
|
||||
@@ -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: <FileText size={16} />,
|
||||
onClick: (k) => console.log(k),
|
||||
},
|
||||
{
|
||||
key: 'print-copy',
|
||||
label: 'Print Copy',
|
||||
icon: <FileText size={16} />,
|
||||
onClick: (k) => console.log(k)
|
||||
{
|
||||
key: 'print-copy',
|
||||
label: 'Print Copy',
|
||||
icon: <FileText size={16} />,
|
||||
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
|
||||
|
||||
@@ -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 `<AppShell.Navbar>` 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 `<Burger>` 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 `<Burger>` 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 `<CoreAppShell>` 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 `<CoreAppShell>` 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: <MyHeader />,
|
||||
sidebar: (
|
||||
<Stack p="md" gap="xs">
|
||||
<Button variant="subtle" fullWidth>Dashboard</Button>
|
||||
<Button variant="subtle" fullWidth>Settings</Button>
|
||||
<Button variant="subtle" fullWidth>
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button variant="subtle" fullWidth>
|
||||
Settings
|
||||
</Button>
|
||||
</Stack>
|
||||
),
|
||||
}}
|
||||
@@ -300,7 +301,9 @@ function AppHeader() {
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||
<Text fw={700} size="lg">Enterprise Dashboard</Text>
|
||||
<Text fw={700} size="lg">
|
||||
Enterprise Dashboard
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
@@ -393,7 +396,9 @@ function App() {
|
||||
),
|
||||
sidebarPanel: (
|
||||
<Box p="md">
|
||||
<Text fw={700} mb="sm">Navigation</Text>
|
||||
<Text fw={700} mb="sm">
|
||||
Navigation
|
||||
</Text>
|
||||
{/* Contextual links based on active rail icon */}
|
||||
</Box>
|
||||
),
|
||||
@@ -429,14 +434,17 @@ function ShellDemo() {
|
||||
const [collapseVariant, setCollapseVariant] = useState<DesktopCollapseVariant>('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 (
|
||||
<CoreAppShell config={config} slots={{ header: <MyHeader />, sidebar: <MySidebar /> }}>
|
||||
@@ -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={
|
||||
<Group justify="space-between">
|
||||
<Text component="h1" size="xl" fw={700}>Users</Text>
|
||||
<Text component="h1" size="xl" fw={700}>
|
||||
Users
|
||||
</Text>
|
||||
<Button>Add User</Button>
|
||||
</Group>
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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<MantineComponentProps>(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<TextInputProps>('FieldTextInput', TextInput);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Performance & Memoization
|
||||
@@ -153,10 +151,10 @@ export const FieldTextInput = withRHF<TextInputProps>('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 <form>...</form>;
|
||||
@@ -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 (
|
||||
<form>
|
||||
<FieldSelect
|
||||
name="department"
|
||||
control={control}
|
||||
label="Department"
|
||||
data={[{ value: 'IT', label: 'Information Technology' }]}
|
||||
<FieldSelect
|
||||
name="department"
|
||||
control={control}
|
||||
label="Department"
|
||||
data={[{ value: 'IT', label: 'Information Technology' }]}
|
||||
/>
|
||||
|
||||
{/* 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<string, T>`) 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) | `<Select />` | `T \| null` | `string \| null` | `T \| null` |
|
||||
| `multiple={true}` | `<MultiSelect />` | `T[]` | `string[]` | `T[]` |
|
||||
| Mode | Mantine Component | RHF Value | Mantine `value` Prop | `onChange` Payload |
|
||||
| ---------------------------- | ----------------- | ----------- | -------------------- | ------------------ |
|
||||
| `multiple={false}` (default) | `<Select />` | `T \| null` | `string \| null` | `T \| null` |
|
||||
| `multiple={true}` | `<MultiSelect />` | `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<T>` | ✅ | 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<T>` | ✅ | 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() {
|
||||
<FieldTextInput name="name" control={control} label="Product Name" />
|
||||
<FieldTextInput name="sku" control={control} label="SKU" placeholder="ABC-1234" />
|
||||
<FieldNumberInput name="price" control={control} label="Price" min={0} prefix="$" />
|
||||
<FieldSelect
|
||||
name="category"
|
||||
control={control}
|
||||
label="Category"
|
||||
data={['Electronics', 'Clothing', 'Food']}
|
||||
/>
|
||||
<FieldSelect name="category" control={control} label="Category" data={['Electronics', 'Clothing', 'Food']} />
|
||||
<FieldCheckbox name="isActive" control={control} label="Active" />
|
||||
<button type="submit">Save Product</button>
|
||||
</form>
|
||||
@@ -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<DatePickerInputProps>(
|
||||
'FieldDatePicker',
|
||||
DatePickerInput,
|
||||
);
|
||||
export const FieldDatePicker = withRHF<DatePickerInputProps>('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 `<select>` 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` | `<FileInput />` | `File | File[] | null` |
|
||||
| `FieldRichTextEditor` | `@mantine/tiptap` | `string` (HTML) |
|
||||
| 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 `<select>` 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` | `<FileInput />` | `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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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 (
|
||||
<form onSubmit={handleSubmit(console.log)}>
|
||||
<FieldTextInput name="name" control={control} label="Name" />
|
||||
<FieldSelect
|
||||
name="role"
|
||||
control={control}
|
||||
label="Role"
|
||||
data={['Admin', 'Editor', 'Viewer']}
|
||||
/>
|
||||
<FieldSelect name="role" control={control} label="Role" data={['Admin', 'Editor', 'Viewer']} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
@@ -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
|
||||
|
||||
|
||||
+29
-33
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user