Skip to content

#682 Replace The Position Card Stat Tiles With Four Status Circles - #685

Open
cielbellerose wants to merge 6 commits into
devfrom
682-position-card-status-circles
Open

cielbellerose wants to merge 6 commits into
devfrom
682-position-card-status-circles

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #682

Summary

  • Replaces the per-position stat cluster (a Total tile + 4 tiles that omitted reached_out/reviewing) with a 2×2 grid of four status circles plus a Total: N line — the new buckets partition every status getPositionApplicationStats counts, so the circles now always sum to Total.
  • PositionCard stays a server component; the new circles are a small 'use client' leaf (PositionStatCircles) since Radix Tooltip requires it.

Changes

  • lib/constants.ts — removed POSITION_CARD_STAT_STATUSES (single consumer, being replaced); added POSITION_STAT_BUCKETS (Applied / In progress / Accepted / Rejected, ordered, each mapped to a BadgeVariant) and STATUS_BADGE_VARIANT_TO_FILL (fill classes, sibling to the existing STATUS_BADGE_VARIANT_TO_DOT).
  • components/features/position-stat-circles.tsx — new 'use client' leaf rendering the 2×2 circle grid, tooltips (category + count on hover/focus), sr-independent aria-labels, hover/focus scale animation (motion-reduce:transition-none), a pointer-coarse:block visible label per circle for touch devices, and the Total: N line.
  • components/features/position-card.tsx — removed the old PositionStatCluster and its now-unused imports; renders PositionStatCircles in the existing right-column slot.
  • tests/unit/constants.test.ts — added two invariants to status-set invariants: the bucket statuses union to every status but draft/withdrawn, and the buckets are pairwise disjoint.
  • docs/WORKFLOWS.md — updated PM-2's happy path to describe the four status circles and the Total: N line.

Testing plan

  • Sign in as a manager, go to /manage/positions. Confirm the four circle counts sum exactly to Total: N, including a position with applications in reached_out or reviewing (the old cluster under-reported these).
  • Cross-check one position's numbers against /manage/applications?positionId=<id> filtered by status.
  • Hover each circle: it grows smoothly and the tooltip shows category + count (e.g. In progress — 4).
  • Tab through a card: each circle takes focus in order, shows a focus ring, grows, and shows its tooltip.
  • Enable OS "reduce motion" and re-hover: the size change is instant, no animation.
  • Mobile emulation / a real phone at 375px: each circle has a visible label beneath it, no tooltip needed, cluster sits below card content without clipping.
  • Check 768px and 1280px: cluster stays in the card's right column, nothing overlaps.
  • Find a position with zero applications: four grey 0 circles and Total: 0, same footprint as a populated card.
  • Toggle dark mode and confirm every circle's number is legible on its fill, including the grey zero state.
  • Sign in as an admin and confirm "All Positions" (drafts + Archived disclosure) renders the same circles.
  • Open /manage/positions/[id]/edit for an archived position and confirm the archived callout's unresolved-count line is unchanged (this page doesn't render PositionCard).
  • With a screen reader, confirm a circle announces "Applied: 12, button" and the touch label isn't double-announced.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — 351 passed; 1 pre-existing failure (tests/unit/email-delivery-events.test.ts, missing DATABASE_URL in this sandbox) reproduces identically on dev before this change — unrelated to this PR.

Notes

  • No Prisma or server-action changes — this is a presentation-only fix; PositionApplicationStats is unchanged.
  • Per the plan's scope correction: /manage/positions/[id]/edit never rendered PositionCard (it only reads the archived-callout count), so that acceptance criterion is "verify unaffected" rather than a code change.

@cielbellerose cielbellerose added the claude Will be worked on by Claude label Sep 8, 2026
@cielbellerose cielbellerose self-assigned this Sep 8, 2026
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aplio Ready Ready Preview Sep 8, 2026 1:41am UTC

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 8, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 1 · needs revision

