Design Lounge
Install

Nº 675 of 677

Rackwell strength set logger

A bench-press logger: set rows with last session's numbers as ghosts, a rest ring that starts on each tick, a plate-math sheet and a squash-pop personal best.

Preview

Open full size
390 × 844 · 21.9 KB · 234 lines

Use it

Free to use · credit appreciated

1 Once, in your project · Node 22.20+

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Rackwell strength set logger" (piece app-fitness-set-logger) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

216 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The in-workout screen of a fictional strength app, Rackwell, on the second exercise of “Push day A”: barbell bench press. It is built for sweaty thumbs between sets. Each set is a 60px row with last Tuesday’s numbers beside it; empty inputs show those numbers as ghost placeholders, and ticking a set with an empty field accepts the ghost, so a repeat session is one tap per set. Ticking starts a 2:00 rest ring in a dark dock above the home indicator, a plate-math sheet turns 82.5 kg into “25 + 5 + 1.25 per side”, and beating your estimated one-rep max pops a gold “New best” card with a squash-and-stretch medal and paper confetti. Material 3 Expressive language: tonal chalk surfaces, one oxide accent, big pill and rounded-square controls, emphasised easing. The detail worth copying: the ghost values are real placeholders, not pre-filled text, so the row shows at a glance which numbers you’ve actually typed.

Reference behaviour

  1. First frame: warm-up (40 × 10) and sets 1–2 (60 × 8, 75 × 5) are ticked on sage rows. Set 3 is the current row (chalk tint) with 80 typed and reps empty showing a ghost “5”. Set 4 has both fields empty with ghosts “77.5” and “5”. The dock is already resting: ring at 1:24 of 2:00, “Resting / Set 3 · 80 kg × 5”, buttons −15, +15, Skip. Header: “Push day A”, elapsed 38:12 counting every second, Finish.
  2. Tick a set: empty fields take their ghost values, the row turns sage, the tick button fills #2F6B3A and plays a squash pop (scale 1.28 × 0.86, held for 8% of 420ms as a hit-stop, then 0.92 × 1.08, then 1). Rest restarts at 2:00 and the dock names the next set.
  3. Personal best: estimated 1RM uses Epley, w × (1 + reps / 30). If it beats the stored best (80 × 4 → 90.7 kg), the gold card rises over the list (420ms expo), the medal squashes in 120ms later, 18 confetti chips burst from the tick button, a “PB” tag appears on the row, and the subline updates to “Best 80 × 5 · est. 93.3 kg”. The card leaves after 3.4s.
  4. Untick a done set: it reopens (inputs keep their values) and loses any PB tag.
  5. Rest ring: a 27px-radius circle, stroke 6, stroke-dasharray 169.6; offset = 169.6 × (1 − remaining / total), updated on requestAnimationFrame while running only. −15 and +15 change the time (never below 1s). Skip ends the rest; at zero the dock reads “Rest done”, the ring shows “Go” and thumps twice, and the adjust buttons hide. Skip on an idle dock starts a fresh 2:00.
  6. Plate math chip opens a bottom sheet for the current set’s weight (typed or ghost): a 56px weight with − / + steppers (2.5 kg), a 20 kg / 15 kg bar switch, a one-side barbell drawing with coloured plates, “Per side 25 + 5 / 30 kg each side”, and “Use 80 kg on set 3”. Plates are greedy from 25, 20, 15, 10, 5, 2.5, 1.25 kg; any remainder reads “x kg short”.
  7. “Use …” writes the weight into the current set and closes the sheet. Scrim tap, the close button and Escape close it too; focus returns to the chip.
  8. Add set appends a row whose ghosts are the last row’s numbers.

Structure

