---
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`), tree row indent per depth level (`pl-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:
- Chevron toggle button: 28px click target minimum (`p-1` around 16px icon = 24px; acceptable for desktop-only admin tool).

Note: Tree row indent is `pl-4` (16px) per depth level — standard `md` token, no exception required. Root tags (depth 0) have no indent.

---

## Typography

| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px (text-sm) | 400 (regular) | 1.5 |
| Label / Display | 11px (text-[11px]) | 600 (semibold) | 1.4 |
| Heading | 18px (text-lg) | 600 (semibold) | 1.2 |

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`.

Label/Display role = field labels and table column headers (uppercase, tracked, muted gray-400). Set to 11px (`text-[11px]`) to establish a clear 3px hierarchy gap above 14px body. Previously 12px — adjusted to resolve weak hierarchy flag.

Three sizes in use: 11px (label/display) · 14px (body) · 18px (heading). Two weights: 400 (regular) · 600 (semibold).

---

## 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.

---

## Focal Point

**List page primary visual anchor: the tag tree view.**

The collapsible `TagTreeView` card is the dominant element on the list page. It occupies the main content column and carries all primary interactions (expand/collapse, row-level edit navigation). The `TagQuickAddForm` is positioned above the tree as a supporting action — it is intentionally compact (`flex items-center gap-3`) to avoid competing with the tree for attention.

Visual hierarchy enforcement:
- Tree card: `rounded-xl border border-gray-100 bg-white` — elevated white surface drawing the eye.
- Quick-add form: no card wrapper, sits flush in the page background (`bg-gray-50`), visually subordinate.

**Edit page primary visual anchor: the tag name input field.**

On the edit page the name field is the first and most prominent form element. The parent picker and action buttons are secondary.

---

## 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-4` (16px) 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 Tag" |
| 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
