---
name: ui-components
description: |
  Generates standardized SmartStack UI components.
  Use this skill when:
  - Creating React page (.tsx) in src/pages/
  - Creating React component in src/components/
  - User asks to create cards, grids, tables, or Kanban
  - Claude detects creation of an entity list with display
  - User mentions "card", "grid", "table", "list", "kanban"
  - User asks for tooltips or infobubbles
  - Creating a page with entity display
  - Managing disabled states with explanatory messages
  Scope: Pages, Components, Cards, Tables, Grids, Kanban boards, Tooltips
---

# Skill UI Components SmartStack

**Reference:** [_shared.md](../_shared.md) for architecture, i18n

## WHEN THIS SKILL ACTIVATES

| Trigger | Example |
|---------|---------|
| React page/component creation | File in `src/pages/**/*.tsx` or `src/components/**/*.tsx` |
| List creation | "Display products in cards" |
| Keywords | "card", "grid", "table", "kanban", "tooltip" |
| Disabled states | "Disable button with explanatory message" |

## MANDATORY COMPONENT: EntityCard

**ALWAYS use `EntityCard` for entity cards. NEVER use custom cards with divs.**

```typescript
import { EntityCard, ProviderCard, TemplateCard } from '@/components/ui/EntityCard';
```

### EntityCard Usage
```tsx
<EntityCard
  avatar={{ letter: 'O', color: '#10a37f' }}
  title="OpenAI" subtitle="openai"
  description="OpenAI GPT models"
  stats="15 model(s)"
  badge={{ icon: Shield, tooltip: 'Admin API supported' }}
  links={[{ icon: ExternalLink, label: 'Website', href: 'https://...' }]}
  actions={[{ label: 'Config', onClick: () => {}, variant: 'primary' }]}
/>
```

### ProviderCard Usage (AI Providers)
```tsx
<ProviderCard name="OpenAI" code="openai" description="..." modelCount={15} color="#10a37f"
  websiteUrl="..." docsUrl="..." apiKeyUrl="..." hasAdminKey />
```

### TemplateCard Usage (Templates)
```tsx
<TemplateCard name="Welcome" code="welcome" category="Transactional" isActive isSystem
  icon={Mail} translationsCount={3} onClick={() => {}} onEdit={() => {}} onDelete={() => {}} />
```

### EntityCard Props
| Prop | Type |
|------|------|
| `avatar` | `{ letter, color, imageUrl? }` |
| `title`, `subtitle`, `description` | `string` |
| `stats` | `string` |
| `badge` | `{ icon?, tooltip?, color? }` |
| `links` | `Array<{ icon, label, href?, onClick? }>` |
| `actions` | `Array<{ label, href?, onClick?, variant, icon?, disabled? }>` |

### Action Variants (EntityCard)
| Variant | Style |
|---------|-------|
| `primary` | `bg-[var(--color-accent-600)] text-white` |
| `secondary` | `bg-[var(--bg-secondary)] text-[var(--text-secondary)]` |
| `ghost` | `text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]` (NO border!) |

## RESPONSIVE GRID

```tsx
// Standard
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
  {items.map(item => <EntityCard key={item.id} {...mapToCardProps(item)} />)}
</div>

// With empty state
{items.length === 0 ? (
  <div className="text-center py-12"><p>No items</p></div>
) : (
  <div className="grid ...">...</div>
)}
```

## CUSTOM CARDS (Status, Dashboard)

Button alignment pattern at bottom (mandatory for custom cards):

```tsx
// WARNING: h-full flex flex-col + flex-1 + mt-auto
<div className="h-full flex flex-col rounded-[var(--radius-card)] border ...">
  {/* Header */}
  <div className="px-4 py-3 bg-gradient-to-r from-[var(--color-accent-500)]/10 ...">
    {/* ... */}
  </div>
  {/* Content with flex-1 */}
  <div className="flex-1 flex flex-col p-4">
    {/* Variable content */}
    {/* WARNING: mt-auto for button at bottom */}
    <button className="mt-auto w-full ...">Action</button>
  </div>
</div>
```

## COMPONENT: DataTable

```tsx
import { DataTable } from '@/components/ui/DataTable';

<DataTable
  data={users}
  columns={[
    { key: 'name', label: 'Name', sortable: true },
    { key: 'role', label: 'Role', render: (user) => <Badge>{user.role}</Badge> }
  ]}
  pagination={{ pageSize: 10 }}
  searchable
  onRowClick={(user) => navigate(`/users/${user.id}`)}
/>
```

