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
+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