Files
platform/src/apps/CLAUDE.md
T
pastilhas 32aa1e7cc3 delete the combobox instead of giving it anchor semantics
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.
2026-08-07 12:35:58 +00:00

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

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:

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 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):

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, 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:

// 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.mdauthoritative 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