390 × 844, scrolling column, fixed dock
┌──────────────────────────────────────┐
│ 54px clear                           │
│ (‹)  Push day A           [Finish]   │ top row 56h, icon 44, pill 40h
│      38:12 · Week 6                  │
│ ▬▬ ▬▬ ▭▭ ▭▭ ▭▭                       │ progress: 5 × 6px, gap 4
│ EXERCISE 2 OF 5 · BARBELL            │ 12px, wdth 75, 0.12em
│ Bench press                          │ 38px / 850 / wdth 128
│ Best 80 × 4 · est. 90.7 kg [Plate math]│ chip 40h r12
│ ┌──────────────────────────────────┐ │ sets card r24, pad 8, #FFFDF9
│ │SET LAST TUE      KG   REPS       │ │ grid 40 / 1fr / 76 / 60 / 48, gap 8
│ │(W) 40 × 10     [ 40 ] [10] [✓]   │ │ rows 60h r16, inputs 44h r12
│ │(3) 77.5 × 5    [ 80 ] [ 5] [ ]   │ │ current row tinted #F5F1EA
│ │(4) 77.5 × 5    [77.5] [ 5] [ ]   │ │ ghosts in #6F685E, weight 450
│ │ [        + Add set         ]     │ │ 48h tonal
│ └──────────────────────────────────┘ │
│ ┌ Incline dumbbell press  next ────┐ │ outlined card r20
│ ┌──────────────────────────────────┐ │ toast (when PB): bottom 150, gold
│ │(1:24) Resting      [−15][+15][Skip]│ dock: bottom 34, inset 12, r28,
│ └──────────────────────────────────┘ │ #24211C, ring 64
└──────────────────────────────────────┘
  • main.app scrolls (padding 54 / 16 / 170) with a header row, div.prog, the title block, section.sets[aria-label=Sets] and the next-exercise card.
  • Each row: span.no (set label), span.prev (last time, with an aria-label), two input[inputmode] with placeholder ghosts, button.ck[aria-pressed].
  • div.toast[role=status] with the medal SVG and text; div.bits[aria-hidden] for confetti.
  • div.dock with the SVG ring, div.dtx, and the −15 / +15 / Skip buttons.
  • section.sheet[role=dialog][aria-modal=true] with stepper, [role=radiogroup] bar switch, the barbell SVG, the per-side text and the Use button, over a div.scrim.

Tokens

:root {
  /* surfaces */
  --bg: #F5F1EA;        /* page, current row */
  --card: #FFFDF9;      /* sets card, sheet */
  --tone: #EAE4D9;      /* tonal buttons, set chips, progress track */
  --edge: #8A8276;      /* input and tick borders, 3.3:1 on white */
  /* ink */
  --ink: #1D1B17; --ink-2: #4F4941; --ghost: #6F685E;   /* ghost 5.4:1 on white */
  /* accent and states */
  --pri: #B3401E;       /* Use button, current progress segment */
  --pri-c: #FFDBCD; --on-pri-c: #3A0B00;   /* Finish */
  --done: #DDEBD9; --done-ink: #245A30; --tick: #2F6B3A;
  --inv: #24211C; --inv-ink: #F5F1EA; --inv-2: #C9C1B4; --ring: #FFB59A; --ring-track: #3E3A33;
  --gold: #F2B33D; --gold-ink: #2B1D00;
  /* plates */
  --p25: #C8372D; --p20: #2E5BA8; --p15: #E5B62E; --p10: #3C8C4C; --p5: #F2EEE6; --p2_5: #1D1B17; --p1_25: #9A9388;
  /* type */
  --font: "Anybody", system-ui, sans-serif;   /* one variable family, wdth 50–150, wght 400–900 */
  --fs-title: 38px; --fs-weight: 56px; --fs-toast: 20px; --fs-input: 18px; --fs-body: 15px; --fs-prev: 14px; --fs-kick: 12px; --fs-head: 11px;
  /* shape */
  --r-card: 24px; --r-row: 16px; --r-input: 12px; --r-tick: 14px; --r-dock: 28px; --r-sheet: 28px; --r-pill: 999px;
  --row: 60px; --input: 44px; --ring: 64px;
  /* motion */
  --ease: cubic-bezier(.2,.7,.2,1);
  --emph: cubic-bezier(.2,0,0,1);
  --sheet: cubic-bezier(.32,.72,0,1);
  --expo: cubic-bezier(.16,1,.3,1);
  --t-pop: 420ms; --t-toast: 420ms; --t-medal: 640ms; --t-bits: 900ms; --t-sheet: 420ms; --t-row: 240ms;
  --rest: 120s; --pb-hold: 3400ms;
}

