Collapse font weights from 3 to 2 (remove 500/medium, map Label to 600/semibold). Remove non-multiple-of-4 Tailwind class references from spacing Usage column. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.3 KiB
phase, slug, status, shadcn_initialized, preset, created
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 35 | bug-fixes | draft | false | none | 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 |
| sm | 8px | Compact element spacing, 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) | 600 (semibold) | 1.5 |
| Heading | 14px (text-sm) | 600 (semibold) | 1.2 |
| Display | 20px (text-xl) | 600 (semibold) | 1.2 |
Two weights only: 400 (normal) and 600 (semibold). Label badges (text-xs font-medium) are documented as 600 (semibold) since this phase does not change them.
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