docs(37): add research, validation strategy, and UI design contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
314
.planning/phases/37-admin-global-item-management/37-UI-SPEC.md
Normal file
314
.planning/phases/37-admin-global-item-management/37-UI-SPEC.md
Normal file
@@ -0,0 +1,314 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user