Create components that work with RadTools visual editor and component discovery. Use when building new UI components for projects using RadTools.
Build components that integrate with RadTools' visual editor, component discovery, and design system.
Use this skill when:
Before creating a new component:
Shift+Cmd+K) → Components tabOnly create new components when necessary.
RadTools component discovery has strict requirements. All are mandatory.
// ✅ Correct - default export
export default function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
// ❌ Wrong - named export (will not be discovered)
export function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
The visual editor renders components without props. All optional props must have defaults.
// ✅ Correct - all optional props have defaults in destructuring
interface ButtonProps {
variant?: 'default' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
children: React.ReactNode; // Required props don't need defaults
}
export default function Button({
variant = 'default',
size = 'md',
disabled = false,
children
}: ButtonProps) {
// ...
}
// ❌ Wrong - optional props without defaults (visual editor will break)
export default function Button({
variant, // undefined causes rendering issues
size,
children
}: ButtonProps) {
// ...
}
Note: The component scanner extracts default values from destructuring patterns. Defaults defined elsewhere (e.g., defaultProps) won't be detected.
Props must be typed for discovery to extract prop information.
// ✅ Correct - explicit interface
interface CardProps {
title?: string;
children: React.ReactNode;
}
export default function Card({ title = '', children }: CardProps) {
// ...
}
Components must be in /components/ structure:
| Location | Purpose | Tab in RadTools |
|---|---|---|
/components/ui/ |
Design system primitives (Button, Card, Input) | "Design System" tab |
/components/[FolderName]/ |
Grouped components (e.g., /components/Rad_os/) |
Dynamic folder tab |
/components/*.tsx |
Ungrouped project components | Scanned on demand |
Scanner ignores: Files matching .test., .stories., or missing default exports.
Use tokens from the @theme block in globals.css. Check RadTools → Variables tab for available tokens.
// ✅ Correct - radOS design tokens
<button className="bg-sun-yellow text-black border-black hover:bg-sunset-fuzz">
<div className="bg-cream border border-black rounded-md">
<span className="text-sun-red">Error message</span>
// ❌ Wrong - hardcoded Tailwind palette colors
<button className="bg-yellow-400 text-gray-900">
<button className="bg-[#FCE184]">
Available radOS tokens:
| Category | Tokens |
|---|---|
| Brand | sun-yellow, sky-blue, cream, black, white, warm-cloud, sunset-fuzz, sun-red, green |
| System | success-green, error-red, focus-state |
| Radius | rounded-none, rounded-xs, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-full |
// /components/ui/ComponentName.tsx
interface ComponentNameProps {
variant?: 'default' | 'outline';
size?: 'sm' | 'md' | 'lg';
className?: string;
children: React.ReactNode;
}
export default function ComponentName({
variant = 'default',
size = 'md',
className = '',
children
}: ComponentNameProps) {
const variants = {
default: 'bg-cream border border-black text-black',
outline: 'bg-transparent border-2 border-black text-black',
};
const sizes = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<div className={`rounded-md ${variants[variant]} ${sizes[size]} ${className}`}>
{children}
</div>
);
}
Previews enable visual editing in the Components tab. They load dynamically by folder name.
/components/Rad_os/ → loads ./previews/Rad_os.tsx
/components/Forms/ → loads ./previews/Forms.tsx
/components/ui/ → uses DesignSystemTab (built-in)
The file name must exactly match the folder name (case-sensitive).
Create /devtools/tabs/ComponentsTab/previews/{FolderName}.tsx:
// /devtools/tabs/ComponentsTab/previews/MyComponents.tsx
// ↑ File name matches folder: /components/MyComponents/
'use client';
import React from 'react';
import ComponentName from '@/components/MyComponents/ComponentName';
import AnotherComponent from '@/components/MyComponents/AnotherComponent';
// Helper for displaying props
function PropsDisplay({ props }: { props: string }) {
return (
<code className="bg-black/5 px-2 py-1 rounded-sm block mt-2 text-sm">
{props}
</code>
);
}
export default function Preview() {
return (
<div className="space-y-6">
{/* Section for ComponentName */}
<div className="p-4 border border-black bg-cream rounded">
<h3 className="mb-3 border-b border-black/20 pb-2">ComponentName</h3>
{/* Base component - edits affect all variants */}
<ComponentName
variant="default"
data-edit-scope="component-definition"
data-component="ComponentName"
>
Default variant
</ComponentName>
<PropsDisplay props="variant='default'" />
{/* Variant - edits affect only this variant */}
<ComponentName
variant="outline"
data-edit-scope="component-definition"
data-component="ComponentName"
data-edit-variant="outline"
>
Outline variant
</ComponentName>
<PropsDisplay props="variant='outline'" />
{/* Size variations - preview only (no data attributes) */}
<div className="flex gap-2 items-center mt-4">
<ComponentName size="sm">Small</ComponentName>
<ComponentName size="md">Medium</ComponentName>
<ComponentName size="lg">Large</ComponentName>
</div>
<PropsDisplay props="size='sm' | 'md' | 'lg'" />
</div>
{/* Section for AnotherComponent */}
<div className="p-4 border border-black bg-cream rounded">
<h3 className="mb-3 border-b border-black/20 pb-2">AnotherComponent</h3>
<AnotherComponent
data-edit-scope="component-definition"
data-component="AnotherComponent"
/>
</div>
</div>
);
}
If this is a new component folder, add it to tabs via RadTools UI:
Or add directly to /devtools/tabs/ComponentsTab/tabConfig.ts:
export const COMPONENT_TABS: ComponentTabConfig[] = [
// ... existing tabs
{
id: 'folder-MyComponents', // Must be 'folder-{FolderName}'
label: 'My Components',
description: 'Description for this component group',
},
];
These attributes control what gets edited when using the visual editor:
| Attribute Combo | Effect |
|---|---|
data-edit-scope="component-definition" + data-component="Name" |
Edits base styles (affects all variants) |
Above + data-edit-variant="variantName" |
Edits only that variant's styles |
| No attributes | Preview only, no editing power |
Key points:
data-component value must match the component's file name (without extension)data-edit-variant) edits propagate to all variants unless overriddenexport default function/components/ or /components/{FolderName}/.test.tsx, .stories.tsx suffixes@/components/ alias consistentlydata-component must match file namedevelopmentAfter creating a component:
/components/ or /components/ui/ or /components/{Folder}/import { forwardRef } from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(({
label = '',
error = '',
className = '',
...props
}, ref) => {
return (
<div>
{label && <label className="block text-sm font-medium mb-1">{label}</label>}
<input
ref={ref}
className={`w-full px-3 py-2 border rounded-md bg-white ${error ? 'border-sun-red' : 'border-black'} ${className}`}
{...props}
/>
{error && <p className="text-sm text-sun-red mt-1">{error}</p>}
</div>
);
});
Input.displayName = 'Input';
export default Input;
import { useState, createContext, useContext } from 'react';
interface AccordionContextValue {
isOpen: boolean;
toggle: () => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
interface AccordionProps {
children: React.ReactNode;
defaultOpen?: boolean;
}
function Accordion({ children, defaultOpen = false }: AccordionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<AccordionContext.Provider value={{ isOpen, toggle: () => setIsOpen(!isOpen) }}>
<div className="border border-black rounded-md bg-cream">{children}</div>
</AccordionContext.Provider>
);
}
Accordion.Trigger = function AccordionTrigger({ children }: { children: React.ReactNode }) {
const ctx = useContext(AccordionContext);
return (
<button onClick={ctx?.toggle} className="w-full p-3 text-left font-medium text-black">
{children}
</button>
);
};
Accordion.Content = function AccordionContent({ children }: { children: React.ReactNode }) {
const ctx = useContext(AccordionContext);
if (!ctx?.isOpen) return null;
return <div className="p-3 border-t border-black">{children}</div>;
};
export default Accordion;
type PolymorphicProps<T extends React.ElementType> = {
as?: T;
variant?: 'filled' | 'outline';
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'variant' | 'children'>;
export default function Button<T extends React.ElementType = 'button'>({
as,
variant = 'filled',
children,
...props
}: PolymorphicProps<T>) {
const Component = as || 'button';
const variants = {
filled: 'bg-sun-yellow text-black border border-black',
outline: 'bg-transparent border-2 border-black text-black',
};
return (
<Component className={`px-4 py-2 rounded-md ${variants[variant]}`} {...props}>
{children}
</Component>
);
}