## COMPONENT: Tooltip

```tsx
import { Tooltip } from '@/components/ui/Tooltip';

// Variants: default, error, warning, success, info
// Positions: top, bottom, left, right

<Tooltip content="Permission required" variant="error">
  <button disabled>Protected action</button>
</Tooltip>
```

### Disabled Button with Explanation Pattern
```tsx
const canExecute = hasPermission('module.action.execute');
<Tooltip content={!canExecute ? t('errors.noPermission') : undefined} variant="error" disabled={canExecute}>
  <button disabled={!canExecute}>Action</button>
</Tooltip>
```

## WHEN TO USE EntityCard vs Custom

| EntityCard for | Custom for |
|----------------|------------|
| Homogeneous entity lists | Dashboard stats cards |
| Catalogs | Cards with complex interactive states |
| Clickable grids | Cards with integrated forms |

## CRITICAL: CSS VARIABLES (NEVER HARDCODE COLORS)

**NEVER use hardcoded Tailwind colors.** ALWAYS use CSS variables for theme compliance.

### Status Colors
| Status | Background | Text | Border | Dot (solid) |
|--------|------------|------|--------|-------------|
| **Info** (Pending, Business) | `--info-bg` | `--info-text` | `--info-border` | `--info-dot` |
| **Success** (Active) | `--success-bg` | `--success-text` | `--success-border` | `--success-dot` |
| **Warning** (Suspended, Owner) | `--warning-bg` | `--warning-text` | `--warning-border` | `--warning-dot` |
| **Error** (Danger, Archive) | `--error-bg` | `--error-text` | `--error-border` | `--error-dot` |
| **Accent** (Personal, Primary) | `--accent-bg` | `--accent-text` | `--accent-border` | `--color-accent-500` |

### Badge Pattern
```tsx
// ✅ CORRECT - Uses CSS variables
<span className="px-2 py-0.5 text-xs rounded-[var(--radius-badge)] bg-[var(--info-bg)] text-[var(--info-text)]">
  Info Badge
</span>

// ❌ WRONG - Hardcoded Tailwind colors
<span className="bg-blue-500/10 text-blue-500">Info Badge</span>
```

### Status Config Pattern
```tsx
// ✅ CORRECT
const STATUS_CONFIG = {
  Pending: { bgColor: 'bg-[var(--info-bg)]', color: 'text-[var(--info-text)]', borderColor: 'border-[var(--info-border)]' },
  Active: { bgColor: 'bg-[var(--success-bg)]', color: 'text-[var(--success-text)]', borderColor: 'border-[var(--success-border)]' },
  Suspended: { bgColor: 'bg-[var(--warning-bg)]', color: 'text-[var(--warning-text)]', borderColor: 'border-[var(--warning-border)]' },
  Archived: { bgColor: 'bg-[var(--bg-tertiary)]', color: 'text-[var(--text-tertiary)]', borderColor: 'border-[var(--border-color)]' }
};

// ❌ WRONG - dark: variants are obsolete
const STATUS_CONFIG = {
  Active: { bgColor: 'bg-emerald-50 dark:bg-emerald-900/20', ... }
};
```

## BUTTON VARIANTS

### Action Buttons (Status changes)
```tsx
// Success action (Activate)
<button className="px-4 py-2 rounded-[var(--radius-button)] bg-[var(--success-dot)] text-white hover:opacity-90">
  Activate
</button>

// Warning action (Suspend)
<button className="px-4 py-2 rounded-[var(--radius-button)] bg-[var(--warning-dot)] text-white hover:opacity-90">
  Suspend
</button>

// Danger action (Archive/Delete)
<button className="px-4 py-2 rounded-[var(--radius-button)] bg-[var(--error-dot)] text-white hover:opacity-90">
  Archive
</button>

// Primary action
<button className="px-4 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-600)] text-white hover:bg-[var(--color-accent-700)]">
  Save
</button>

// Ghost action (NO BORDER!)
<button className="px-4 py-2 rounded-[var(--radius-button)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]">
  Refresh
</button>

// Secondary action
<button className="px-4 py-2 rounded-[var(--radius-button)] bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]">
  Cancel
</button>
```

