13 KiB
phase, slug, status, shadcn_initialized, preset, created, reviewed_at
| phase | slug | status | shadcn_initialized | preset | created | reviewed_at |
|---|---|---|---|---|---|---|
| 37 | admin-global-item-management | approved | false | none | 2026-04-19 | 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-fulldisplaying "+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>:
<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
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: approved 2026-04-19