1 open — 1 🟡 Low (see inline)

Comment thread lib/constants.ts Outdated
@cielbellerose cielbellerose added needs revision Review found issues that need fixing and removed reviewing Review agent working (in-flight) labels Sep 8, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Human feedback — apply alongside the Cycle 1 review findings

Two design changes to components/features/position-stat-circles.tsx, requested directly by the ticket owner. Treat these as required scope for this revision, in addition to R1-L1.

1. The circles must touch — no gap in the 2×2 grid

Drop the grid gap to zero so adjacent circles are tangent and the cluster reads as one bubble cluster rather than four separate tiles.

Two knock-on effects that must be handled, not ignored:

  • Hover overlap. With no gap, hover:scale-110 makes a circle grow into its neighbours. That is fine visually — bubbles overlapping is the intended look — but the growing circle must sit on top, so add a stacking bump on hover/focus (e.g. relative + hover:z-10 focus-visible:z-10). Without it the scaled circle renders under its neighbours and looks clipped.
  • Touch labels collide. The pointer-coarse:block labels currently sit directly beneath each circle. At zero horizontal gap, Applied and In progress will run into each other. Suggested resolution: keep gap-0 for hover-capable pointers and restore spacing only on coarse pointers (gap-0 pointer-coarse:gap-3, or equivalent), so touch users keep readable labels while pointer users get the tangent cluster. Use your judgement if there is a cleaner fix, but do not ship overlapping labels.

Each circle must still meet the ~44px touch-target minimum (docs/DESIGN.md §Mobile-first). Adjacent targets having no separation between them is acceptable; targets shrinking below 44px is not.

2. Make the hover animation more "bubbly"

The current transition-transform duration-150 hover:scale-110 reads as a flat, linear nudge. Make it feel springy — a bubble inflating, not a box resizing. Reasonable approaches:

  • An overshoot / back-out easing curve (custom cubic-bezier via an arbitrary Tailwind value) so the circle overshoots its target scale slightly and settles back, rather than easing in a straight line.
  • A slightly stronger scale than 110 if it still reads well at 44px and doesn't cause clipping.
  • Optionally a subtle squash-and-settle rather than a pure uniform scale.

Constraints that still hold:

  • motion-reduce:transition-none stays — reduced-motion users get the size change instantly, with no animation.
  • Transform only. Nothing may reflow the grid or the surrounding card.
  • Keep the effect on focus-visible as well as hover, so keyboard users get the same affordance.
  • Tailwind 4 gates hover: behind @media (hover: hover), so the scale must not stick after a tap on touch devices.

Verify the result at 375 / 768 / 1280 and in dark mode, per the plan's existing testing checklist.

@cielbellerose cielbellerose added revising Revise agent working (in-flight) and removed needs revision Review found issues that need fixing labels Sep 8, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-L1 · 78edb06

Also applied the human-feedback design changes to position-stat-circles.tsx: zero grid gap (gap-0 pointer-coarse:gap-3, so touch labels stay separated) with relative + hover:z-10 focus-visible:z-10 on each circle so a scaled hover/focus target stacks above its neighbours; and a springier hover/focus transition (duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)], an overshoot-and-settle curve) in place of the flat linear scale, with motion-reduce:transition-none untouched.

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 8, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Human feedback — Cycle 2 colour change

One change, requested by the ticket owner after reviewing the rendered circles. No other scope.

Replace the "In progress" bucket's amber fill with violet, and put white text on it

Why: the current --warning amber is oklch(0.7 0.15 85)#c99500, which is 2.55:1 against white — a hard WCAG AA fail for the circle's 16px semibold count. That is why --warning-foreground is near-black today. Amber is intrinsically high-luminance: any amber dark enough to carry white text has stopped reading as amber. So the bucket moves to a different hue rather than a darker yellow.

