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:
258
.planning/phases/38-admin-tag-management/38-UI-SPEC.md
Normal file
258
.planning/phases/38-admin-tag-management/38-UI-SPEC.md
Normal 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
|
||||
Reference in New Issue
Block a user