# UI/UX Patterns

> Visual and interaction patterns for Dynamic widgets.

---

## CRITICAL: Use Component Props Over Utility Classes

**GOLDEN RULE:** When a Dynamic UI component provides props for styling/behavior, **ALWAYS use props instead of utility classes.**

```tsx
// ❌ WRONG → ✅ CORRECT
<div className="p-3 rounded-circle bg-primary bg-opacity-10"><DIcon icon="Wallet" /></div>
→ <DIcon icon="Wallet" size="24px" color="primary" hasCircle />

<button className="btn btn-primary">Click</button>
→ <DButton text="Click" color="primary" />

<span className="badge bg-primary bg-opacity-10 text-primary">Active</span>
→ <DBadge text="Active" color="primary" soft />

// DCard: use iconStart prop instead of manual icon in header
<DCard iconStart={<DIcon icon="Settings" />}>...</DCard>
```

**Utility classes ARE appropriate for:** layout (`d-flex`, `gap-3`), spacing (`mb-4`, `p-3`), positioning (`text-end`), sizing (`w-100`), visibility (`d-none`, `d-md-block`).

**Rule:** Utility classes for **layout/spacing**, component props for **appearance/behavior**.

### Common Mistakes

```tsx
// DIcon: use color prop, not className="text-primary". Use hasCircle, not wrapper div.
// DButton: use color="primary", not theme="primary" (deprecated).
// DBadge: use color="success" + soft, not className="bg-success".
```

---

## 1. Form Button Pattern (MANDATORY)

**Rule:** Every form MUST have exactly 2 buttons: primary (submit) + secondary (cancel). Right-aligned, primary on the right.

```tsx
{/* Always at end of form, right-aligned */}
<div className="d-flex gap-2 justify-content-end mt-4">
  <DButton text="Cancel" variant="outline" color="secondary" onClick={handleCancel} />
  <DButton text="Submit" color="primary" type="submit" />
</div>
```

- Text variations: `"Back"/"Save"/"Next"`. Destructive: `color="danger"` for primary.
- Mobile stacking: `flex-column-reverse flex-md-row justify-content-md-end`
- Cancel MUST use `variant="outline"`. Never `justify-content-start`.

---

## 2. Form Validation Pattern

Pattern: validation function per field returning error string, validate on blur + all on submit.

```tsx
const [errors, setErrors] = useState<Record<string, string>>({});
const validateEmail = (v: string): string => {
  if (!v) return 'Email is required';
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return 'Invalid email';
  return '';
};
// Blur handler: setErrors(prev => ({ ...prev, field: validateField(value) }));
// Submit: validate all fields, return early if any errors

// In JSX:
<DInput label="Email *" value={email} onChange={setEmail}
  onBlur={() => setErrors(p => ({ ...p, email: validateEmail(email) }))}
  invalid={!!errors.email} />
{errors.email && <small className="text-danger d-block mt-1">{errors.email}</small>}
```

- Validate on **blur** (best UX), validate **all on submit** (always required), real-time only for complex rules (password strength)
- Submit button always enabled. Required fields marked with `*`. Be specific in error messages.

---

## 3. Modal Patterns

See `modyo://docs/widgets/components-modals` for full DModal API. Key pattern variants:

**Confirmation:** `<DModal name="confirm-delete" centered>` with Cancel (outline) + Delete (`color="danger"`)
**Information:** `<DModal name="details" size="lg">` with single "Close" button
**Form:** Place `<form id="editForm">` in Body, connect with `<DButton type="submit" form="editForm" />` in Footer

### Modal Rules

- Register every modal in `DContextProvider.availablePortals` — `name` must match key
- Confirmation: `color="danger"` for destructive action
- Form modals: `form="formId"` connects submit button outside `<form>`
- Use `centered` for small modals, `size="lg"` for detail modals

---

## 4. List Patterns

### 4.1 SNIPPET: Card Grid Pattern

