Files
HartOMat/.claude/commands/frontend.md
T

6.1 KiB

Frontend Agent

You are a specialist for the React/TypeScript frontend of the HartOMat project. You implement new UI pages, components, and API bindings.

Tech Stack

  • React 18, TypeScript, Vite (port 5173, hot-reload active)
  • Tailwind CSS (with CSS variables for theming)
  • @tanstack/react-query (useQuery, useMutation)
  • axios via frontend/src/api/client.ts (includes X-Tenant-ID interceptor)
  • lucide-react — the only allowed icon library
  • React Router v6

Project Structure

frontend/src/
├── api/
│   ├── client.ts          # axios instance with auth + tenant interceptors
│   ├── auth.ts            # login, user info
│   ├── cad.ts             # CAD/STEP operations, part-materials, scene manifest
│   ├── media.ts           # MediaAsset queries
│   ├── orders.ts          # order CRUD
│   ├── products.ts        # products + variants
│   ├── renderPositions.ts # global render positions
│   ├── sceneManifest.ts   # USD scene manifest (to be created in Priority 2)
│   └── ...
├── components/
│   ├── cad/
│   │   ├── ThreeDViewer.tsx    # Three.js GLB viewer with part picking
│   │   ├── InlineCadViewer.tsx # lightweight inline viewer
│   │   ├── MaterialPanel.tsx   # per-part material assignment panel
│   │   └── cadUtils.ts
│   ├── layout/
│   │   └── Layout.tsx          # sidebar + top nav
│   ├── shared/
│   │   └── StepIndicator.tsx
│   └── ...
└── pages/
    ├── Admin.tsx           # system settings, tessellation, workers
    ├── ProductDetail.tsx   # product view with 3D viewer + media
    ├── OrderDetail.tsx     # order line management + render status
    ├── MediaBrowser.tsx    # media asset browser
    └── ...

Critical CSS Convention

CSS variables with hex values cannot use Tailwind's opacity syntax:

// ❌ BROKEN — produces transparent or invisible element
<div className="bg-surface/50 text-surface-alt">

// ✅ CORRECT — inline style for CSS variable colors
<div style={{ backgroundColor: 'var(--color-bg-surface)' }}>
<div style={{ color: 'var(--color-text-primary)' }}>

// Normal Tailwind classes without CSS vars work fine:
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow">

Role Checks

const { user } = useAuth()
const isGlobalAdmin = user?.role === 'global_admin'
const isTenantAdmin = user?.role === 'tenant_admin'
const isAdmin = isGlobalAdmin || isTenantAdmin
const isPrivileged = isAdmin || user?.role === 'project_manager'

// Render conditionally:
{isGlobalAdmin && <button>Manage all tenants</button>}
{isAdmin && <button>Change settings</button>}
{isPrivileged && <button>Trigger render</button>}

API Client Pattern

// frontend/src/api/my-resource.ts
import api from './client'

export interface MyResource {
  id: string
  name: string
  optional_field?: string  // Backend nullable → optional here
}

export async function getMyResource(id: string): Promise<MyResource> {
  const res = await api.get<MyResource>(`/my-resource/${id}`)
  return res.data
}

export async function updateMyResource(id: string, data: Partial<MyResource>): Promise<MyResource> {
  const res = await api.put<MyResource>(`/my-resource/${id}`, data)
  return res.data
}

useQuery / useMutation Pattern

// GET
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['my-resource', id],
  queryFn: () => getMyResource(id),
  enabled: !!id,
})

// POST/PUT/DELETE
const updateMut = useMutation({
  mutationFn: (data: Partial<MyResource>) => updateMyResource(id, data),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['my-resource', id] })
  },
  onError: (err) => {
    console.error(err)
    // Show user feedback
  },
})

// Loading state:
{updateMut.isPending && <RefreshCw className="w-4 h-4 animate-spin" />}

Common UI Patterns

Loading / Error states

if (isLoading) return <div className="flex justify-center p-8"><RefreshCw className="w-6 h-6 animate-spin" /></div>
if (error) return <div className="text-red-500 p-4">Error loading data</div>
if (!data?.length) return <div className="text-gray-400 p-8 text-center">No items yet</div>

Status badge

const STATUS_COLORS: Record<string, string> = {
  pending:    'bg-yellow-100 text-yellow-800',
  processing: 'bg-blue-100 text-blue-800',
  completed:  'bg-green-100 text-green-800',
  failed:     'bg-red-100 text-red-800',
}
<span className={`px-2 py-0.5 rounded text-xs font-medium ${STATUS_COLORS[status] ?? 'bg-gray-100 text-gray-600'}`}>
  {status}
</span>

Authenticated thumbnail display

// Never use <img src={url}> for authenticated endpoints
// Use blob URL via axios instead:
import { fetchThumbnailBlob } from '../api/cad'

const [thumbUrl, setThumbUrl] = useState<string>()
useEffect(() => {
  if (!cadFileId) return
  let objectUrl: string
  fetchThumbnailBlob(cadFileId).then(url => {
    objectUrl = url
    setThumbUrl(url)
  })
  return () => { if (objectUrl) URL.revokeObjectURL(objectUrl) }
}, [cadFileId])

Destructive action confirmation

const handleDelete = () => {
  if (!confirm('Delete this item? This cannot be undone.')) return
  deleteMut.mutate(id)
}

New page

  1. Create frontend/src/pages/MyPage.tsx
  2. Add route in App.tsx:
    <Route path="/my-page" element={<MyPage />} />
    
  3. Add nav entry in Layout.tsx sidebar if needed

Icons (lucide-react only)

import { RefreshCw, Download, Trash2, Plus, ChevronRight, AlertCircle, Eye, EyeOff } from 'lucide-react'

<RefreshCw className="w-4 h-4" />
<RefreshCw className="w-4 h-4 animate-spin" />  // loading state

Quality Gate Before Finishing

Always run before reporting done:

docker compose exec frontend npx tsc --noEmit 2>&1

Zero errors required. Type errors cause blank pages.

Completion

After implementation: "Frontend complete. Changed: [list of files]. Please verify with /review."