Typography

One variable family, used at different widths as different voices.

RoleSizeWeightWidth (wdth)Line-heightNotes
Exercise title38px850128%0.95−0.02em
Plate weight56px (unit 20px)900135%1tabular
Toast value20px850118%1.2
Input value18px750112%1tabular; ghost 450 in --ghost
Header title17px750110%1.15
Ring time17px800105%1tabular
Body, buttons14–16px650–750100%1.3
Kicker, table head, PB tag, NEW BEST10–12px700–80075–80%10.08–0.14em, upper

The multiplication sign is set in system-ui with 0.22em side margins, because Anybody’s × is too small at 14px.

Motion

WhatTriggerFrom → toDuration / easingReduced motion
Tick poptickscale 1 → 1.28×0.86 (hold 30–38%) → 0.92×1.08 → 1420ms --exponone
Row tintdone / reopenbackground chalk → sage240ms --easesame
Rest ringrAF while restingdashoffset follows remaining timelivesame
Rest overreaches 0ring scale 1 → 1.12 → 1, twice600ms --exponone
PB cardnew besttranslateY 16px, scale .96, opacity 0 → 1420ms --expoopacity 200ms
MedalPB card showsscale .3 rot −18° → 1.18×.84 → .94×1.06 → 1640ms --expo, delay 120msnone
ConfettiPB18 chips, 7×12px, fly 70–140px in a 150° fan, spin ±270°900ms --expo, stagger 8ms from 60msnot drawn
Plate sheetopen / closetranslateY 105% → 0420ms --sheetopacity 200ms

States

  • Done row: --done background, chip #C3D9BE with --done-ink, inputs bordered #6E8F6A on #F1F7EF, tick filled.
  • Current row: first undone row, tinted --bg.
  • Ghost: empty input shows the previous value at weight 450 in --ghost; a typed value is 750 in --ink.
  • PB row: a gold 10px “PB” tag above the previous-value column.
  • Resting / Rest done / idle: see Reference behaviour 5; the adjust buttons are display:none when not resting.
  • Sheet closed: visibility:hidden after the slide so its text never counts as on-screen.
  • Bar switch: selected segment is a raised --card pill with a 1px 3px shadow.
  • Focus-visible: 3px --pri outline, 2px offset, on every input and button.

Accessibility

  • Each input has its own label (“Set 3 weight, kilograms”, “Set 3 reps”); the previous value has “Last time 77.5 kilograms for 5”.
  • Tick buttons are aria-pressed with “Complete set 3” / “Undo set 3”; focus stays on the tick after re-render.
  • A polite live region announces “Set 3 done, 80 kilograms for 5. Rest 2 minutes.”, the new best with its 1RM, rest adjustments, “Rest done. Up next: …”, and sheet actions. The PB card is also role=status.
  • The plate sheet is a modal dialog: focus moves to + on open, Tab and Shift+Tab loop inside, Escape closes, focus returns to the chip.
  • The bar switch is a radiogroup with aria-checked.
  • Contrast: ghost #6F685E on white 5.4:1; --ink-2 on --bg 8.4:1; --inv-2 on --inv 9.6:1; white on --pri 5.7:1; --gold-ink on --gold 11:1. Input and tick borders #8A8276 are 3.3:1 on white.
  • Hit targets: ticks and inputs 44px tall, stepper 56px, dock buttons 42 × 44, Use 56px.

