This commit is contained in:
2026-02-16 19:34:35 +00:00
commit 9ab0940ca4
784 changed files with 41710 additions and 0 deletions
+265
View File
@@ -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