first
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
# Frontend Apps
|
||||
|
||||
Shared patterns and conventions for all frontend applications (dashboard, editor, runtime).
|
||||
|
||||
## UI Components
|
||||
|
||||
### shadcn/ui (Base)
|
||||
Location: `src/workspaces/components/ui/`
|
||||
|
||||
Standard shadcn/ui components with Tailwind CSS. Import via:
|
||||
```tsx
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
```
|
||||
|
||||
### Custom Components
|
||||
Location: `src/workspaces/components/`
|
||||
|
||||
Built on top of shadcn primitives:
|
||||
- `Avatar.tsx` - User avatars
|
||||
- `Card.tsx` - Custom card wrapper
|
||||
- `Combobox.tsx` - Searchable select
|
||||
- `ColorPicker.tsx` - Color selection
|
||||
- `DataTable/` - Table with sorting, filtering, pagination (see below)
|
||||
- `Dialogs/` - Common dialog patterns
|
||||
- `ErrorDialogs/` - Error display dialogs
|
||||
- `MetricCard.tsx` - Stats display card
|
||||
- `SearchInput.tsx` - Search with debounce
|
||||
- `Select.tsx` - Enhanced select
|
||||
- `Slider/` - Custom slider
|
||||
- `Tabs/` - Enhanced tabs
|
||||
|
||||
### DataTable + useDataControl
|
||||
|
||||
A complete data table solution with sorting, filtering, and pagination.
|
||||
|
||||
**Pattern:**
|
||||
```tsx
|
||||
import { DataTable, useDataControl } from '@/components/DataTable';
|
||||
|
||||
function ExperimentsLibrary() {
|
||||
const { experiments } = useExperimentsList();
|
||||
const dataController = useDataControl<Experiment>(experiments || []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Wire search input to controller */}
|
||||
<SearchInput
|
||||
value={dataController.searchQuery}
|
||||
handleSearch={dataController.setSearchQuery}
|
||||
/>
|
||||
|
||||
<DataTable<Experiment>
|
||||
dataController={dataController}
|
||||
pageSize={10}
|
||||
columns={[
|
||||
{ field: 'id', label: 'ID', sortKey: 'id' },
|
||||
{ field: 'name', label: 'Name', sortKey: 'name' },
|
||||
{
|
||||
field: 'status',
|
||||
label: 'Status',
|
||||
condition: view === 'All', // conditional column
|
||||
format: ({ value, item }) => <Badge>{value}</Badge>
|
||||
},
|
||||
{
|
||||
label: 'Actions',
|
||||
format: ({ item }) => <ActionsCell experiment={item} />
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**useDataControl returns:**
|
||||
- `data` - Current page of filtered/sorted data
|
||||
- `rawData` - Original unfiltered data
|
||||
- `searchQuery`, `setSearchQuery` - Search state
|
||||
- `searchKeys`, `setSearchKeys` - Which fields to search
|
||||
- `sortedBy`, `setSortedBy`, `sortBy` - Sort state
|
||||
- `sortKey`, `sortDirection` - Parsed sort info
|
||||
- `currentPage`, `changePage`, `pageCount` - Pagination
|
||||
- `pageSize`, `setPageSize` - Items per page
|
||||
- `setCustomSort` - Custom sort function
|
||||
|
||||
**Column options:**
|
||||
- `field` - Key in data object
|
||||
- `label` / `labelMobile` - Header text
|
||||
- `sortKey` - Enable sorting on this column
|
||||
- `format` - Custom render: `({ value, item, data, idx }) => ReactNode`
|
||||
- `condition` - Show/hide column: `boolean | () => boolean`
|
||||
- `tooltip` - Header tooltip text
|
||||
- `headerClassName` / `cellClassName` - Styling
|
||||
|
||||
**Features:**
|
||||
- Auto-generates columns from data if not specified
|
||||
- Resets to page 1 on search/sort change
|
||||
- Search strips diacritics for accent-insensitive matching
|
||||
- Built-in pagination bar (shows when data exceeds pageSize)
|
||||
|
||||
## Hooks
|
||||
|
||||
Location: `src/workspaces/hooks/src/`
|
||||
|
||||
### Data Fetching - `useClient`
|
||||
HTTP client with auth token handling:
|
||||
```tsx
|
||||
const client = useClient();
|
||||
const data = await client.get<User[]>("/users");
|
||||
await client.post("/experiments", payload);
|
||||
```
|
||||
|
||||
Methods: `get`, `getText`, `getBlob`, `post`, `put`, `patch`, `delete`
|
||||
|
||||
Auto-attaches Bearer token from localStorage/sessionStorage.
|
||||
|
||||
### Forms - `useForm`
|
||||
Custom form hook (not react-hook-form):
|
||||
```tsx
|
||||
const { state, formRef, update, reset, isValid } = useForm<FormState>(
|
||||
initialState,
|
||||
validateFn
|
||||
);
|
||||
|
||||
return <form ref={formRef}>...</form>;
|
||||
```
|
||||
|
||||
Features:
|
||||
- Auto-syncs form inputs with state via `name` attribute
|
||||
- Handles checkboxes, radios, selects, number inputs
|
||||
- Nested object support via data attributes
|
||||
- Validation function support
|
||||
|
||||
### Other Hooks
|
||||
- `useAuth/` - Authentication state and methods
|
||||
- `useDebounce` - Debounced values
|
||||
- `useDragAndDrop` - Drag and drop functionality
|
||||
- `useFullscreen/` - Fullscreen API wrapper
|
||||
- `useIsMobile` - Responsive breakpoint detection
|
||||
- `useLocalStorageState` - Persisted state
|
||||
- `useMounted` - Component mount status
|
||||
- `usePopover` - Popover state management
|
||||
- `useTimeout` - Timeout management
|
||||
- `useTimer` - Interval-based timer
|
||||
- `useWebsockets` - WebSocket connection
|
||||
|
||||
## State Management
|
||||
|
||||
Uses React Query cache as both server and client state manager.
|
||||
|
||||
### Global State - `useGlobal`
|
||||
|
||||
Uses React Query cache as a global state store (no Context providers needed):
|
||||
|
||||
```tsx
|
||||
const [value, setValue, refresh, reset] = useGlobal<string>("SIDEBAR_STATE", "expanded");
|
||||
|
||||
// Any component using the same key shares state and reacts to changes
|
||||
setValue("collapsed");
|
||||
```
|
||||
|
||||
How it works:
|
||||
- `enabled: false` + `staleTime: Infinity` = never fetches, just stores
|
||||
- Automatic re-renders when state changes
|
||||
- Visible in React Query DevTools
|
||||
- Supports functional updates: `setValue(prev => ...)`
|
||||
|
||||
### URL Query State - `useQueryState`
|
||||
|
||||
Syncs state with URL query parameters:
|
||||
|
||||
```tsx
|
||||
const [page, setPage, reset, clear] = useQueryState<number>("page", 1);
|
||||
const [filter, setFilter] = useQueryState<string>("filter", null, true); // isGlobal = true
|
||||
```
|
||||
|
||||
Features:
|
||||
- Updates URL via `history.replaceState` (no page reload)
|
||||
- `isGlobal = true`: persists across navigation, uses `useGlobal` internally
|
||||
- `isGlobal = false`: local to component, uses `useState`
|
||||
- Auto-rebuilds URL when pathname changes (preserves global query state)
|
||||
- Type coercion based on `defaultValue` type
|
||||
|
||||
### Server State
|
||||
|
||||
Domain-specific hooks in `state/` directories wrap React Query:
|
||||
|
||||
```tsx
|
||||
// src/apps/dashboard/state/experiments/useExperiment.ts
|
||||
const { data, isLoading } = useExperiment(experimentId);
|
||||
```
|
||||
|
||||
### When to Use What
|
||||
|
||||
| Scenario | Hook |
|
||||
|----------|------|
|
||||
| API data | Domain hooks (`useExperiment`, etc.) |
|
||||
| Shared UI state | `useGlobal` |
|
||||
| URL-driven state (filters, pagination) | `useQueryState` |
|
||||
| Component-only state | `useState` |
|
||||
| Persisted to localStorage | `useLocalStorageState` |
|
||||
|
||||
## Event Handlers
|
||||
|
||||
**Always use `ev` for event parameters**, not `e`:
|
||||
```tsx
|
||||
// ✅ Good
|
||||
onChange={(ev) => setName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
const handleSubmit = async (ev: React.FormEvent) => {
|
||||
ev.preventDefault();
|
||||
// ...
|
||||
};
|
||||
|
||||
// ❌ Bad
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
```
|
||||
|
||||
## React 19 Patterns
|
||||
|
||||
**RefObject includes null**: In React 19, `useRef` returns `RefObject<T | null>`. Update prop types accordingly:
|
||||
```tsx
|
||||
// ✅ Good - allow null in ref type
|
||||
type PanelProps = {
|
||||
triggerRef: React.RefObject<HTMLButtonElement | null>;
|
||||
};
|
||||
|
||||
// ❌ Bad - will error when passing useRef result
|
||||
type PanelProps = {
|
||||
triggerRef: React.RefObject<HTMLButtonElement>;
|
||||
};
|
||||
```
|
||||
|
||||
**Hook return types**: Use `ReturnType<typeof hookName>` for typing hook returns in props:
|
||||
```tsx
|
||||
import { useExperimentsList } from '@/state/experiments/useExperimentsList';
|
||||
|
||||
type TopHeaderProps = {
|
||||
manager: ReturnType<typeof useExperimentsList>;
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Toast notifications** via Sonner:
|
||||
```tsx
|
||||
import { toast } from "sonner";
|
||||
|
||||
toast.error("Something went wrong");
|
||||
toast.success("Saved successfully");
|
||||
```
|
||||
|
||||
API errors automatically trigger via `useClient.config.onError`.
|
||||
|
||||
## App-Specific Docs
|
||||
|
||||
- [Dashboard](./dashboard/CLAUDE.md) - Admin UI specifics
|
||||
- [Editor](./editor/CLAUDE.md) - Visual editor specifics
|
||||
- [Runtime](./runtime/CLAUDE.md) - Injected scripts specifics
|
||||
Reference in New Issue
Block a user