|
| 1 | +--- |
| 2 | +name: emcn-design-review |
| 3 | +description: Review UI code for alignment with the emcn design system — components, tokens, patterns, and conventions |
| 4 | +--- |
| 5 | + |
| 6 | +# EMCN Design Review |
| 7 | + |
| 8 | +Arguments: |
| 9 | +- scope: what to review (default: your current changes). Examples: "diff to main", "PR #123", "src/components/", "whole codebase" |
| 10 | +- fix: whether to apply fixes (default: true). Set to false to only propose changes. |
| 11 | + |
| 12 | +User arguments: $ARGUMENTS |
| 13 | + |
| 14 | +## Context |
| 15 | + |
| 16 | +This codebase uses **emcn**, a custom component library built on Radix UI primitives with CVA (class-variance-authority) variants and CSS variable design tokens. All UI must use emcn components and tokens — never raw HTML elements or hardcoded colors. |
| 17 | + |
| 18 | +## Steps |
| 19 | + |
| 20 | +1. Read the emcn barrel export at `apps/sim/components/emcn/components/index.ts` to know what's available |
| 21 | +2. Read `apps/sim/app/_styles/globals.css` for the full set of CSS variable tokens |
| 22 | +3. Analyze the specified scope against every rule below |
| 23 | +4. If fix=true, apply the fixes. If fix=false, propose the fixes without applying. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Imports |
| 28 | + |
| 29 | +- Import components from `@/components/emcn`, never from subpaths |
| 30 | +- Import icons from `@/components/emcn/icons` or `lucide-react` |
| 31 | +- Import `cn` from `@/lib/core/utils/cn` for conditional class merging |
| 32 | +- Import app-specific wrappers (Select, VerifiedBadge) from `@/components/ui` |
| 33 | + |
| 34 | +```tsx |
| 35 | +// Good |
| 36 | +import { Button, Modal, Badge } from '@/components/emcn' |
| 37 | +// Bad |
| 38 | +import { Button } from '@/components/emcn/components/button/button' |
| 39 | +``` |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Design Tokens (CSS Variables) |
| 44 | + |
| 45 | +Never use raw color values. Always use CSS variable tokens via Tailwind arbitrary values: `text-[var(--text-primary)]`, not `text-gray-500` or `#333`. The CSS variable pattern is canonical (1,700+ uses) — do not use Tailwind semantic classes like `text-muted-foreground`. |
| 46 | + |
| 47 | +### Text hierarchy |
| 48 | +| Token | Use | |
| 49 | +|-------|-----| |
| 50 | +| `text-[var(--text-primary)]` | Main content text | |
| 51 | +| `text-[var(--text-secondary)]` | Secondary/supporting text | |
| 52 | +| `text-[var(--text-tertiary)]` | Tertiary text | |
| 53 | +| `text-[var(--text-muted)]` | Disabled, placeholder text | |
| 54 | +| `text-[var(--text-icon)]` | Icon tinting | |
| 55 | +| `text-[var(--text-inverse)]` | Text on dark backgrounds | |
| 56 | +| `text-[var(--text-error)]` | Error/warning messages | |
| 57 | + |
| 58 | +### Surfaces (elevation) |
| 59 | +| Token | Use | |
| 60 | +|-------|-----| |
| 61 | +| `bg-[var(--bg)]` | Page background | |
| 62 | +| `bg-[var(--surface-2)]` through `bg-[var(--surface-7)]` | Increasing elevation | |
| 63 | +| `bg-[var(--surface-hover)]` | Hover state backgrounds | |
| 64 | +| `bg-[var(--surface-active)]` | Active/selected backgrounds | |
| 65 | + |
| 66 | +### Borders |
| 67 | +| Token | Use | |
| 68 | +|-------|-----| |
| 69 | +| `border-[var(--border)]` | Default borders | |
| 70 | +| `border-[var(--border-1)]` | Stronger borders (inputs, cards) | |
| 71 | +| `border-[var(--border-muted)]` | Subtle dividers | |
| 72 | + |
| 73 | +### Status |
| 74 | +| Token | Use | |
| 75 | +|-------|-----| |
| 76 | +| `--success` | Success states | |
| 77 | +| `--error` | Error states | |
| 78 | +| `--caution` | Warning states | |
| 79 | + |
| 80 | +### Brand |
| 81 | +| Token | Use | |
| 82 | +|-------|-----| |
| 83 | +| `--brand-secondary` | Brand color | |
| 84 | +| `--brand-accent` | Accent/CTA color | |
| 85 | + |
| 86 | +### Shadows |
| 87 | +Use shadow tokens, never raw box-shadow values: |
| 88 | +- `shadow-subtle`, `shadow-medium`, `shadow-overlay` |
| 89 | +- `shadow-kbd`, `shadow-card` |
| 90 | + |
| 91 | +### Z-Index |
| 92 | +Use z-index tokens for layering: |
| 93 | +- `z-[var(--z-dropdown)]` (100), `z-[var(--z-modal)]` (200), `z-[var(--z-popover)]` (300), `z-[var(--z-tooltip)]` (400), `z-[var(--z-toast)]` (500) |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## Component Usage Rules |
| 98 | + |
| 99 | +### Buttons |
| 100 | +Available variants: `default`, `primary`, `destructive`, `ghost`, `outline`, `active`, `secondary`, `tertiary`, `subtle`, `ghost-secondary`, `3d` |
| 101 | + |
| 102 | +| Action type | Variant | Frequency | |
| 103 | +|-------------|---------|-----------| |
| 104 | +| Toolbar, icon-only, utility actions | `ghost` | Most common (28%) | |
| 105 | +| Primary action (create, save, submit) | `primary` | Very common (24%) | |
| 106 | +| Cancel, close, secondary action | `default` | Common | |
| 107 | +| Delete, remove, destructive action | `destructive` | Targeted use only | |
| 108 | +| Active/selected state | `active` | Targeted use only | |
| 109 | +| Toggle, mode switch | `outline` | Moderate | |
| 110 | + |
| 111 | +Sizes: `sm` (compact, 32% of buttons) or `md` (default, used when no size specified). Never create custom button styles — use an existing variant. |
| 112 | + |
| 113 | +Buttons without an explicit variant prop get `default` styling. This is acceptable for cancel/secondary actions. |
| 114 | + |
| 115 | +### Modals (Dialogs) |
| 116 | +Use `Modal` + subcomponents. Never build custom dialog overlays. |
| 117 | + |
| 118 | +```tsx |
| 119 | +<Modal open={open} onOpenChange={setOpen}> |
| 120 | + <ModalContent size="sm"> |
| 121 | + <ModalHeader>Title</ModalHeader> |
| 122 | + <ModalBody>Content</ModalBody> |
| 123 | + <ModalFooter> |
| 124 | + <Button variant="default" onClick={() => setOpen(false)}>Cancel</Button> |
| 125 | + <Button variant="primary" onClick={handleSubmit}>Save</Button> |
| 126 | + </ModalFooter> |
| 127 | + </ModalContent> |
| 128 | +</Modal> |
| 129 | +``` |
| 130 | + |
| 131 | +Modal sizes by frequency: `sm` (440px, most common — confirmations and simple dialogs), `md` (500px, forms), `lg` (600px, content-heavy), `xl` (800px, rare), `full` (1200px, rare). |
| 132 | + |
| 133 | +Footer buttons: Cancel on left (`variant="default"`), primary action on right. This pattern is followed 100% across the codebase. |
| 134 | + |
| 135 | +### Delete/Remove Confirmations |
| 136 | +Always use Modal with `size="sm"`. The established pattern: |
| 137 | + |
| 138 | +```tsx |
| 139 | +<Modal open={open} onOpenChange={setOpen}> |
| 140 | + <ModalContent size="sm"> |
| 141 | + <ModalHeader>Delete {itemType}</ModalHeader> |
| 142 | + <ModalBody> |
| 143 | + <p>Description of consequences</p> |
| 144 | + <p className="text-[var(--text-error)]">Warning about irreversibility</p> |
| 145 | + </ModalBody> |
| 146 | + <ModalFooter> |
| 147 | + <Button variant="default" onClick={() => setOpen(false)}>Cancel</Button> |
| 148 | + <Button variant="destructive" onClick={handleDelete} disabled={isDeleting}> |
| 149 | + Delete |
| 150 | + </Button> |
| 151 | + </ModalFooter> |
| 152 | + </ModalContent> |
| 153 | +</Modal> |
| 154 | +``` |
| 155 | + |
| 156 | +Rules: |
| 157 | +- Title: "Delete {ItemType}" or "Remove {ItemType}" (use "Remove" for membership/association changes) |
| 158 | +- Include consequence description |
| 159 | +- Use `text-[var(--text-error)]` for warning text when the action is irreversible |
| 160 | +- `variant="destructive"` for the action button (100% compliance) |
| 161 | +- `variant="default"` for cancel (100% compliance) |
| 162 | +- Cancel left, destructive right (100% compliance) |
| 163 | +- For high-risk deletes (workspaces), require typing the name to confirm |
| 164 | +- Include recovery info if soft-delete: "You can restore it from Recently Deleted in Settings" |
| 165 | + |
| 166 | +### Toast Notifications |
| 167 | +Use the imperative `toast` API from `@/components/emcn`. Never build custom notification UI. |
| 168 | + |
| 169 | +```tsx |
| 170 | +import { toast } from '@/components/emcn' |
| 171 | + |
| 172 | +toast.success('Item saved') |
| 173 | +toast.error('Something went wrong') |
| 174 | +toast.success('Deleted', { action: { label: 'Undo', onClick: handleUndo } }) |
| 175 | +``` |
| 176 | + |
| 177 | +Variants: `default`, `success`, `error`. Auto-dismiss after 5s. Supports optional action buttons with callbacks. |
| 178 | + |
| 179 | +### Badges |
| 180 | +Use semantic color variants for status: |
| 181 | + |
| 182 | +| Status | Variant | Usage | |
| 183 | +|--------|---------|-------| |
| 184 | +| Error, failed, disconnected | `red` | Most common (15 uses) | |
| 185 | +| Metadata, roles, auth types, scopes | `gray-secondary` | Very common (12 uses) | |
| 186 | +| Type annotations (TS types, field types) | `type` | Very common (12 uses) | |
| 187 | +| Success, active, enabled, running | `green` | Common (7 uses) | |
| 188 | +| Neutral, default, unknown | `gray` | Common (6 uses) | |
| 189 | +| Outline, parameters, public | `outline` | Moderate (6 uses) | |
| 190 | +| Warning, processing | `amber` | Moderate (5 uses) | |
| 191 | +| Paused, warning | `orange` | Occasional | |
| 192 | +| Info, queued | `blue` | Occasional | |
| 193 | +| Data types (arrays) | `purple` | Occasional | |
| 194 | +| Generic with border | `default` | Occasional | |
| 195 | + |
| 196 | +Use `dot` prop for status indicators (19 instances in codebase). `icon` prop is available but rarely used. |
| 197 | + |
| 198 | +### Tooltips |
| 199 | +Use `Tooltip` from emcn with namespace pattern: |
| 200 | + |
| 201 | +```tsx |
| 202 | +<Tooltip.Root> |
| 203 | + <Tooltip.Trigger asChild> |
| 204 | + <Button variant="ghost">{icon}</Button> |
| 205 | + </Tooltip.Trigger> |
| 206 | + <Tooltip.Content>Helpful text</Tooltip.Content> |
| 207 | +</Tooltip.Root> |
| 208 | +``` |
| 209 | + |
| 210 | +Use tooltips for icon-only buttons and truncated text. Don't tooltip self-explanatory elements. |
| 211 | + |
| 212 | +### Popovers |
| 213 | +Use for filters, option menus, and nested navigation: |
| 214 | + |
| 215 | +```tsx |
| 216 | +<Popover open={open} onOpenChange={setOpen} size="sm"> |
| 217 | + <PopoverTrigger asChild> |
| 218 | + <Button variant="ghost">Trigger</Button> |
| 219 | + </PopoverTrigger> |
| 220 | + <PopoverContent side="bottom" align="end" minWidth={160}> |
| 221 | + <PopoverSection>Section Title</PopoverSection> |
| 222 | + <PopoverItem active={isActive} onClick={handleClick}> |
| 223 | + Item Label |
| 224 | + </PopoverItem> |
| 225 | + <PopoverDivider /> |
| 226 | + </PopoverContent> |
| 227 | +</Popover> |
| 228 | +``` |
| 229 | + |
| 230 | +### Dropdown Menus |
| 231 | +Use for context menus and action menus: |
| 232 | + |
| 233 | +```tsx |
| 234 | +<DropdownMenu> |
| 235 | + <DropdownMenuTrigger asChild> |
| 236 | + <Button variant="ghost"> |
| 237 | + <MoreHorizontal className="h-[14px] w-[14px]" /> |
| 238 | + </Button> |
| 239 | + </DropdownMenuTrigger> |
| 240 | + <DropdownMenuContent align="end"> |
| 241 | + <DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem> |
| 242 | + <DropdownMenuSeparator /> |
| 243 | + <DropdownMenuItem onClick={handleDelete} className="text-[var(--text-error)]"> |
| 244 | + Delete |
| 245 | + </DropdownMenuItem> |
| 246 | + </DropdownMenuContent> |
| 247 | +</DropdownMenu> |
| 248 | +``` |
| 249 | + |
| 250 | +Destructive items go last, after a separator, in error color. |
| 251 | + |
| 252 | +### Forms |
| 253 | +Use `FormField` wrapper for labeled inputs: |
| 254 | + |
| 255 | +```tsx |
| 256 | +<FormField label="Name" htmlFor="name" error={errors.name} optional> |
| 257 | + <Input id="name" value={name} onChange={e => setName(e.target.value)} /> |
| 258 | +</FormField> |
| 259 | +``` |
| 260 | + |
| 261 | +Rules: |
| 262 | +- Use `Input` from emcn, never raw `<input>` (exception: hidden file inputs) |
| 263 | +- Use `Textarea` from emcn, never raw `<textarea>` |
| 264 | +- Use `FormField` for label + input + error layout |
| 265 | +- Mark optional fields with `optional` prop |
| 266 | +- Show errors inline below the input |
| 267 | +- Use `Combobox` for searchable selects |
| 268 | +- Use `TagInput` for multi-value inputs |
| 269 | + |
| 270 | +### Loading States |
| 271 | +Use `Skeleton` for content placeholders: |
| 272 | + |
| 273 | +```tsx |
| 274 | +<Skeleton className="h-5 w-[200px] rounded-md" /> |
| 275 | +``` |
| 276 | + |
| 277 | +Rules: |
| 278 | +- Mirror the actual UI structure with skeletons |
| 279 | +- Match exact dimensions of the final content |
| 280 | +- Use `rounded-md` to match component radius |
| 281 | +- Stack multiple skeletons for lists |
| 282 | + |
| 283 | +### Icons |
| 284 | +Standard sizing — `h-[14px] w-[14px]` is the dominant pattern (400+ uses): |
| 285 | + |
| 286 | +```tsx |
| 287 | +<Icon className="h-[14px] w-[14px] text-[var(--text-icon)]" /> |
| 288 | +``` |
| 289 | + |
| 290 | +Size scale by frequency: |
| 291 | +1. `h-[14px] w-[14px]` — default for inline icons (most common) |
| 292 | +2. `h-[16px] w-[16px]` — slightly larger inline icons |
| 293 | +3. `h-3 w-3` (12px) — compact/tight spaces |
| 294 | +4. `h-4 w-4` (16px) — Tailwind equivalent, also common |
| 295 | +5. `h-3.5 w-3.5` (14px) — Tailwind equivalent of 14px |
| 296 | +6. `h-5 w-5` (20px) — larger icons, section headers |
| 297 | + |
| 298 | +Use `text-[var(--text-icon)]` for icon color (113+ uses in codebase). |
| 299 | + |
| 300 | +--- |
| 301 | + |
| 302 | +## Styling Rules |
| 303 | + |
| 304 | +1. **Use `cn()` for conditional classes**: `cn('base', condition && 'conditional')` — never template literal concatenation like `` `base ${condition ? 'active' : ''}` `` |
| 305 | +2. **Inline styles**: Avoid. Exception: dynamic values that can't be expressed as Tailwind classes (e.g., `style={{ width: dynamicVar }}` or CSS variable references). Never use inline styles for colors or static values. |
| 306 | +3. **Never hardcode colors**: Use CSS variable tokens. Never `text-gray-500`, `bg-red-100`, `#fff`, or `rgb()`. Always `text-[var(--text-*)]`, `bg-[var(--surface-*)]`, etc. |
| 307 | +4. **Never use Tailwind semantic color classes**: Use `text-[var(--text-muted)]` not `text-muted-foreground`. The CSS variable pattern is canonical. |
| 308 | +5. **Never use global styles**: Keep all styling local to components |
| 309 | +6. **Hover states**: Use `hover-hover:` pseudo-class for hover-capable devices |
| 310 | +7. **Transitions**: Use `transition-colors` for color changes, `transition-colors duration-100` for fast hover |
| 311 | +8. **Border radius**: `rounded-lg` (large cards), `rounded-md` (medium), `rounded-sm` (small), `rounded-xs` (tiny) |
| 312 | +9. **Typography**: Use semantic sizes — `text-small` (13px), `text-caption` (12px), `text-xs` (11px), `text-micro` (10px) |
| 313 | +10. **Font weight**: Use `font-medium` for emphasis, avoid `font-bold` unless for headings |
| 314 | +11. **Spacing**: Use Tailwind gap/padding utilities. Common patterns: `gap-2`, `gap-3`, `px-4 py-2.5` |
| 315 | + |
| 316 | +--- |
| 317 | + |
| 318 | +## Anti-patterns to flag |
| 319 | + |
| 320 | +- Raw HTML `<button>` instead of Button component (exception: inside Radix primitives) |
| 321 | +- Raw HTML `<input>` instead of Input component (exception: hidden file inputs, read-only checkboxes in markdown) |
| 322 | +- Hardcoded Tailwind default colors (`text-gray-*`, `bg-red-*`, `text-blue-*`) |
| 323 | +- Hex values in className (`bg-[#fff]`, `text-[#333]`) |
| 324 | +- Tailwind semantic classes (`text-muted-foreground`) instead of CSS variables (`text-[var(--text-muted)]`) |
| 325 | +- Custom modal/dialog implementations instead of `Modal` |
| 326 | +- Custom toast/notification implementations instead of `toast` |
| 327 | +- Inline styles for colors or static values (dynamic values are acceptable) |
| 328 | +- Template literal className concatenation instead of `cn()` |
| 329 | +- Wrong button variant for the action type |
| 330 | +- Missing loading/skeleton states |
| 331 | +- Missing error states on forms |
| 332 | +- Importing from emcn subpaths instead of barrel export |
| 333 | +- Using arbitrary z-index (`z-50`, `z-[9999]`) instead of z-index tokens |
| 334 | +- Custom shadows instead of shadow tokens |
| 335 | +- Icon sizes that don't follow the established scale (default to `h-[14px] w-[14px]`) |
0 commit comments