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:
shancheas
2026-08-25 17:50:17 +07:00
parent ff6814d038
commit f2f0be111a
48 changed files with 962 additions and 742 deletions
@@ -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
+32 -31
View File
@@ -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 |
+5 -4
View File
@@ -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>
+76 -61
View File
@@ -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
```
+92 -92
View File
@@ -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()` |
+27 -21
View File
@@ -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`.
+31 -36
View File
@@ -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
+92 -82
View File
@@ -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`.
+151 -158
View File
@@ -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).
---
+12 -17
View File
@@ -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
View File
@@ -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.