# 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
- `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 JobsLibrary() {
  const { jobs } = useJobs();
  const dataController = useDataControl<Job>(jobs || []);

  return (
    <>
      {/* Wire search input to controller */}
      <SearchInput
        value={dataController.searchQuery}
        handleSearch={dataController.setSearchQuery}
      />

      <DataTable<Job>
        dataController={dataController}
        pageSize={10}
        columns={[
          { field: 'id', label: 'ID', sortKey: 'id' },
          { field: 'type', label: 'Type', sortKey: 'type' },
          {
            field: 'status',
            label: 'Status',
            condition: view === 'All',  // conditional column
            format: ({ value, item }) => <Badge>{value}</Badge>
          },
          {
            label: 'Actions',
            format: ({ item }) => <ActionsCell job={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
- `useChatWebSocket` - the chat socket, with reconnect + replay cursor
- `usePanelChannel` - panel-to-panel signals (refresh buses, NOT selection — selection is the URL)
- `useJobs` - background jobs
- `useCustomSorter`, `useImageLoader`, `useIsProduction`, `usePhotoEditor`

## 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/workspaces/hooks/src/useJobs.ts
const { jobs, isLoading } = useJobs();
```

### When to Use What

| Scenario | Hook |
|----------|------|
| API data | Domain hooks (`useJobs`, `useModels`, …) |
| Shared UI state | `useGlobal` |
| URL-driven state (filters, pagination) | `useQueryState` |
| Component-only state | `useState` |
| Shared UI state that must survive a refresh, per tab | `useSessionState` |
| 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 { useJobs } from 'hooks/useJobs';

type TopHeaderProps = {
  manager: ReturnType<typeof useJobs>;
};
```

## 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`.

## Further Reading

- `../../CLAUDE.md` — architecture and the frontend route conventions
- `../../docs/navigation-audit.md` — **authoritative** on routing and selection. The short version:
  addressable state lives in the URL (`useParams` / `?selected=`), never in a channel or a global; rows
  and nav items are real `<Link>`/`<NavLink>`s. Read it before building a screen that selects things.
- `../../CONVENTIONS.md` — component organisation and React patterns
- `../workspaces/officerdev/APP_CONVENTIONS.md` — panel apps and the AppRegistry
