Navigation: Root AGENTS.md → Web → Components
All UI must be built from primitives. The ui_primitives/ directory holds the reusable, theme-driven components. Read the Primitives Strategy before writing any frontend code.
Never import raw MUI components in component files. The following MUI imports are banned outside of ui_primitives/ and editor_ui/:
BANNED: import { Typography, Button, IconButton, Tooltip, CircularProgress,
Chip, Dialog, Alert, Divider, Paper, Skeleton, Tabs, Tab, Drawer,
Breadcrumbs, Select, Switch, TextField, LinearProgress } from "@mui/material";
Use the corresponding primitive instead:
| Instead of (raw MUI) | Use (primitive) |
|---|---|
<Typography> |
Text, Label, Caption, TruncatedText |
<Button> |
EditorButton, or a semantic button (CopyButton, DeleteButton, etc.) |
<IconButton> + <Tooltip> |
ToolbarIconButton, StateIconButton, or action buttons |
<CircularProgress> |
LoadingSpinner |
<Tooltip> |
Tooltip primitive |
<Chip> |
Chip primitive |
<Dialog> |
Dialog primitive |
<Alert> |
AlertBanner |
<Divider> |
Divider primitive |
<Paper> |
Card, Surface, or Panel |
<Skeleton> |
Skeleton primitive |
<Tabs>/<Tab> |
TabGroup / TabPanel |
<Breadcrumbs> |
Breadcrumbs primitive |
<TextField> |
NodeTextField, TextInput, or SearchInput |
<Select> |
NodeSelect, SelectField |
<Switch> |
NodeSwitch, LabeledSwitch |
<LinearProgress> |
ProgressBar |
display: "flex" in sx |
FlexRow or FlexColumn |
textOverflow: "ellipsis" |
TruncatedText |
overflow: "auto" scroll container |
ScrollArea or Container |
| Empty/no-data message | EmptyState |
| Label + input + helper text | FormField |
| Section title + action button | SectionHeader |
| Expand/collapse pattern | CollapsibleSection |
import { FlexColumn, Text, LoadingSpinner, Card } from "../ui_primitives";All primitives are re-exported from ui_primitives/index.ts. Use relative imports from the component's location.
When editing any component file for any reason, also migrate raw MUI usage in that file to primitives. That incremental migration is the only thing draining the raw MUI usage still left in the codebase.
If no existing primitive fits your use case:
- Create the component in
ui_primitives/ - Use
useTheme()for all styling — no hardcoded values - Support the
sxprop for overrides - Define a TypeScript props interface
- Export from
ui_primitives/index.ts - Add tests in
ui_primitives/__tests__/ - Update the Strategy doc
- Use functional components only. No class components.
- Define a TypeScript interface for all component props.
- Keep components focused on a single responsibility.
- Use composition over inheritance and deep prop drilling.
- Use
React.memoonly when the component is pure, receives stable props, renders often, and is expensive to render. - Don't create new inline objects/functions in JSX when passing to memoized children.
- Co-locate tests in
__tests__/subdirectories next to source files.
Anything a user sees when something failed — an error box, a crashed panel, a
failed job, an error notification — carries a Report control that opens the
dialog in components/support/. A new error surface without one is a dead end:
the user reads a stack trace they cannot send anywhere.
Wire it with ReportBugButton (or openBugReport() from
stores/BugReportStore when you are inside a class error boundary):
<ReportBugButton
context={{
source: "panel-crash",
summary: "Assets panel failed to render",
errorText: error.message,
stackTrace: error.stack
}}
/>source picks the label and the issue title; the rest is optional. Already
wired: NodeErrors, ErrorBoundary, PanelErrorBoundary,
SearchErrorBoundary, NotificationButton (error notifications), JobItem,
the metadata boot-failure screen in index.tsx, and the command menu.
Two rules for the payload:
- The dialog cannot read editor-scoped stores. It mounts at the app root,
outside
NodeContext, so anything only the surface can see — a node's properties, its wiring — travels incontext.nodeDetailas formatted text. - Redaction happens in
utils/bugReportBundle.ts, not at the call site. Pass the real error text;redactSecretsInTextstrips credential shapes andredactDeepdrops secret-keyed values out of the graph. If you find a credential format that survives, add the pattern there with a test.
The user-facing flow is documented in docs/troubleshooting.md.
From shipped fixes in node_editor/, node/, and the Inspector:
- Per-type handle/edge colors are generated CSS scoped to the canvas wrapper,
not inherited globally. When you move or rename the component hosting the
ReactFlow canvas, carry the
css={generateCSS}injection onto the new wrapper — drop it and every handle falls back to grey. - A property whose value is driven by a connected input edge must be rendered
inert and dimmed — never editable. Plumb
isConnectedintoPropertyInputand use theinertattribute (not justdisabled/pointer-events) so the field also leaves the tab order; editing a connection-driven value is a no-op that confuses users. - Don't write
x !== nullafter a truthiness guard (x && …). The truthy check already excludesnull/undefined; the extra comparison is dead code and gets flagged by CodeQL ("comparison between inconvertible types"). This bit several node-status guards inBaseNode/Dynamic*Node.
See docs/DESIGN.md for the full design token reference and migration checklist. Summary of the mandatory rules:
- Use
sxprop on primitives for one-off style overrides. - Use
styled()only insideui_primitives/for defining new primitives — not in regular component files. - Use theme values (
theme.spacing(),theme.vars.palette,theme.fontSizeSmall, etc.) — never hardcode colors, fonts, or spacing. - No inline
display: "flex"— useFlexRow/FlexColumnprimitives. - No hardcoded hex colors (
#fff,#000,rgb(...)) — use theme palette values. - Spacing: use
SPACING.*/GAP.*/PADDING.*— forbidden:5px,10px,13px,20px, or any off-grid pixel value. - Typography: use
<Text>/<Label>/<Caption>orTYPOGRAPHY.*— forbidden: rawfontSizepx/rem literals, weights700 / "bold" / 300. - Border radius: use
BORDER_RADIUS.xs / sm / md / lg / xl / xxl / pill / circle— forbidden: magic numbers (4,10,18), raw"var(--rounded-*)"strings where a constant exists. - Transitions: use
MOTION.all / border / background / transform / opacity / shadow— forbidden: raw timing strings ("all 200ms ease"). - Z-index: use
Z_INDEX.*— forbidden: raw integers (9999,1000).
cd web
npm test # Run all tests
npm run test:watch # Watch mode
npm test -- --testPathPattern=components # Components only- Use React Testing Library queries (
getByRole,getByLabelText,getByText). - Use
userEventfor interactions. - Test user-facing behavior, not implementation details.
- Mock external dependencies (stores, API calls).