docs(38): add UI design contract for admin tag management

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:57:19 +02:00
parent 11ff1eb1dd
commit 096cb5a1dd

View File

@@ -0,0 +1,258 @@
---
phase: 38
slug: admin-tag-management
status: draft
shadcn_initialized: false
preset: none
created: 2026-04-19
---
# Phase 38 — UI Design Contract
> Visual and interaction contract for Phase 38: Admin Tag Management.
> Generated by gsd-ui-researcher. All values extracted from existing codebase — no user questions required.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — hand-crafted Tailwind v4 |
| Preset | not applicable |
| Component library | none (custom components only) |
| Icon library | Lucide via `LucideIcon` from `src/client/lib/iconData` |
| Font | system default (no custom font declared in app.css) |
Source: `src/client/app.css` (single `@import "tailwindcss"` — no preset), existing admin routes.
---
## Spacing Scale
Declared values (multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps (`gap-1`), inline badge padding (`px-1.5 py-0.5`) |
| sm | 8px | Tag chip gaps (`gap-2`), compact row padding (`py-2`) |
| md | 16px | Default field spacing (`mt-4`), grid gaps (`gap-4`) |
| lg | 24px | Section padding (`p-6`), page section breaks (`pt-6`) |
| xl | 32px | Form bottom margin (`mt-8`) |
| 2xl | 48px | Not used in admin shell |
| 3xl | 64px | Not used in admin shell |
Exceptions:
- Tree row indent: 20px per depth level (value between `sm` and `md` — closest Tailwind class: `pl-5` per level). Source: CONTEXT.md specifics, "16-24px per level".
- Chevron toggle button: 28px click target minimum (`p-1` around 16px icon = 24px; acceptable for desktop-only admin tool).
---
## Typography
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px (text-sm) | 400 (regular) | 1.5 |
| Label | 12px (text-xs) | 600 (semibold) | 1.4 |
| Heading | 18px (text-lg) | 600 (semibold) | 1.2 |
| Display | 12px (text-xs) | 600 (semibold, uppercase, tracking-wide) | 1.4 |
Source: `src/client/routes/admin/items.tsx``text-lg font-semibold text-gray-900`, `text-xs font-semibold text-gray-400 uppercase tracking-wide`, `text-sm`.
Display role = table column headers (uppercase, tracked, muted gray-400). Not a separate heading level.
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `#f9fafb` (gray-50) | Main content area background (`bg-gray-50`) |
| Secondary (30%) | `#ffffff` (white) | Cards, sidebar, modal dialogs, form containers (`bg-white`) |
| Accent (10%) | `#2563eb` (blue-600) | Primary save/submit buttons, active input ring |
| Destructive | `#dc2626` (red-600) | Delete button background, delete confirmation button only |
Accent reserved for:
- "Save Changes" / "Add Tag" primary submit button (`bg-blue-600 hover:bg-blue-700 text-white`)
- Input focus ring (`focus:ring-2 focus:ring-blue-500/20 focus:border-blue-300`)
- Active tag filter chip in existing items list (`bg-blue-50 text-blue-600 border border-blue-200`)
Destructive reserved for:
- Delete button on edit page (`border-red-200 text-red-600 hover:bg-red-50`)
- Confirm delete button inside modal (`bg-red-600 hover:bg-red-700 text-white`)
All other interactive elements (back links, chevron toggles, row hovers) use gray-scale only.
Source: `src/client/routes/admin/items.$itemId.tsx` — extracted exact Tailwind classes.
---
## Component Inventory
### Existing (reuse without modification)
| Component | File | Usage in Phase 38 |
|-----------|------|-------------------|
| Admin shell layout | `src/client/routes/admin.tsx` | Unchanged — enable Tags sidebar Link |
| Tags sidebar Link | `src/client/routes/admin.tsx` L43-52 | Change disabled `<div>``<Link to="/admin/tags">` |
| Input class pattern | `items.$itemId.tsx` L177 | Reuse `inputClass` constant verbatim |
| Label class pattern | `items.$itemId.tsx` L178 | Reuse `labelClass` constant verbatim |
| Section divider class | `items.$itemId.tsx` L179 | Reuse `sectionClass` constant verbatim |
| Delete confirmation modal | `items.$itemId.tsx` L397-432 | Extend with child count copy; same structural pattern |
| Back navigation button | `items.$itemId.tsx` L212-218 | Replicate with "← Tags" label |
| Skeleton loader rows | `items.tsx` L139-148 | Reuse `animate-pulse` skeleton pattern |
| Error state | `items.tsx` L105-109 | Replicate for tag list error state |
### New (build for Phase 38)
| Component | Description | Visual Spec |
|-----------|-------------|-------------|
| `TagTreeRow` | Single row in the collapsible tree | See Tree Row spec below |
| `TagTreeView` | Container rendering all tree rows from flat list | Renders inside `rounded-xl border border-gray-100 bg-white` card |
| `TagQuickAddForm` | Inline form above the tree | Name input + parent picker + "Add Tag" button |
| `TagParentPicker` | Searchable dropdown for parent selection | Reuse `inputClass` for trigger; popover with filtered options |
---
## Tree Row Visual Spec
Each row in the collapsible tag tree:
```
[ chevron ] [ tag name ] [ item count ] [ Edit ]
indent per level
```
### Structure
- Indent: `pl-5` (20px) per depth level starting at depth 1. Root tags (depth 0) have no indent.
- Chevron: `LucideIcon name="chevron-right"` (collapsed) / `LucideIcon name="chevron-down"` (expanded), `size={14}`, color `text-gray-400`. Click area: `p-1 rounded hover:bg-gray-100`.
- Leaf nodes (no children): no chevron rendered; indent pad replaced with `w-6` spacer to align name column.
- Row hover: `hover:bg-gray-50 transition-colors` on the full row `<div>`.
- Tag name: `text-sm font-medium text-gray-900`.
- Item count: `text-sm text-gray-400` — e.g. "12 items" or "0 items".
- Edit action: `text-xs text-gray-400 hover:text-gray-600` link-style button → navigates to `/admin/tags/$tagId`.
- Row height: `py-2.5` (10px top/bottom) giving ~36px per row.
### Tree Expand/Collapse
- State: local `useState<Set<number>>` of expanded tag IDs. All parent IDs populated on mount (start expanded per D-03).
- Toggle: clicking chevron button calls `toggle(tagId)` — no row-click collapse (chevron-only per Claude's discretion).
- Hidden children: `display: none` equivalent — conditionally render child rows when parent ID is in expanded set.
### Search / Filter Behavior (D-05)
- Non-matching leaf rows: not rendered.
- Parent rows with matching children: rendered even if parent name does not match.
- Parent rows whose name matches: rendered with all their children visible (show context).
- Search input: `w-64 rounded-lg border border-gray-200 px-3 py-2 text-sm` — identical to items list.
---
## Quick-Add Form Spec (D-07)
Position: above the tree view card, below the page header.
```
[ Name input (flex-1) ] [ Parent picker (w-48) ] [ Add Tag button ]
```
- Name input: `inputClass` styling, placeholder "Tag name...".
- Parent picker: native `<select>` with `inputClass` styling, `appearance-none bg-white`, options = flat list of all tags (no descendants filter needed at create time since new tag has no children yet). First option: `<option value="">No parent (top-level)</option>`.
- "Add Tag" button: `px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium` — same as "Save Changes" pattern.
- Form layout: `flex items-center gap-3 mb-4`.
- On submit success: clear name input, reset parent to empty, show no toast (tree updates via React Query invalidation).
- On submit error: inline error text below form, `text-sm text-red-500`.
---
## Edit Page Spec (D-08, D-09)
Route: `/admin/tags/$tagId`
Layout: `max-w-2xl mx-auto` — matches items edit page.
Sections:
1. Back link: `← Tags` (same style as `← Items` in phase 37)
2. Page heading: tag name as `text-lg font-semibold text-gray-900` + item count as `text-sm text-gray-400`
3. Form fields:
- Name: text input with `labelClass` + `inputClass`
- Parent: searchable `<select>` with `labelClass` + `inputClass`; options exclude the tag itself and all its descendants (cycle prevention per D-11)
4. Actions row: `flex items-center justify-between mt-8 pt-6 border-t border-gray-100`
- Left: "Delete Tag" destructive button
- Right: "Save Changes" primary button
Parent picker "no parent" option label: "No parent (top-level)" as first `<option value="">`.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA (list page) | "Add Tag" |
| Primary CTA (edit page) | "Save Changes" |
| Back navigation | "← Tags" |
| Empty state heading | "No tags yet" |
| Empty state body | "Add your first tag using the form above." |
| Search empty state | "No tags match your search." |
| Error state (list) | "Failed to load tags. Please try again." |
| Error state (edit load) | "Failed to load tag. Please try again." |
| Error state (save) | "Failed to save. Please try again." |
| Error state (cycle detection) | "Cannot set this tag as its own descendant's parent." |
| Loading more indicator | "Loading..." |
| Page subtitle — item count | "{N} tags" |
| Edit page subtitle | "{N} items use this tag" |
| Edit page subtitle (0 items) | "Not used by any items" |
| Destructive action: delete (with items + children) | "Delete '{name}'? {N} items use this tag. Its {C} child tags will become top-level. This cannot be undone." |
| Destructive action: delete (with items, no children) | "Delete '{name}'? {N} items use this tag. This cannot be undone." |
| Destructive action: delete (no items, with children) | "Delete '{name}'? Its {C} child tags will become top-level. This cannot be undone." |
| Destructive action: delete (no items, no children) | "Delete '{name}'? This cannot be undone." |
| Delete confirmation button | "Delete" |
| Delete pending button | "Deleting..." |
| Save pending button | "Saving..." |
| Add pending button | "Adding..." |
| Column header: tag name | "Tag" (uppercase, tracked, gray-400) |
| Column header: item count | "Items" (uppercase, tracked, gray-400) |
| Column header: actions | "" (empty — right-aligned) |
Source: D-13, D-14 from CONTEXT.md. All other copy follows Phase 37 patterns from `items.$itemId.tsx`.
---
## Interaction States
| Element | States |
|---------|--------|
| Tree row | default, hover (gray-50 bg), loading skeleton |
| Chevron button | default (gray-400), hover (gray-600 via parent hover:bg-gray-100) |
| Quick-add form | idle, submitting (button disabled + "Adding..."), error (inline red text) |
| Edit form save button | idle, pending ("Saving...", disabled), error (inline red text) |
| Delete button | idle, opens modal — NOT pending until modal confirm pressed |
| Delete confirm button | idle, pending ("Deleting...", disabled) |
| Parent picker | idle, focused (blue ring), disabled for cycle-creating options (visually filtered out of options list, not `disabled` attribute) |
| Search input | idle, focused (blue ring), has-query (tree filtered in-place) |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — no shadcn initialized |
| third-party | none | not applicable |
No third-party registries. All components hand-crafted with Tailwind v4. No vetting required.
---
## 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:** pending