# Admin Dashboard UI — Implementation Rules

Applies to all UI work inside `apps/admin/`. These rules take precedence over
general project conventions for anything under this directory.

---

## 1. Component Library — ShadCN UI Only

All UI must be composed strictly from ShadCN UI components. No other component
libraries, no raw HTML element styling where a ShadCN component exists.

**Before building any UI, check if ShadCN has a component for it:**

| Category     | Components |
|--------------|-----------|
| Layout       | `Sheet`, `Separator`, `ScrollArea`, `ResizablePanel` |
| Navigation   | `NavigationMenu`, `Breadcrumb`, `Tabs` |
| Data display | `Table`, `Card`, `Badge`, `Avatar` |
| Forms        | `Form`, `Input`, `Select`, `Checkbox`, `Switch`, `Textarea`, `RadioGroup`, `DatePicker` |
| Feedback     | `Toast` (via Sonner), `Alert`, `Dialog`, `AlertDialog` |
| Loading      | `Skeleton` |
| Actions      | `Button`, `DropdownMenu`, `ContextMenu`, `Command` |

**Install components via the ShadCN CLI — never copy-paste or hand-write ShadCN component source:**

```bash
npx shadcn@latest add button
npx shadcn@latest add table
```

**ShadCN best practices:**

- Use `cn()` from `lib/utils.ts` for all conditional className merging — never string concatenation
- Extend ShadCN components via `className` props, never modify files in `components/ui/` directly
- Use `variant` and `size` props before reaching for custom styles
- Compose complex components by combining primitives — a stat card is `Card` + `CardHeader` + `CardContent`, not a custom div
- Use `asChild` when you need to change the rendered element (e.g. wrapping a Next.js `Link` in a `Button`)

---

## 2. Skills & MCP Usage

**Always invoke the `shadcn-ui` skill** before starting any new page or significant component:

```
/shadcn-ui
```

The skill guides intentional layout, spacing, and visual hierarchy decisions within the ShadCN constraint.

**If the ShadCN MCP server is available**, use it to look up component APIs before implementing. Do not guess prop names or variant values from memory.

**When MCP is unavailable**, refer to https://ui.shadcn.com/docs/components before writing component usage.

---

## 3. No SEO

- No `<Head>` metadata beyond the bare minimum `layout.tsx` title
- No `generateMetadata` functions on admin pages
- No Open Graph, Twitter card, or structured data tags
- No sitemap or robots.txt entries for admin routes

---

## 4. Accessibility — Required Minimums

ShadCN handles most accessibility via Radix UI primitives. Additionally ensure:

- All interactive elements are keyboard navigable (use ShadCN correctly and this is automatic)
- Form fields always have an associated `<label>` — use ShadCN `Label`; use `sr-only` if the design hides it visually
- Data tables include `scope` on `<th>` elements
- Icon-only buttons always have `aria-label` — e.g. `<Button aria-label="Delete product" size="icon"><Trash2 /></Button>`
- Modal dialogs use ShadCN `Dialog` or `AlertDialog` — never custom divs with `display:none` toggling
- Color is never the only indicator of state — always pair color with text or icon
- Focus rings must remain visible — never add `outline-none` without a replacement focus style

---

## 5. Code Quality

- Admin page components are **Server Components by default**. Add `"use client"` only when the component uses hooks, event handlers, or browser APIs
- Data fetching happens in Server Components via Convex server-side queries — not in `useEffect`
- Forms use `react-hook-form` + `zod` validation wired through ShadCN `Form` components
- Loading states use ShadCN `Skeleton` — never spinners on full page loads
- Form submit buttons **must** show a spinner while submitting. Use an inline SVG with `data-icon="inline-start"` and `animate-spin` placed before the label text, and set `disabled={isSubmitting}`. The button label should also change to reflect the in-progress state (e.g. "Creating…", "Saving…"):
  ```tsx
  <Button type="submit" disabled={isSubmitting}>
    {isSubmitting && (
      <svg data-icon="inline-start" className="animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
        <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
      </svg>
    )}
    {isSubmitting ? "Saving…" : "Save changes"}
  </Button>
  ```
- Destructive actions (delete, archive) always use `AlertDialog` for confirmation — never `window.confirm`
- Empty states are always handled explicitly — never render an empty table or blank page silently

---

## 6. Imports

The admin app tsconfig has **no path alias**. Use relative imports only:

```typescript
// Correct
import { cn } from "../../lib/utils";

// Wrong — no @/ alias in this app
import { cn } from "@/lib/utils";
```
