audit m7 ranked this medium because "every caller inherits the opaque click". there are no callers. nothing has imported Combobox since the initial commit, there is no barrel that re-exports it, and nothing anywhere sets `href` on a SelectOption — so the navigate, the separator that only appeared for href options, and the href field on both declarations of the type were all unreachable. writing anchor semantics into a component that is never rendered is building, not fixing. the Command primitives it used stay; AIHarnessesSection needs them.
7.9 KiB
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:
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 avatarsCard.tsx- Custom card wrapperColorPicker.tsx- Color selectionDataTable/- Table with sorting, filtering, pagination (see below)Dialogs/- Common dialog patternsErrorDialogs/- Error display dialogsMetricCard.tsx- Stats display cardSearchInput.tsx- Search with debounceSelect.tsx- Enhanced selectSlider/- Custom sliderTabs/- Enhanced tabs
DataTable + useDataControl
A complete data table solution with sorting, filtering, and pagination.
Pattern:
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 datarawData- Original unfiltered datasearchQuery,setSearchQuery- Search statesearchKeys,setSearchKeys- Which fields to searchsortedBy,setSortedBy,sortBy- Sort statesortKey,sortDirection- Parsed sort infocurrentPage,changePage,pageCount- PaginationpageSize,setPageSize- Items per pagesetCustomSort- Custom sort function
Column options:
field- Key in data objectlabel/labelMobile- Header textsortKey- Enable sorting on this columnformat- Custom render:({ value, item, data, idx }) => ReactNodecondition- Show/hide column:boolean | () => booleantooltip- Header tooltip textheaderClassName/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:
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):
const { state, formRef, update, reset, isValid } = useForm<FormState>(
initialState,
validateFn
);
return <form ref={formRef}>...</form>;
Features:
- Auto-syncs form inputs with state via
nameattribute - Handles checkboxes, radios, selects, number inputs
- Nested object support via data attributes
- Validation function support
Other Hooks
useAuth/- Authentication state and methodsuseDebounce- Debounced valuesuseDragAndDrop- Drag and drop functionalityuseFullscreen/- Fullscreen API wrapperuseIsMobile- Responsive breakpoint detectionuseLocalStorageState- Persisted stateuseMounted- Component mount statususePopover- Popover state managementuseTimeout- Timeout managementuseTimer- Interval-based timeruseChatWebSocket- the chat socket, with reconnect + replay cursorusePanelChannel- panel-to-panel signals (refresh buses, NOT selection — selection is the URL)useJobs- background jobsuseCustomSorter,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):
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:
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, usesuseGlobalinternallyisGlobal = false: local to component, usesuseState- Auto-rebuilds URL when pathname changes (preserves global query state)
- Type coercion based on
defaultValuetype
Server State
Domain-specific hooks in state/ directories wrap React Query:
// 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:
// ✅ 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:
// ✅ 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:
import { useJobs } from 'hooks/useJobs';
type TopHeaderProps = {
manager: ReturnType<typeof useJobs>;
};
Error Handling
Toast notifications via Sonner:
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