```tsx
<div className="row g-3">
  {items.map((item) => (
    <div key={item.id} className="col-12 col-md-6 col-lg-4">
      <DCard>
        <DCard.Body>
          <div className="d-flex justify-content-between align-items-start mb-2">
            <h6 className="mb-0">{item.title}</h6>
            <DBadge text={item.status} color="success" />
          </div>
          <p className="text-muted mb-2">{item.subtitle}</p>
          <h4 className="mb-3">{formatCurrency(item.amount)}</h4>
          <DButton
            text="View Details"
            variant="outline"
            size="sm"
            onClick={() => handleView(item.id)}
          />
        </DCard.Body>
      </DCard>
    </div>
  ))}
</div>
```

Grid: `col-12` (mobile 1-col) → `col-md-6` (tablet 2-col) → `col-lg-4` (desktop 3-col)

---

### 4.2 SNIPPET: List Item Pattern

```tsx
<div className="list-group">
  {items.map((item) => (
    <div
      key={item.id}
      className="list-group-item list-group-item-action"
      onClick={() => handleClick(item.id)}
    >
      <div className="d-flex justify-content-between align-items-center">
        {/* Left side: Icon + Info */}
        <div className="d-flex align-items-center gap-3">
          <DIcon icon={item.icon} size="24px" />
          <div>
            <h6 className="mb-0">{item.title}</h6>
            <small className="text-muted">{item.subtitle}</small>
          </div>
        </div>
        {/* Right side: Amount + Status */}
        <div className="text-end">
          <div className="text-dark fw-semibold">
            {item.amount > 0 ? '+' : ''}{formatCurrency(item.amount)}
          </div>
          <small className="text-muted">{item.status}</small>
        </div>
      </div>
    </div>
  ))}
</div>
```

---

### 4.3 CollapsibleWithList Pattern

DCollapse + DListGroup for dashboard sections with actions.

```tsx
<DCollapse className="mb-4" defaultCollapsed={false}
  Component={<div><h5 className="mb-1">Cuentas</h5><small className="text-muted">Total: $15,248.32</small></div>}>
  <DListGroup>
    {items.map((item) => (
      <DListGroup.Item key={item.id}>
        <div className="grid">
          <div className="g-col-3 d-flex gap-2 align-items-center">
            <DIcon icon={item.icon} size="1rem" hasCircle color={item.iconColor} />
            <div><h6 className="mb-0">{item.name}</h6><small className="text-muted">{item.subtitle}</small></div>
          </div>
          <div className="g-col-2"><small className="text-muted">Available</small><div className="fw-bold">{formatCurrency(item.balance)}</div></div>
          <div className="g-col-7 d-flex gap-2 align-items-center justify-content-end">
            <DButton variant="outline" text="Transfer" color="secondary" />
            <DDropdown actions={[{ label: 'Pay Bills', icon: 'Receipt' }, { label: 'Share', icon: 'Share2' }]} />
          </div>
        </div>
      </DListGroup.Item>
    ))}
  </DListGroup>
</DCollapse>
```

**Rules:** Use DCollapse directly (no DCard wrapper). Use `Component` prop for custom header. CSS Grid `.grid` + `.g-col-X` (total=12). Actions always visible. DDropdown for overflow menu.

---

## 5. Empty State Pattern

### SNIPPET: Empty State

```tsx
function EmptyState() {
  const { t } = useTranslation();
  const { openPortal } = useDPortalContext();

  return (
    <div className="text-center py-5">
      <DIcon icon="inbox" size="64px" color="secondary" />
      <h5 className="mt-3 mb-2">
        {t('empty.title', { defaultValue: 'No Items Yet' })}
      </h5>
      <p className="text-muted mb-4">
        {t('empty.message', { defaultValue: 'Your items will appear here' })}
      </p>
      <DButton
        text={t('empty.action', { defaultValue: 'Add Item' })}
        color="primary"
        onClick={() => openPortal('add-item-modal', {})}
      />
    </div>
  );
}
```

---

## 6. Loading State Patterns

### 6.1 SNIPPET: Skeleton Loader