Target colour: oklch(0.52 0.18 300)#7b47bf, 5.66:1 against white (AA). Chosen because it is maximally distinct from the other three circles, which matters more than usual here since the category labels are hover-only on pointer devices:

Bucket Fill vs white
Applied #006ebe (info) 4.99:1
In progress #7b47bf (new) 5.66:1
Accepted #097f23 (success) 4.86:1
Rejected #d40e14 (destructive) 5.13:1

All four circles now carry white text uniformly.

Scope this to the circles — do NOT repoint the global --warning token

--warning also drives components/ui/warning-callout.tsx, components/features/profile-completeness-banner.tsx, and the "closing soon" icon in components/features/position-date-line.tsx. Those are genuine caution signals where amber carries meaning and violet would mislead; they use the token as a tint (/5, /10, /40) or an icon colour and already pass AA. Leave --warning and --warning-foreground exactly as they are.

Suggested implementation

  • Add a new semantic token in app/globals.css — e.g. --progress: oklch(0.52 0.18 300) and --progress-foreground: oklch(0.98 0 0) — in both the :root and dark blocks, using the same value in both, matching how --info / --success / --destructive are already defined identically across modes (lines 75/120, 88/132, 92/136). Register --color-progress / --color-progress-foreground in the @theme block alongside the existing --color-warning entries (lines 43–48).
  • The in_progress entry in POSITION_STAT_BUCKETS (lib/constants.ts) can no longer get its colour from STATUS_BADGE_VARIANT_TO_FILL, since the new colour is outside the BadgeVariant union. Decouple it cleanly — e.g. let each bucket carry its fill classes directly rather than routing through variant. Use your judgement on the exact shape; the requirement is that bucket colour still lives in one place in lib/constants.ts, not split between a constant and a component-local map.
  • If STATUS_BADGE_VARIANT_TO_FILL ends up with no consumers after this, delete it rather than leaving it unused.
  • The zero-count treatment is unchanged: bg-muted text-foreground, never opacity-*.
  • Naming: In progress stays the label. Only the fill changes.

