Adding a component
Follow this checklist when introducing a new documented component to Opus.
1. Implement the component
Create the React component and CSS module:
components/MyComponent.tsx
components/MyComponent.module.css
For form controls, wrap content in FieldShell:
components/fields/MyField.tsx
components/fields/MyField.module.css
Match existing patterns for naming, prop types, and CSS token usage.
2. Export it
Add to components/fields/index.ts (re-exported by components/index.ts):
export { MyComponent } from "../MyComponent";
3. Register the slug
File: lib/controls/types.ts
Add to the ControlSlug union:
| "my-component"
Add a settings interface and extend ControlSettingsBySlug:
export type MyComponentSettings = {
label: string;
// …demo props
};
If the component is in forms, add the slug to formsControlOrder.
4. Add defaults
File: lib/controls/defaults.ts
"my-component": {
label: "Example",
// …
},
5. Add registry entry
File: lib/controls/registry.ts
For forms, add to formsControls. For content/overlays, add to the controls array:
{
slug: "my-component",
title: "My component",
category: "content",
componentName: "MyComponent",
description: "Short description for overview cards.",
sourceFiles: ["components/MyComponent.tsx", "components/MyComponent.module.css"],
usesFieldShell: false,
},
6. Add an icon
File: lib/controls/componentIcons.ts
"my-component": faSomeIcon,
Import the icon from @fortawesome/free-solid-svg-icons.
7. Wire the detail page
Three switches need a new case:
Preview — components/control-detail/ControlPreview.tsx
case "my-component": {
const s = settings as ControlSettingsBySlug["my-component"];
return <MyComponent label={s.label} />;
}
Settings — components/control-detail/ControlSettingsPanel.tsx
Add inputs (SettingInput, SettingSelect, SettingToggle, …) that call onChange with updated settings.
Usage code — lib/controls/generateUsageCode.ts
case "my-component": {
const s = settings as ControlSettingsBySlug["my-component"];
return `import { MyComponent } from "@/components/fields";
export function Example() {
return (
<MyComponent${formatStringProp("label", s.label)} />
);
}`;
}
Use existing formatStringProp, formatBoolProp, etc. helpers in that file.
8. Category overview (optional)
Add a live demo to the relevant overview if it needs more than the auto-generated hub card:
ContentOverview.tsx— usesOverviewDemoCardFormsOverview.tsx— add to thedemosmapOverlaysOverview.tsx— usesOverviewDemoCard
The main hub overview (ComponentsHubOverview) picks up new registry entries automatically.
9. Add documentation
File: lib/controls/componentDocumentation.ts
Add a markdown entry for the slug with Overview, When to use, Props, and Accessibility sections. You can optionally override it with a file at documentation/content/components/my-component.md.
The detail page renders this in the Documentation panel between Preview and Usage.
10. Verify
npm run build # confirms SSG includes the new slug
npm run dev # visit /documentation/components/my-component
Check:
- Sidebar shows the component under the correct category
- Preview renders and responds to settings
- Usage code copies valid JSX
- Icon appears in sidebar and hub grid
- Documentation panel shows overview, props table, and accessibility notes
- Light and dark themes look correct
Route generation
No route file changes are required. generateStaticParams in app/documentation/components/[slug]/page.tsx reads all slugs from the registry:
export function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
Common pitfalls
- Forgetting one of the three switches (preview, settings, codegen) — the detail page will break or show stale output for that slug.
- Hardcoding colours instead of CSS tokens — breaks theme switching.
- Portal components without
data-themeon the portal root — CSS variables fall back to wrong values. - Form controls without
FieldShellaccessibility wiring — labels and errors won't associate correctly.
