docs(35): UI design contract for bug-fixes phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
190
.planning/phases/35-bug-fixes/35-UI-SPEC.md
Normal file
190
.planning/phases/35-bug-fixes/35-UI-SPEC.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
phase: 35
|
||||
slug: bug-fixes
|
||||
status: draft
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-04-19
|
||||
---
|
||||
|
||||
# Phase 35 — UI Design Contract
|
||||
|
||||
> Visual and interaction contract for Phase 35: bug-fixes. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||||
>
|
||||
> **Phase scope:** No new UI. All 5 fixes restore or polish existing interactions. The contract enforces consistency with patterns already established in the codebase — executor must not introduce new design tokens or visual patterns.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Tool | none (no shadcn — Tailwind v4 direct) |
|
||||
| Preset | not applicable |
|
||||
| Component library | none (custom components in `src/client/components/`) |
|
||||
| Icon library | Lucide (curated subset via `src/client/lib/iconData.ts`) |
|
||||
| Font | system-ui (browser default, no custom font declared) |
|
||||
|
||||
Source: `src/client/app.css` (`@import "tailwindcss"` — no additional config), `CONTEXT.md` code_context, codebase scan.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Declared values (must be multiples of 4). This phase does not introduce any new spacing — all values are the existing codebase standard.
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| xs | 4px | Icon gaps, inline padding (e.g. `px-1.5 py-0.5` badge padding) |
|
||||
| sm | 8px | Compact element spacing (e.g. `gap-1.5` badge rows) |
|
||||
| md | 16px | Default card content padding (`p-4`) |
|
||||
| lg | 24px | Section padding, modal padding (`p-6`) |
|
||||
| xl | 32px | Layout gaps |
|
||||
| 2xl | 48px | Major section breaks |
|
||||
| 3xl | 64px | Page-level spacing |
|
||||
|
||||
Exceptions: none for this phase.
|
||||
|
||||
Source: Codebase scan of `ItemCard.tsx`, `GlobalItemCard.tsx`, `CandidateCard.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
Matches existing codebase usage — no new type styles introduced in this phase.
|
||||
|
||||
| Role | Size | Weight | Line Height |
|
||||
|------|------|--------|-------------|
|
||||
| Body | 14px (text-sm) | 400 (normal) | 1.5 |
|
||||
| Label | 12px (text-xs) | 500 (medium) | 1.5 |
|
||||
| Heading | 14px (text-sm) | 600 (semibold) | 1.2 |
|
||||
| Display | 20px (text-xl) | 600 (semibold) | 1.2 |
|
||||
|
||||
Source: Codebase scan of `ItemCard.tsx` (`text-sm font-semibold text-gray-900`), `GlobalItemCard.tsx` (`text-xs font-medium text-gray-400`), `login.tsx` (`text-xl font-semibold`).
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
No new colors introduced. All values are the existing Tailwind gray/blue/green palette already used across card components.
|
||||
|
||||
| Role | Value | Usage |
|
||||
|------|-------|-------|
|
||||
| Dominant (60%) | `gray-50` (`#f9fafb`) | Page backgrounds, fallback image areas |
|
||||
| Secondary (30%) | `white` + `gray-100` border | Cards (`bg-white rounded-xl border border-gray-100`) |
|
||||
| Accent (10%) | `blue-50`/`blue-400` | Weight badges only |
|
||||
| Destructive | `red-100`/`red-500` | Remove-from-setup hover only (existing `ItemCard` remove button) |
|
||||
|
||||
Accent reserved for: weight value badges (`bg-blue-50 text-blue-400`). Price badges use `green-50`/`green-500`. Category badges use `gray-50`/`gray-600`. No new accent usage introduced this phase.
|
||||
|
||||
Source: Codebase scan of `ItemCard.tsx`, `GlobalItemCard.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## Image Skeleton Contract (FIX-03)
|
||||
|
||||
This phase adds image-specific loading states to all card types. The skeleton pattern must be identical across all three card components.
|
||||
|
||||
**Pattern:** `animate-pulse` gray placeholder fills the image area while `imageUrl` is resolving. Matches the existing `SkeletonGrid` pattern already used in `src/client/routes/global-items/index.tsx` and `src/client/routes/index.tsx`.
|
||||
|
||||
| Card | Image Area Selector | Skeleton Class |
|
||||
|------|---------------------|----------------|
|
||||
| `ItemCard` | `.aspect-[4/3]` container | `bg-gray-100 animate-pulse` |
|
||||
| `CandidateCard` | image area container | `bg-gray-100 animate-pulse` |
|
||||
| `GlobalItemCard` | `.aspect-[4/3]` container | `bg-gray-100 animate-pulse` |
|
||||
|
||||
**Loading state trigger:** When `imageUrl` is truthy but the `<img>` `onLoad` has not yet fired. Use React `useState` for a `loaded` boolean on each image.
|
||||
|
||||
**Loaded state transition:** Fade-in via `opacity-0 → opacity-100 transition-opacity duration-200` on the `<img>` tag once loaded.
|
||||
|
||||
**Fallback (no image):** Existing no-image placeholder (category icon centered on `bg-gray-50`) — unchanged.
|
||||
|
||||
Source: `CONTEXT.md` D-07/D-08, existing `animate-pulse` pattern in codebase.
|
||||
|
||||
---
|
||||
|
||||
## Cursor Contract (FIX-05)
|
||||
|
||||
All interactive elements must show `cursor-pointer`. Use Tailwind utility `cursor-pointer` — not a global CSS rule.
|
||||
|
||||
| Element Type | Cursor Rule | Notes |
|
||||
|---|---|---|
|
||||
| `<button>` that navigates (`linkTo` is not null) | `cursor-pointer` | `ItemCard` — add to existing conditional class |
|
||||
| `<button>` with `linkTo === null` | `cursor-default` | Existing correct behavior — do not change |
|
||||
| `span[role="button"]` action buttons | `cursor-pointer` | Already present on `ItemCard` action icons — verify all have it |
|
||||
| `<Link>` components | `cursor-pointer` | Links already inherit pointer via browser default, but add explicitly if missing |
|
||||
| Badges with `onClick` | `cursor-pointer` | Cycle badges (e.g. `ClassificationBadge`) |
|
||||
| FAB menu items | `cursor-pointer` | Verify `FabMenu.tsx` |
|
||||
| All `role="button"` elements | `cursor-pointer` | Tailwind utility per element |
|
||||
|
||||
Source: `CONTEXT.md` D-11/D-12, `ItemCard.tsx` lines 76 and 106/138/170.
|
||||
|
||||
---
|
||||
|
||||
## Auth Redirect Contract (FIX-04)
|
||||
|
||||
The `LoginPage` component (`src/client/routes/login.tsx`) must auto-redirect without showing any UI.
|
||||
|
||||
**Before:** Renders a full card UI with heading, description text, and a "Sign in" button that calls `window.location.href = "/login"`.
|
||||
|
||||
**After:** Immediately calls `window.location.href = "/login"` inside `useEffect` on mount (no auth-state check needed — redirect on mount unconditionally, because the server `/login` route handles the Logto redirect and will bounce back to `/` if already authenticated).
|
||||
|
||||
**Loading state:** A minimal full-screen centered loading indicator (`text-gray-500 text-sm`) is acceptable during the brief `useEffect` tick. No elaborate UI.
|
||||
|
||||
Source: `CONTEXT.md` D-09/D-10, `src/client/routes/login.tsx` existing implementation.
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
Phase 35 is a bug-fix phase. There are no new user-visible copy elements. The only copy consideration is the login page, which is being stripped of its UI.
|
||||
|
||||
| Element | Copy | Notes |
|
||||
|---------|------|-------|
|
||||
| Primary CTA | none (login page removes its button entirely) | FIX-04 |
|
||||
| Empty state heading | n/a — no new empty states introduced | |
|
||||
| Empty state body | n/a | |
|
||||
| Error state | n/a — no new error states | |
|
||||
| Destructive confirmation | n/a — no destructive actions in this phase | |
|
||||
|
||||
Source: `CONTEXT.md` decisions, `REQUIREMENTS.md` FIX-01 through FIX-05 — no copy requirements.
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
Components touched by this phase — executor must not create new components unless extending these.
|
||||
|
||||
| Component | File | Fix | Change Type |
|
||||
|-----------|------|-----|-------------|
|
||||
| `GearImage` | `src/client/components/GearImage.tsx` | FIX-03 | Add `loading="lazy"` to all `<img>` elements |
|
||||
| `ItemCard` | `src/client/components/ItemCard.tsx` | FIX-03, FIX-05 | Add image skeleton state; verify cursor-pointer coverage |
|
||||
| `CandidateCard` | `src/client/components/CandidateCard.tsx` | FIX-03, FIX-05 | Add image skeleton state; verify cursor-pointer coverage |
|
||||
| `GlobalItemCard` | `src/client/components/GlobalItemCard.tsx` | FIX-03, FIX-05 | Add image skeleton state; verify cursor-pointer coverage |
|
||||
| Thread detail route | `src/client/routes/threads/$threadId/index.tsx` | FIX-01 | Wire toolbar button to `openCatalogSearch`; delete `AddCandidateModal` |
|
||||
| `useItems` hook | `src/client/hooks/useItems.ts` | FIX-02 | Extend `ItemWithCategory` interface with image fields |
|
||||
| Login route | `src/client/routes/login.tsx` | FIX-04 | Replace page UI with immediate `useEffect` redirect |
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| shadcn official | none | not applicable — shadcn not initialized |
|
||||
| third-party | none | not applicable |
|
||||
|
||||
No new third-party components or registries in this phase.
|
||||
|
||||
---
|
||||
|
||||
## 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