Responsive rules

  • The column caps at 440px and centres; the dock and sheet cap at 440 / 460px.
  • At 360 wide the grid keeps 40 / 1fr / 76 / 60 / 48; the previous column shrinks to about 70px and stays on one line with white-space: nowrap.
  • The list scrolls under the fixed dock; bottom padding 170px keeps the last row clear of the dock and any PB card.
  • On tablet, put the plate sheet in a 420px side panel instead of a bottom sheet.

Acceptance checklist

Always

  • Rows use real placeholders for last session’s numbers, and ticking an empty field accepts the ghost.
  • Ticking a set starts the rest ring from the full rest time and names the next set.
  • The ring is drawn with stroke-dashoffset from remaining ÷ total, on rAF only while resting.
  • −15 / +15 / Skip work, and the rest-over state is clearly different.
  • Estimated 1RM uses w × (1 + r / 30); beating the best shows the PB card, tag and updated subline.
  • Tick pop has a visible hit-stop at peak squash.
  • Plate math is greedy per side from 25 down to 1.25 kg, with a bar-weight switch and a “short” remainder.
  • The sheet is a focus-trapped modal that closes on scrim, close, Escape, and returns focus.
  • Inputs keep a 3:1 border; inputs and ticks are ≥ 44px.
  • Reduced motion: no pop, medal, confetti or thump; sheet and toast fade.
  • Scroll width is 390; the dock sits 34px above the bottom.

This demo

  • Push day A, exercise 2 of 5, Bench press; best 80 × 4 (est. 90.7 kg).
  • Sets W 40 × 10, 1 60 × 8, 2 75 × 5 done; set 3 80 kg with ghost 5 reps; set 4 ghosts 77.5 × 5.
  • Ticking set 3 shows “New best / 80 kg × 5 / Est. 1RM 93.3 kg, up 2.7 kg”.
  • Plate math for 80 kg on a 20 kg bar reads “Per side 25 + 5 / 30 kg each side”.

Implementation notes

Ghost values are placeholders, and the tick resolves them. Store typed values as strings, fall back to the previous session only on completion:

s.kg = s.kg || String(s.prev[0]);
s.r  = s.r  || String(s.prev[1]);
s.done = true;
const est = +s.kg * (1 + +s.r / 30);
if (est > best + 0.05) { celebrate(est - best); best = est; s.pb = true; }
startRest(120);
.row input::placeholder { color: #6F685E; font-weight: 450; opacity: 1; }  /* Firefox dims to .54 without opacity:1 */

The hit-stop is two equal keyframes. Holding the squash for a few frames is what makes the tick feel heavy:

@keyframes pop {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.28, .86); }
  38%  { transform: scale(1.28, .86); }  /* about 34ms hold at 420ms */
  60%  { transform: scale(.92, 1.08); }
  100% { transform: scale(1); }
}

Greedy plates per side.

const PLATES = [25, 20, 15, 10, 5, 2.5, 1.25];
function perSide(total, bar) {
  let left = (total - bar) / 2; const out = [];
  if (left < 0) return null;
  for (const p of PLATES) while (left >= p - 1e-6) { out.push(p); left -= p; }
  return { out, short: left };
}

Common mistakes: pre-filling the inputs with last session’s values (you can’t tell typed from copied), running the rest ring with setInterval(…, 1000) so it steps, re-rendering rows and losing focus on the tick, and leaving the closed sheet only translated off-screen without visibility:hidden.

Details

Palette
Type
Anybody
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Icon Museum

Let the icons be the only colour. Give each one a shelf, a lift on hover, and a quiet label.

All sources

Install once. Ask for anything.

The MCP server gives your agent 600+ pieces, the themes and the review tools. It installs the skill too. Free, no account.

claude mcp add -s user design-lounge -- npx -y design-lounge-mcp@latest

Claude Code. For Cursor, Codex, VS Code, ChatGPT and the skill alone, see Install.