Files
GearBox/.planning/phases/37-admin-global-item-management/37-UI-SPEC.md

315 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 37
slug: admin-global-item-management
status: approved
shadcn_initialized: false
preset: none
created: 2026-04-19
reviewed_at: 2026-04-19
---
# Phase 37 — UI Design Contract
> Visual and interaction contract for Admin — Global Item Management. Generated from CONTEXT.md decisions, RESEARCH.md findings, and codebase pattern analysis.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — Tailwind CSS v4 utility-first |
| Preset | not applicable |
| Component library | none (custom components) |
| Icon library | LucideIcon from `src/client/lib/iconData` |
| Font | System default (Inter-like sans-serif via browser) |
No `components.json` detected. No shadcn. All styling is plain Tailwind utility classes matching the app's existing light/airy aesthetic.
---
## Spacing Scale
Standard 8-point scale from existing app patterns:
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px (`gap-1`, `p-1`) | Icon gaps, inline chip gaps |
| sm | 8px (`gap-2`, `p-2`) | Compact table cell padding, tag chips |
| md | 16px (`gap-4`, `p-4`) | Default form field spacing, sidebar padding |
| lg | 24px (`gap-6`, `p-6`) | Section padding, edit page padding |
| xl | 32px (`gap-8`, `p-8`) | Page-level layout gaps |
| 2xl | 48px | Major section breaks (used sparingly) |
| 3xl | 64px | Not used in admin panel |
Exceptions:
- Table row height: `py-3` (12px vertical) + `px-4` (16px horizontal) for dense data rows
- Sidebar nav items: `px-3 py-2` (12px vertical) — matches Phase 36 admin shell
---
## Typography
Matches existing app type scale. No new sizes introduced.
| Role | Size | Weight | Line Height | Tailwind |
|------|------|--------|-------------|---------|
| Body / table cell | 14px | 400 | 1.5 | `text-sm` |
| Label / column header | 12px | 600 | 1.4 | `text-xs font-semibold` |
| Form field label | 14px | 600 | 1.4 | `text-sm font-semibold` |
| Page heading | 18px | 600 | 1.3 | `text-lg font-semibold` |
Weights used: 400 (regular) + 600 (semibold). No medium (500), no bold (700).
---
## Color
60/30/10 rule — matches Phase 36 admin shell exactly:
| Role | Value | Tailwind | Usage |
|------|-------|---------|-------|
| Dominant (60%) | #ffffff | `bg-white` | Page backgrounds, table rows, edit form |
| Secondary (30%) | #f9fafb | `bg-gray-50` | Main content area, input backgrounds |
| Border / divider | #f3f4f6 | `border-gray-100` | Table row dividers, sidebar border |
| Text primary | #111827 | `text-gray-900` | Item names, column data |
| Text secondary | #4b5563 | `text-gray-600` | Labels, form hints |
| Text muted | #9ca3af | `text-gray-400` | Column headers, disabled states |
| Accent (10%) | #2563eb | `text-blue-600`, `bg-blue-50` | Save button, active nav link indicator |
| Destructive | #dc2626 | `bg-red-600 hover:bg-red-700` | Delete button ONLY |
Accent reserved for:
- Save/Update primary button
- Active sidebar nav link highlight (left border or background)
Destructive reserved for:
- Delete button in edit page
- Confirm delete button inside the delete dialog
---
## Component Inventory
### Admin Items List (`/admin/items`)
**Layout:**
```
┌─────────────────────────────────────────────────┐
│ Heading: "Catalog Items" [search input] │
│ Tag filter chips │
│ "1,247 items" count label │
├──────┬────────────┬─────┬───────┬───────┬───────┤
│ Brand+Model │ Category │ Wt │ Price │ Tags │ Owners│
├──────┴────────────┴─────┴───────┴───────┴───────┤
│ row row row ... (50 per page) │
│ [sentinel div — triggers next page] │
└─────────────────────────────────────────────────┘
```
**Table element specs:**
- Wrapper: `w-full overflow-hidden rounded-xl border border-gray-100 bg-white`
- `<table>`: `w-full text-sm`
- `<thead>`: `bg-gray-50 border-b border-gray-100`
- Column header cells: `px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide`
- `<tbody>` rows: `border-b border-gray-50 hover:bg-gray-50 cursor-pointer transition-colors`
- Body cells: `px-4 py-3 text-sm text-gray-700`
- Brand+Model cell: bold brand (`font-medium text-gray-900`) + muted model (`text-gray-500`)
**Tags column:**
- ≤ 2 tags: render as inline chips — `text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full`
- > 2 tags: render count badge — `text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full` displaying "+N"
**Owner count column:**
- Numeric, right-aligned, `text-gray-500`
- 0 owners: `text-gray-300`
**Search input:**
- `rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-300`
- Placeholder: "Search catalog..."
- Width: `w-64`
**Tag filter:**
- Row of clickable chips below search bar
- Selected: `bg-blue-50 text-blue-600 border border-blue-200`
- Unselected: `bg-gray-100 text-gray-600`
- Multi-select allowed (matches existing CatalogSearchOverlay pattern)
**Infinite scroll sentinel:**
- `<div ref={sentinelRef} className="h-4" />` at bottom of table
- IntersectionObserver triggers `fetchNextPage()` when sentinel enters viewport
- Loading indicator: `<div className="py-4 text-center text-sm text-gray-400">Loading...</div>`
### Admin Items Edit Page (`/admin/items/$itemId`)
**Layout:**
```
← Items (back link)
Salsa Woodsmoke 700 (page heading: brand + model)
3 users in collection (owner count subtext)
┌──────────────────────────────────────────────────┐
│ [Image preview — GearImage] │
│ [Image URL input] │
├──────────────────────────────────────────────────┤
│ Brand (Manufacturer) [dropdown] │
│ Model [text input] │
│ Category [text input] │
├──────────────────────────────────────────────────┤
│ Weight (g) [number input] │
│ Price (€) [number input] │
├──────────────────────────────────────────────────┤
│ Tags [chip input] │
│ Description [textarea] │
│ Source URL [url input] │
│ Image Credit [text input] │
│ Image Source URL [url input] │
├──────────────────────────────────────────────────┤
│ [Save Changes] [Delete Item]│
└──────────────────────────────────────────────────┘
```
**Form wrapper:** `max-w-2xl mx-auto` (consistent with catalog detail page width)
**Field groups:** separated by `border-t border-gray-100 pt-6 mt-6`
**Input styles (all fields):**
- Text/number/url: `w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-300`
- Textarea: same + `min-h-[80px] resize-y`
- Manufacturer dropdown: `<select>` styled same as inputs, `appearance-none bg-white`
- Label: `block text-sm font-medium text-gray-700 mb-1`
**Tags chip input:**
- Active chips: `inline-flex items-center gap-1 bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded-full`
- Remove button per chip: `×` button, `text-gray-400 hover:text-gray-600`
- Input: inline text input at end of chip row, presses Enter or comma to add
- Wrapper: `flex flex-wrap gap-2 rounded-lg border border-gray-200 px-3 py-2 min-h-[40px]`
**Save button:**
- `px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium transition-colors disabled:opacity-50`
- Label: "Save Changes"
**Delete button:**
- `px-4 py-2 rounded-lg border border-red-200 text-red-600 hover:bg-red-50 text-sm font-medium transition-colors`
- Positioned at far right of action row (space-between layout)
- Label: "Delete Item"
**Back link:**
- `text-sm text-gray-400 hover:text-gray-600 transition-colors`
- Text: "← Items"
- Navigates to `/admin/items`
**Page heading:**
- `text-lg font-semibold text-gray-900` — "{Brand} {Model}"
- Subtext: `text-sm text-gray-400 mt-0.5` — "{N} users in collection" (uses ownerCount from API)
### Delete Confirmation Dialog
Inline state-driven modal (not UIStore — local `useState`):
```
┌──────────────────────────────────────────────┐
│ Delete {Brand} {Model}? │
│ │
│ {N} users have this item in their │
│ collection. This cannot be undone. │
│ │
│ [Cancel] [Delete] │
└──────────────────────────────────────────────┘
```
- Modal backdrop: `fixed inset-0 z-50 flex items-center justify-center` + `absolute inset-0 bg-black/30`
- Dialog box: `relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full` (matches ConfirmDialog)
- Heading: `text-lg font-semibold text-gray-900 mb-2`
- Body: `text-sm text-gray-600 mb-6`
- Cancel: `px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg`
- Confirm delete: `px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg`
### Sidebar Items Link (admin.tsx update)
Replace the disabled `<div>` with an active `<Link>`:
```tsx
<Link
to="/admin/items"
activeProps={{ className: "bg-gray-100 text-gray-900 font-medium" }}
inactiveProps={{ className: "text-gray-600 hover:bg-gray-50" }}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors"
>
<LucideIcon name="package" size={16} />
<span>Items</span>
</Link>
```
Remove the "Soon" badge and `cursor-not-allowed` class.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Page heading (list) | "Catalog Items" |
| Item count label | "{N} items" (e.g. "1,247 items") |
| Search placeholder | "Search catalog..." |
| Empty state heading | "No items found" |
| Empty state body | "Try a different search or clear your filters." |
| Edit page — 0 owners | "Not in any collection" |
| Edit page — 1 owner | "1 user in collection" |
| Edit page — N owners | "{N} users in collection" |
| Save CTA | "Save Changes" |
| Delete CTA (edit page) | "Delete Item" |
| Delete dialog heading | "Delete {Brand} {Model}?" |
| Delete dialog body (0 owners) | "This item is not in any collection. This cannot be undone." |
| Delete dialog body (N owners) | "{N} users have this item in their collection. This cannot be undone." |
| Delete dialog confirm | "Delete" |
| Delete dialog cancel | "Cancel" |
| Back link | "← Items" |
| Loading more (infinite scroll) | "Loading..." |
| All items loaded | "All {N} items loaded" |
| Error state | "Failed to load catalog items. Please try again." |
| Save success | (no toast — form stays; navigation optional) |
---
## States to Handle
| Component | States |
|-----------|--------|
| Items list | Loading (skeleton rows), Loaded, Empty, Error, Loading next page |
| Edit page | Loading (skeleton form), Loaded, Saving, Save success, Save error |
| Delete dialog | Closed, Open, Deleting, Delete error |
| Manufacturer dropdown | Loading manufacturers, Loaded |
**Loading skeleton for table rows:**
```
4-6 rows with `<div className="h-4 bg-gray-100 rounded animate-pulse" />` in each cell
```
**Save/delete pending state:**
- Button `disabled` + `opacity-50`
- No spinner needed — button text stays static
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| None — no shadcn | — | not applicable |
No third-party component registries used. All UI is plain Tailwind + custom components.
---
## Checker Sign-Off
- [x] Dimension 1 Copywriting: PASS
- [x] Dimension 2 Visuals: PASS
- [x] Dimension 3 Color: PASS
- [x] Dimension 4 Typography: PASS
- [x] Dimension 5 Spacing: PASS
- [x] Dimension 6 Registry Safety: PASS
**Approval:** approved 2026-04-19