```tsx
import { DCard } from '@dynamic-framework/ui-react';

export default function CardLoader() {
  return (
    <DCard>
      <DCard.Body>
        <div className="placeholder-glow">
          <span className="placeholder col-7 mb-2"></span>
          <span className="placeholder col-4 mb-3"></span>
          <span className="placeholder col-8 mb-2"></span>
          <span className="placeholder col-6"></span>
        </div>
      </DCard.Body>
    </DCard>
  );
}
```

### 6.2 Button Loading State

```tsx
<DButton
  text="Saving..."
  loading={isSaving}
  color="primary"
  disabled={isSaving}
/>
```

---

## 7. Responsive Design

### SNIPPET: Responsive Layout

```tsx
<div className="container">
  <div className="row">
    {/* Main content: Full width mobile, 2/3 desktop */}
    <div className="col-12 col-lg-8">{/* Main content */}</div>
    {/* Sidebar: Full width mobile, 1/3 desktop */}
    <div className="col-12 col-lg-4">{/* Sidebar */}</div>
  </div>
</div>
```

### Breakpoints

| Class | Breakpoint | Width |
|-------|-----------|-------|
| `col-*` | All | < 576px |
| `col-sm-*` | Small | ≥ 576px |
| `col-md-*` | Medium | ≥ 768px |
| `col-lg-*` | Large | ≥ 992px |
| `col-xl-*` | XLarge | ≥ 1200px |

See `modyo://docs/widgets/reference-accessibility` for accessibility patterns.

---

## Financial/Transactional UI Patterns

### CRITICAL: Currency Amount Display Rules

**RULE:** Currency amounts MUST use `text-dark`. NEVER `text-success`/`text-danger`/`text-primary` on amounts. Use +/- sign for direction, not color.

**Colors ARE allowed for:** percentages, icons, badges, decorative borders.

```tsx
// ❌ WRONG
<div className="display-5 text-primary fw-bold">US$5,247.85</div>
<td className="text-danger fw-semibold">US$-127.43</td>

// ✅ CORRECT - neutral amount, colored percentage
<div className="display-6 fw-bold text-dark">US$5,247.85</div>
<td className="text-dark fw-semibold">-US$127.43</td>
<span className="text-success small"><DIcon icon="TrendingUp" size="12px" /> +5.5%</span>
```

Income/expense cards: `border-start border-{color} border-3` with colored icon, amounts always `text-dark`.

### Display Class Hierarchy

**RULE:** In transactional widgets, only `.display-6` (never `.display-1` through `.display-5`).

| Use Case | Class |
|----------|-------|
| Hero metric (1-2 per widget) | `.display-6` |
| Primary metrics | `.fs-1` / `.fs-2` |
| Secondary/Tertiary | `.fs-3` / `.fs-4` |
| Labels / Small text | `.fs-5` / `.fs-6` / `.small` |

---

## Dropdown Menus and Popovers

- **DDropdown**: Action lists/menus with built-in three-dot trigger.
- **DPopover**: Custom JSX content. See `modyo://docs/widgets/components-overlay-widgets`.

```tsx
<DDropdown actions={[
  { label: 'Edit', icon: 'Pencil', onClick: handleEdit },
  { label: 'Share', icon: 'Share2', onClick: handleShare },
  { isDivider: true },
  { label: 'Delete', icon: 'Trash', color: 'danger', onClick: handleDelete },
]} />
```

**DropdownAction:** `{ label, icon?, onClick?, href?, color?, disabled?, isDivider? }`

### List Item Actions

Always-visible actions (recommended). Avoid hover-only — breaks on mobile.

```tsx
<DListGroup.Item>
  <div className="grid">
    <div className="g-col-6"><h6>Account</h6><small>****1234</small></div>
    <div className="g-col-6 d-flex gap-2 justify-content-end">
      <DButton variant="outline" text="Transfer" color="secondary" />
      <DDropdown actions={[{ label: 'Pay Bills', icon: 'Receipt' }, { isDivider: true }, { label: 'Lock', icon: 'Lock', color: 'danger' }]} />
    </div>
  </div>
</DListGroup.Item>
```

---

## Text Capitalization

**Rule:** Sentence case everywhere (buttons, labels, headings, badges). NEVER ALL CAPS except acronyms (PIN, ATM, USD) and brand names (iOS, GitHub).