### Button Rules
| Variant | Background | Text | Border | Hover |
|---------|------------|------|--------|-------|
| **Primary** | `--color-accent-600` | `white` | none | `--color-accent-700` |
| **Success** | `--success-dot` | `white` | none | `opacity-90` |
| **Warning** | `--warning-dot` | `white` | none | `opacity-90` |
| **Danger** | `--error-dot` | `white` | none | `opacity-90` |
| **Secondary** | `--bg-secondary` | `--text-secondary` | none | `--bg-hover` |
| **Ghost** | transparent | `--text-secondary` | **NONE** | `--bg-hover` |

## DETAIL PAGE TEMPLATE

### Structure
```tsx
<div className="container mx-auto px-4 py-6 max-w-7xl space-y-6">
  {/* Header */}
  <div className="flex items-center gap-4">
    <button onClick={() => navigate(-1)} className="p-2 rounded-[var(--radius-button)] hover:bg-[var(--bg-hover)]">
      <ArrowLeft className="w-5 h-5 text-[var(--text-secondary)]" />
    </button>
    <div className="flex-1">
      <h1 className="text-xl sm:text-2xl font-bold text-[var(--text-primary)]">{title}</h1>
      <p className="text-sm text-[var(--text-secondary)]">{subtitle}</p>
    </div>
    <div className="flex items-center gap-2">
      {/* Status badges */}
    </div>
  </div>

  {/* Pill Tabs */}
  <div className="flex gap-1 p-1 bg-[var(--bg-secondary)] rounded-[var(--radius-card)] border border-[var(--border-color)]">
    {tabs.map(tab => (
      <button
        key={tab.id}
        className={`px-4 py-2 rounded-[var(--radius-button)] font-medium ${
          activeTab === tab.id
            ? 'bg-[var(--color-accent-600)] text-white'
            : 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'
        }`}
      >
        {tab.label}
      </button>
    ))}
  </div>

  {/* Tab Content */}
  <div className="min-h-[400px]">{/* ... */}</div>
</div>
```

### Info Card Pattern
```tsx
<div className="p-4 rounded-[var(--radius-card)] bg-[var(--bg-primary)] border border-[var(--border-color)]">
  <div className="flex items-center justify-between mb-4">
    <h3 className="text-lg font-semibold text-[var(--text-primary)] flex items-center gap-2">
      <Icon className="w-5 h-5" />
      {title}
    </h3>
    <button className="flex items-center gap-1 px-3 py-1.5 rounded-[var(--radius-button)] bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]">
      <Edit3 className="w-4 h-4" />
      {t('common:edit')}
    </button>
  </div>
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
    <div className="p-3 rounded-lg bg-[var(--bg-secondary)]">
      <p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider mb-1">{label}</p>
      <p className="text-sm text-[var(--text-primary)] font-medium">{value}</p>
    </div>
  </div>
</div>
```

### Error Display Pattern
```tsx
{error && (
  <div className="p-4 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)]">
    <div className="flex items-center gap-2 text-[var(--error-text)]">
      <AlertTriangle className="w-5 h-5" />
      <span>{error}</span>
    </div>
  </div>
)}
```

## TYPESCRIPT INTERFACES (MANDATORY)

**ALWAYS define explicit TypeScript interfaces for component props.**

```typescript
// ✅ CORRECT - Explicit interface with all props typed
interface UserCardProps {
  user: User;
  onSelect?: (user: User) => void;
  isSelected?: boolean;
  variant?: 'default' | 'compact';
}

export const UserCard: React.FC<UserCardProps> = ({
  user,
  onSelect,
  isSelected = false,
  variant = 'default'
}) => {
  // ...
};

// ❌ WRONG - Missing interface, inline types, or any
export const UserCard = ({ user, onSelect }: any) => { ... }
export const UserCard = (props: { user: any }) => { ... }
```

### State Typing
```typescript
// ✅ CORRECT - Explicit state types
const [users, setUsers] = useState<User[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');

// ❌ WRONG - Implicit or missing types
const [users, setUsers] = useState([]);
const [data, setData] = useState();
```

## SEMANTIC HTML (ACCESSIBILITY)

**ALWAYS use semantic HTML elements instead of divs with roles.**

```tsx
// ✅ CORRECT - Native button element
<button type="button" onClick={handleClick} disabled={isDisabled}>
  Click me
</button>

// ❌ WRONG - div with role (S6819)
<div role="button" onClick={handleClick} tabIndex={0}>
  Click me
</div>
```

### Interactive Elements Mapping

