diff --git a/PRPs/ai_docs/UI_STANDARDS.md b/PRPs/ai_docs/UI_STANDARDS.md index 8baca1be..a8f67077 100644 --- a/PRPs/ai_docs/UI_STANDARDS.md +++ b/PRPs/ai_docs/UI_STANDARDS.md @@ -163,6 +163,14 @@ grep -rn "bg-.*-[0-9]" [path] --include="*.tsx" | grep -v "dark:" - **Compose with asChild** - Don't wrap, attach behavior to your components - **Style via data attributes** - `[data-state="open"]`, `[data-disabled]` - **Use Portal** for overlays with proper z-index +- **Support both controlled and uncontrolled modes** - All form primitives must work in both modes + +### Controlled vs Uncontrolled Form Components + +**CRITICAL RULE**: Form primitives (Switch, Checkbox, Select, etc.) MUST support both controlled and uncontrolled modes. + +**Controlled Mode**: Parent manages state via `value`/`checked` prop + `onChange`/`onCheckedChange` handler +**Uncontrolled Mode**: Component manages own state via `defaultValue`/`defaultChecked` ### Anti-Patterns ```tsx @@ -172,6 +180,12 @@ grep -rn "bg-.*-[0-9]" [path] --include="*.tsx" | grep -v "dark:" // ❌ Wrong composition + +// ❌ Only supports controlled mode (breaks uncontrolled usage) +const Switch = ({ checked, ...props }) => { + const displayIcon = checked ? iconOn : iconOff; // No internal state! + return +}; ``` ### Good Examples @@ -184,6 +198,20 @@ grep -rn "bg-.*-[0-9]" [path] --include="*.tsx" | grep -v "dark:" // ✅ Radix primitives + +// ✅ Supports both controlled and uncontrolled modes +const Switch = ({ checked, defaultChecked, onCheckedChange, ...props }) => { + const isControlled = checked !== undefined; + const [internalChecked, setInternalChecked] = useState(defaultChecked ?? false); + const actualChecked = isControlled ? checked : internalChecked; + + const handleChange = (newChecked: boolean) => { + if (!isControlled) setInternalChecked(newChecked); + onCheckedChange?.(newChecked); + }; + + return +}; ``` ### Automated Scans @@ -191,9 +219,18 @@ grep -rn "bg-.*-[0-9]" [path] --include="*.tsx" | grep -v "dark:" # Native HTML form elements grep -rn "