Explicitly not changing

  • --destructive stays oklch(0.55 0.22 28). It is already within 3.5° of hue of the site logo red (#d41b2c = oklch(0.557 0.213 24.5)) at the same lightness; the difference is imperceptible and the token is used in 31 files. Do not touch it.
  • No change to --info, --success, --warning, or any status badge variant.

Verify

  • All four circle counts are legible in white at 375 / 768 / 1280, in both light and dark mode, including the 0 state (which stays grey with text-foreground).
  • The archived-position warning callout, profile-completeness banner, and "closing soon" date icon are unchanged and still amber.
  • The reviewing status badge elsewhere in the app is unchanged.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed ready for review PR ready for review agent needs revision Review found issues that need fixing labels Sep 8, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 2 colour change

in-progress circle fill moved to new --progress token (oklch(0.52 0.18 300), white text, same value light/dark); --warning/--warning-foreground untouched · STATUS_BADGE_VARIANT_TO_FILL deleted (no remaining consumers), bucket colour now lives solely in POSITION_STAT_BUCKETS.fillClassName · b5c816f

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Sep 8, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 2 · needs revision

1 open — 1 🟡 Low (see inline)

Comment thread lib/constants.ts Outdated
@cielbellerose cielbellerose added needs revision Review found issues that need fixing and removed reviewing Review agent working (in-flight) labels Sep 8, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 8, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 4 · approved

0 open — clean

@cielbellerose cielbellerose added approved Review passed, ready to merge needs human Pipeline escalation: 3 review cycles without convergence and removed reviewing Review agent working (in-flight) labels Sep 8, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Human feedback — Cycle 5: Total moves left of the circles

Layout-only change, requested by the ticket owner after seeing the approved version. The approved label was removed so this could land in the same PR. No colour, token, bucket, test, or behaviour changes — do not touch anything outside the cluster's layout.

The change

Currently the cluster is a vertical stack: the circle row, with Total: N centred underneath. Make it a single horizontal row:

Total: 21    ( 12 ) ( 4 ) ( 3 ) ( 2 )
  • Total: N sits to the left of the circles, on the same line.
  • The four circles keep their existing order, sizing, rings and spacing.
  • The whole cluster stays anchored at the top right of the position card — the existing right-column slot with sm:items-start already does this, so it should need no change in position-card.tsx.

Alignment detail that matters

On coarse-pointer devices the pointer-coarse labels render beneath each circle, making the circle group taller. Total: N must stay aligned to the circles' centre line, not to the centre of the whole group — otherwise it drifts downward on touch devices while looking correct on desktop.

Suggested approach: make the outer wrapper flex items-start, and give the Total element the same fixed height as the circle button (min-h-11 with flex items-center) so it centres against the rings independently of whatever renders below them. Use your judgement on the exact mechanism; the requirement is that Total lines up with the rings in both pointer modes.

Keep unchanged

  • Total: N keeps its current muted, small, subordinate treatment and tabular-nums; add whitespace-nowrap so the label can't wrap mid-phrase.
  • Circle sizing (size-7 ring inside a min-h-11 min-w-11 button), ring colours from the stock info / warning / success / destructive tokens, and the muted border-border zero state.
  • Tooltip + aria-label per circle, the overshoot hover/focus animation, motion-reduce:transition-none, hover:z-10 / focus-visible:z-10.
  • The pointer-coarse labels and their max-w-11 break-words capping.
  • The role="region" / aria-label="Application stats" wrapper.
  • POSITION_STAT_BUCKETS, the partition tests, and docs/WORKFLOWS.md PM-2 — all still accurate; PM-2 describes the circles and total without asserting their relative position, so confirm that reads true and leave it alone if so.
  • --progress stays deleted. Do not reintroduce any new colour token.

Verify

  • 375 / 768 / 1280, light and dark: the row fits the card's right column without clipping, wrapping, or pushing the position title, and stacks sensibly on mobile.
  • Total is vertically centred against the rings with and without the coarse-pointer labels showing.
  • The zero state (all four rings muted, Total: 0) keeps the same footprint as a populated card.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs human Pipeline escalation: 3 review cycles without convergence needs revision Review found issues that need fixing labels Sep 8, 2026
cielbellerose and others added 6 commits September 7, 2026 22:21
The old cluster omitted reached_out/reviewing from its four tiles, so
Total never matched their sum. The new buckets partition every status
getPositionApplicationStats counts, so the circles always sum to Total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trim the oversize bucket comment (R1-L1) and, per the ticket
owner's follow-up, make the stat circles tangent with a
stacking bump on hover/focus and a springy overshoot easing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a --progress token (identical light/dark) so all four status
circles carry AA-compliant white text; --warning is untouched since
it still drives the callout/banner/date-line caution uses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the 2x2 filled grid with four smaller border-only circles in
a horizontal row, per the Cycle 3 owner feedback. Border-only fill
removes the need for text-on-color contrast, so in_progress reverts
to the stock warning token and --progress/--progress-foreground are
deleted entirely (resolves R3-M1) rather than replaced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Total now sits in a min-h-11 flex-centered box beside the row so it
stays aligned to the circles' centre line even when pointer-coarse
labels render beneath them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cielbellerose
cielbellerose force-pushed the 682-position-card-status-circles branch from 9d75799 to 0f7e633 Compare September 8, 2026 02:23
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 5

0f7e633

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Sep 8, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 5 · approved

0 open — clean

@cielbellerose cielbellerose added approved Review passed, ready to merge needs human Pipeline escalation: 3 review cycles without convergence and removed reviewing Review agent working (in-flight) labels Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge claude Will be worked on by Claude needs human Pipeline escalation: 3 review cycles without convergence

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace The Position Card Stat Tiles With Four Status Circles

1 participant