| Use Case | Correct Element | Wrong Pattern |
|----------|----------------|---------------|
| Clickable action | `<button type="button">` | `<div role="button">` |
| Navigation link | `<a href="...">` or `<Link>` | `<div onClick={navigate}>` |
| Form submission | `<button type="submit">` | `<div onClick={submit}>` |
| Expandable section | `<details><summary>` | `<div onClick={toggle}>` |
| List of items | `<ul><li>` with `<button>` inside | `<div role="listitem">` |

### Clickable Cards Pattern
```tsx
// ✅ CORRECT - Card with button inside
<div className="rounded-[var(--radius-card)] border ...">
  <div className="p-4">
    <h3>{title}</h3>
    <p>{description}</p>
  </div>
  <button
    type="button"
    onClick={() => onSelect(item)}
    className="w-full px-4 py-2 border-t ..."
  >
    Select
  </button>
</div>

// ✅ CORRECT - Entire card as link
<Link to={`/items/${item.id}`} className="block rounded-[var(--radius-card)] ...">
  <div className="p-4">...</div>
</Link>

// ❌ WRONG - div with click handler
<div onClick={() => onSelect(item)} className="cursor-pointer ...">
  ...
</div>
```

## PERFORMANCE OPTIMIZATION

### React.memo for Pure Components
```tsx
import { memo, useCallback, useMemo } from 'react';

// ✅ CORRECT - Memoized component for lists
export const UserCard = memo(({ user, onSelect }: UserCardProps) => {
  return (
    <div className="...">
      <h3>{user.name}</h3>
      <button onClick={() => onSelect(user)}>Select</button>
    </div>
  );
});
UserCard.displayName = 'UserCard';
```

### useCallback for Event Handlers
```tsx
// ✅ CORRECT - Stable callback reference
const handleSelect = useCallback((item: Item) => {
  onSelect?.(item);
  setSelectedId(item.id);
}, [onSelect]);

// ❌ WRONG - New function on every render
const handleSelect = (item: Item) => {
  onSelect?.(item);
};
```

### useMemo for Expensive Computations
```tsx
// ✅ CORRECT - Memoized filtered list
const filteredItems = useMemo(() =>
  items.filter(item => item.status === filter),
  [items, filter]
);

// ❌ WRONG - Recomputed on every render
const filteredItems = items.filter(item => item.status === filter);
```

## LIST RENDERING (UNIQUE KEYS)

**NEVER use array index as React key. ALWAYS use unique identifiers.**

```tsx
// ✅ CORRECT - Unique ID from data
{items.map(item => (
  <EntityCard key={item.id} {...mapToCardProps(item)} />
))}

// ✅ CORRECT - Composite key when needed
{items.map(item => (
  <Row key={`${item.tenantId}-${item.id}`} data={item} />
))}

// ❌ WRONG - Array index as key (S6479)
{items.map((item, index) => (
  <EntityCard key={index} {...mapToCardProps(item)} />
))}

// ❌ WRONG - Random key
{items.map(item => (
  <EntityCard key={Math.random()} {...mapToCardProps(item)} />
))}
```

### Why Index Keys Are Problematic
- Causes unnecessary re-renders when list order changes
- Breaks component state (inputs, animations)
- Causes visual glitches with drag-and-drop
- Makes reconciliation inefficient

## ABSOLUTE RULES

| DO | DON'T |
|----|-------|
| CSS variables for ALL colors | Hardcoded Tailwind colors (`bg-blue-500`) |
| `--success-dot`, `--warning-dot`, `--error-dot` for action buttons | `bg-emerald-500`, `bg-amber-500`, `bg-red-500` |
| `--info-bg`, `--success-bg`, etc. for badges | `bg-blue-500/10 dark:bg-blue-900/20` |
| Ghost buttons WITHOUT border | `border border-[var(--border-color)]` on ghost |
| EntityCard for entities | Custom cards with manual divs |
| Responsive grid 1→2→3→4 | Fixed non-responsive grid |
| h-full + flex-1 + mt-auto | Unaligned buttons in grid |
| Empty and loading states | Native HTML tooltip |
| Explicit TypeScript interfaces | `any` or implicit types |
| `<button>` for click actions | `<div role="button">` |
| `memo()` for list item components | Unmemoized components in loops |
| `useCallback` for event handlers | Inline arrow functions in JSX |
| Unique `id` for list keys | Array index as key |
