docs(01-input-ux): create phase plan

This commit is contained in:
2026-02-16 06:38:05 +01:00
parent 1de3e08b8d
commit c88729dfed
4 changed files with 598 additions and 4 deletions
+4 -4
View File
@@ -22,12 +22,12 @@ Three phases deliver the improvements in order of risk and value. Phase 1 fixes
3. Weight and reps inputs reject negative values — typing or stepping below 0 is blocked
4. All input fields and action buttons are at least 44px tall and usable with one thumb
5. Input font size is at least 16px so iOS does not auto-zoom the page on focus
**Plans**: TBD
**Plans:** 3 plans
Plans:
- [ ] 01-01: Build stepper input component (shared for weight and reps)
- [ ] 01-02: Apply validation constraints and kg suffix to all workout inputs
- [ ] 01-03: Audit and fix touch target sizes across workout UI
- [ ] 01-01-PLAN.md — Create StepperInput, WeightInput, RepsInput components + stepper CSS
- [ ] 01-02-PLAN.md — Integrate WeightInput/RepsInput into WorkoutPage ExerciseCard set rows
- [ ] 01-03-PLAN.md — Audit and fix touch target sizes and input font-size across all UI
### Phase 2: Flexible Sets
**Goal**: Users can add or remove sets on any exercise mid-workout and have those changes persist
+298
View File
@@ -0,0 +1,298 @@
---
phase: 01-input-ux
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/components/StepperInput.jsx
- frontend/src/components/WeightInput.jsx
- frontend/src/components/RepsInput.jsx
- frontend/src/App.css
autonomous: true
must_haves:
truths:
- "StepperInput renders a numeric input flanked by - and + buttons"
- "Tapping - or + changes the value by the configured step amount"
- "Typing a negative number is rejected; the value is clamped to min (0 by default)"
- "The - button is visually disabled when value equals min"
- "WeightInput passes step=2.5, suffix=kg to StepperInput"
- "RepsInput passes step=1, no suffix to StepperInput"
artifacts:
- path: "frontend/src/components/StepperInput.jsx"
provides: "Reusable controlled stepper input component"
exports: ["default StepperInput"]
- path: "frontend/src/components/WeightInput.jsx"
provides: "Weight-specific wrapper (2.5kg steps, kg suffix)"
exports: ["default WeightInput"]
- path: "frontend/src/components/RepsInput.jsx"
provides: "Reps-specific wrapper (1 rep steps)"
exports: ["default RepsInput"]
- path: "frontend/src/App.css"
provides: "Stepper component styles"
contains: ".stepper-wrapper"
key_links:
- from: "frontend/src/components/WeightInput.jsx"
to: "frontend/src/components/StepperInput.jsx"
via: "import StepperInput"
pattern: "import StepperInput"
- from: "frontend/src/components/RepsInput.jsx"
to: "frontend/src/components/StepperInput.jsx"
via: "import StepperInput"
pattern: "import StepperInput"
---
<objective>
Create three new React components: StepperInput (reusable base), WeightInput (2.5kg steps + kg suffix), and RepsInput (1 rep steps). Add CSS styles to App.css.
Purpose: These components are the foundation that Plan 02 will drop into WorkoutPage to replace the bare inputs. They must be complete and self-contained before integration happens.
Output: Three .jsx files in frontend/src/components/, new CSS block in App.css.
</objective>
<execution_context>
@/home/intense/.claude/get-shit-done/workflows/execute-plan.md
@/home/intense/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-input-ux/01-RESEARCH.md
@frontend/src/index.css
@frontend/src/App.css
</context>
<tasks>
<task type="auto">
<name>Task 1: Create StepperInput.jsx</name>
<files>frontend/src/components/StepperInput.jsx</files>
<action>
Create a new controlled React component at frontend/src/components/StepperInput.jsx.
Props:
- value (string, default '')
- onChange (function, receives string)
- step (number, default 1)
- min (number, default 0)
- max (number or null, default null)
- label (string, default 'Value')
- suffix (string, default '')
- disabled (boolean, default false)
Behavior:
- handleInputChange: parse e.target.value as float. If empty string, call onChange(''). If parsed >= min (and <= max if set), call onChange(String(parsed)). If parsed < min, call onChange(String(min)). Reject non-numeric input silently.
- handleDecrement: newVal = Math.max(min, numValue - step). Call onChange(String(newVal)). No-op if disabled.
- handleIncrement: newVal = numValue + step. If max is null or newVal <= max, call onChange(String(newVal)). No-op if disabled.
- canDecrement = numValue > min
- canIncrement = max === null || numValue < max
JSX structure:
```
<div className="stepper-wrapper" role="group" aria-labelledby={`stepper-label-${label}`}>
<label id={`stepper-label-${label}`} className="stepper-label">{label}</label>
<div className="stepper-container">
<button type="button" className="stepper-btn stepper-minus" onClick={handleDecrement}
disabled={!canDecrement || disabled} aria-label={`Decrease ${label}`}></button>
<div className="stepper-input-wrapper">
<input type="number" value={value} onChange={handleInputChange}
min={min} max={max ?? undefined} step={step}
inputMode={step % 1 === 0 ? 'numeric' : 'decimal'}
className="stepper-input" aria-label={label} disabled={disabled} />
{suffix && <span className="input-suffix">{suffix}</span>}
</div>
<button type="button" className="stepper-btn stepper-plus" onClick={handleIncrement}
disabled={!canIncrement || disabled} aria-label={`Increase ${label}`}>+</button>
</div>
</div>
```
Export default StepperInput.
Note: Do NOT use useState or useEffect inside this component — it is a pure controlled component. All state lives in the parent.
</action>
<verify>File exists at frontend/src/components/StepperInput.jsx with exported default function. Check: grep -n "export default" frontend/src/components/StepperInput.jsx</verify>
<done>StepperInput.jsx exists, exports default, contains handleDecrement, handleIncrement, handleInputChange logic with min clamping.</done>
</task>
<task type="auto">
<name>Task 2: Create WeightInput.jsx and RepsInput.jsx, add stepper CSS to App.css</name>
<files>
frontend/src/components/WeightInput.jsx
frontend/src/components/RepsInput.jsx
frontend/src/App.css
</files>
<action>
Create frontend/src/components/WeightInput.jsx:
- Imports StepperInput from './StepperInput'
- Renders: &lt;StepperInput value={value} onChange={onChange} step={2.5} min={0} max={null} label="Weight" suffix="kg" disabled={disabled} /&gt;
- Props: value, onChange, disabled (default false)
- Export default WeightInput
Create frontend/src/components/RepsInput.jsx:
- Imports StepperInput from './StepperInput'
- Renders: &lt;StepperInput value={value} onChange={onChange} step={1} min={0} max={null} label="Reps" suffix="" disabled={disabled} /&gt;
- Props: value, onChange, disabled (default false)
- Export default RepsInput
Append to frontend/src/App.css a new section after the last line:
```css
/* ============================================
STEPPER INPUT COMPONENT
============================================ */
.stepper-wrapper {
display: flex;
flex-direction: column;
gap: 0.4rem;
width: 100%;
}
.stepper-label {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stepper-container {
display: flex;
align-items: center;
gap: 0.25rem;
background: var(--bg-card);
border-radius: 8px;
border: 1px solid var(--border);
padding: 0.2rem;
height: 48px;
}
.stepper-btn {
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
background: var(--bg-secondary);
border: none;
border-radius: 6px;
color: var(--text-primary);
font-size: 1.4rem;
font-weight: 300;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
line-height: 1;
}
.stepper-btn:hover:not(:disabled) {
background: var(--accent);
color: white;
}
.stepper-btn:active:not(:disabled) {
transform: scale(0.94);
}
.stepper-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.stepper-input-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
min-width: 0;
}
.stepper-input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 16px; /* >= 16px prevents iOS auto-zoom */
font-weight: 600;
text-align: center;
padding: 0.4rem 0.25rem;
outline: none;
font-family: inherit;
}
.stepper-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Remove browser native number spinners */
.stepper-input::-webkit-outer-spin-button,
.stepper-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.stepper-input[type='number'] {
-moz-appearance: textfield;
}
.input-suffix {
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
/* Mobile: slightly larger touch targets */
@media (max-width: 480px) {
.stepper-container {
height: 52px;
}
.stepper-btn {
width: 48px;
height: 48px;
min-width: 48px;
min-height: 48px;
}
}
```
Important: Do NOT delete any existing content in App.css. Only append the new block at the end of the file.
</action>
<verify>
1. grep -n "export default WeightInput" frontend/src/components/WeightInput.jsx
2. grep -n "export default RepsInput" frontend/src/components/RepsInput.jsx
3. grep -n "stepper-wrapper" frontend/src/App.css
</verify>
<done>WeightInput.jsx and RepsInput.jsx exist and export defaults. App.css contains .stepper-wrapper block. No existing CSS was removed.</done>
</task>
</tasks>
<verification>
Run the dev server and confirm no import errors:
cd /workspace/gravl/frontend && npm run build 2>&1 | tail -20
Expected: build succeeds (exit 0) or only pre-existing warnings. No "Cannot find module" errors.
</verification>
<success_criteria>
- StepperInput.jsx: controlled component, rejects negative input, +/- buttons 44px, font-size 16px, aria-labels present
- WeightInput.jsx: wraps StepperInput with step=2.5, suffix="kg"
- RepsInput.jsx: wraps StepperInput with step=1, no suffix
- App.css: stepper styles appended, all buttons min 44x44px, font-size 16px on .stepper-input
- Build passes with no new errors
</success_criteria>
<output>
After completion, create `.planning/phases/01-input-ux/01-01-SUMMARY.md` using the summary template.
</output>
+152
View File
@@ -0,0 +1,152 @@
---
phase: 01-input-ux
plan: 02
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
- frontend/src/pages/WorkoutPage.jsx
autonomous: true
must_haves:
truths:
- "Each set row in WorkoutPage shows a WeightInput (- button, value, kg, + button) instead of a bare input"
- "Each set row shows a RepsInput (- button, value, + button) instead of a bare input"
- "Tapping + on weight increments by 2.5; tapping - decrements by 2.5"
- "Tapping + on reps increments by 1; tapping - decrements by 1"
- "Typing a negative weight or reps value is blocked — value stays at 0"
- "The kg suffix is visible next to the weight value inside the stepper"
artifacts:
- path: "frontend/src/pages/WorkoutPage.jsx"
provides: "Updated ExerciseCard using stepper inputs"
contains: "WeightInput"
key_links:
- from: "frontend/src/pages/WorkoutPage.jsx"
to: "frontend/src/components/WeightInput.jsx"
via: "import WeightInput"
pattern: "import WeightInput"
- from: "frontend/src/pages/WorkoutPage.jsx"
to: "frontend/src/components/RepsInput.jsx"
via: "import RepsInput"
pattern: "import RepsInput"
---
<objective>
Replace the two bare `<input type="number">` elements inside ExerciseCard's set-row with WeightInput and RepsInput components. Remove the now-unused .weight-input and .reps-input CSS rules.
Purpose: Users logging weight and reps now see +/- steppers with validation and the kg suffix — satisfying INP-01 through INP-03 and INP-06/INP-07.
Output: Updated WorkoutPage.jsx. The bare inputs are gone; stepper components are in.
</objective>
<execution_context>
@/home/intense/.claude/get-shit-done/workflows/execute-plan.md
@/home/intense/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@frontend/src/pages/WorkoutPage.jsx
@frontend/src/App.css
@.planning/phases/01-input-ux/01-01-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Integrate WeightInput and RepsInput into ExerciseCard</name>
<files>frontend/src/pages/WorkoutPage.jsx</files>
<action>
In frontend/src/pages/WorkoutPage.jsx, make these targeted changes:
1. Add two import statements at the top of the file (after the existing Icon import):
```
import WeightInput from '../components/WeightInput'
import RepsInput from '../components/RepsInput'
```
2. Inside the ExerciseCard component, find the set-row rendering block (around lines 321-343). Replace the two bare `<input>` elements and the separator span with:
```jsx
<WeightInput
value={input.weight}
onChange={(val) => handleInputChange(setNum, 'weight', val)}
/>
<span className="input-separator">×</span>
<RepsInput
value={input.reps}
onChange={(val) => handleInputChange(setNum, 'reps', val)}
/>
```
The handleInputChange function signature already accepts a plain string value (second arg is field name, third is value string) — the new components pass the string directly via onChange, which matches.
3. Update the .set-inputs CSS in App.css. Find the `.set-inputs` rule and change `align-items: center` to `align-items: flex-start` so the taller stepper containers align correctly at the top of the row. Also ensure `.set-row` uses `align-items: flex-start` rather than `center` (the complete-btn can stay aligned via its own styling).
In App.css, update:
```css
.set-inputs {
flex: 1;
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.set-row {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
background: var(--bg-secondary);
border-radius: 8px;
transition: all 0.2s;
}
```
4. Remove the now-redundant `.weight-input` and `.reps-input` rules from App.css. Search for:
```
.weight-input,
.reps-input {
```
and delete that entire rule block (approximately 8 lines). Also delete the mobile override block:
```
.weight-input,
.reps-input {
width: 60px;
padding: 0.5rem;
}
```
inside the `@media (max-width: 480px)` section.
Do NOT change any other part of WorkoutPage.jsx (warmup logic, progression hints, complete-btn, finish-workout-btn, etc.).
</action>
<verify>
1. grep -n "WeightInput\|RepsInput" frontend/src/pages/WorkoutPage.jsx
2. grep -n "weight-input\|reps-input" frontend/src/App.css (should return nothing — rules deleted)
3. cd /workspace/gravl/frontend && npm run build 2>&1 | tail -20
</verify>
<done>
- WorkoutPage.jsx imports and uses WeightInput and RepsInput in set rows
- .weight-input and .reps-input CSS rules are removed
- Build passes with no new errors
</done>
</task>
</tasks>
<verification>
Manual check: open the app in a browser, navigate to a workout, expand an exercise. Each set row should show:
[ - ] [ value ] [ kg ] [ × ] [ - ] [ value ] [ + ] [ complete ]
Tap + on weight: increments by 2.5. Tap - on reps: decrements by 1. Try typing -5 in weight: stays at 0.
</verification>
<success_criteria>
- Set rows use WeightInput and RepsInput, not bare inputs
- Weight increments by 2.5 per tap; reps increments by 1 per tap
- Negative values are blocked
- "kg" suffix is visible inside the weight stepper
- Build passes
</success_criteria>
<output>
After completion, create `.planning/phases/01-input-ux/01-02-SUMMARY.md` using the summary template.
</output>
+144
View File
@@ -0,0 +1,144 @@
---
phase: 01-input-ux
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/App.css
autonomous: true
must_haves:
truths:
- "The back button in WorkoutPage header is at least 44px tall (tappable with one thumb)"
- "The complete-btn (set checkmark) is at least 44px tall — already 44px, verify it is not overridden"
- "The warmup-done-btn is at least 44px tall"
- "Warmup items are at least 44px tall"
- "The finish-workout-btn is at least 44px tall"
- "The .start-btn and .start-workout-btn are at least 44px tall"
- "All form inputs in auth and onboarding pages have font-size 16px to prevent iOS auto-zoom"
artifacts:
- path: "frontend/src/App.css"
provides: "Touch target audit fixes — explicit min-height on all interactive elements"
contains: "min-height: 44px"
key_links: []
---
<objective>
Audit all interactive elements in App.css for touch target compliance (min 44px height) and font-size compliance (min 16px on inputs). Fix any violations with targeted CSS additions.
Purpose: Users on mobile can tap every button and input without missing. iOS auto-zoom does not trigger on any input in the app.
Output: App.css updated with min-height and font-size fixes for non-stepper elements.
</objective>
<execution_context>
@/home/intense/.claude/get-shit-done/workflows/execute-plan.md
@/home/intense/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@frontend/src/App.css
@frontend/src/index.css
</context>
<tasks>
<task type="auto">
<name>Task 1: Audit touch targets and fix all violations in App.css</name>
<files>frontend/src/App.css</files>
<action>
Read App.css in full. Identify all rules that style buttons and inputs. For each, check whether height or min-height is explicitly set to at least 44px.
Elements that need fixing (based on current code review):
1. `.back-btn` — currently has `padding: 0.5rem` only. Add:
```css
min-height: 44px;
```
2. `.warmup-item` — currently `padding: 0.75rem`. The item needs to be at least 44px tall. Add:
```css
min-height: 44px;
```
3. `.warmup-done-btn` — currently `padding: 1rem`. Add:
```css
min-height: 44px;
```
4. `.finish-workout-btn` — currently `padding: 1.25rem`. Add:
```css
min-height: 44px;
```
5. `.complete-btn` — already `width: 44px; height: 44px;`. No change needed. Verify it is not overridden in any mobile media query.
6. `.start-btn` and `.start-workout-btn` — currently `padding: 1rem`. Add `min-height: 44px;` to both (or the shared rule if they share one).
7. `.tab-btn` — currently `padding: 0.75rem`. Add `min-height: 44px;`.
8. `.calendar-nav` — currently `width: 32px; height: 32px;`. This is below 44px. Update to:
```css
width: 44px;
height: 44px;
```
9. `.edit-btn` — currently `padding: 0.5rem 0.75rem;`. Add `min-height: 44px;`.
10. `.cancel-btn` and `.save-btn` — currently `padding: 0.75rem`. Add `min-height: 44px;`.
Font-size audit — all `<input>` elements must have font-size >= 16px:
11. In `.auth-card input` (index.css line 96) the font-size is `1rem`. 1rem = 16px by default, but it depends on root font-size. To be safe, add a rule in App.css:
```css
/* Ensure all inputs have font-size >= 16px to prevent iOS auto-zoom */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
select,
textarea {
font-size: 16px;
}
```
Place this near the top of App.css in the first section, or append it at the end before the stepper block (if Plan 01 runs in parallel, this is fine — the stepper CSS block already has font-size: 16px on .stepper-input).
Approach:
- Edit each rule in-place by adding the missing property inside the existing rule block.
- Do NOT create new duplicate rule blocks — find the existing selector and add inside it.
- For the global input font-size rule, append it as a new block at the end.
After editing, confirm no interactive element visible on WorkoutPage or Dashboard is below 44px in height.
</action>
<verify>
1. grep -n "min-height: 44px" frontend/src/App.css (should appear multiple times)
2. grep -n "font-size: 16px" frontend/src/App.css (should appear for global input rule + stepper)
3. cd /workspace/gravl/frontend && npm run build 2>&1 | tail -10
</verify>
<done>
- All listed interactive elements have explicit min-height: 44px (or height: 44px for circle buttons)
- .calendar-nav updated from 32px to 44px
- Global input font-size: 16px rule added
- Build passes
</done>
</task>
</tasks>
<verification>
Build must pass. Visually: open Dashboard in browser, all buttons are comfortably tappable. Open WorkoutPage, warmup items and complete buttons are reachable with a thumb. No iOS zoom occurs when tapping any input.
</verification>
<success_criteria>
- Every interactive element in App.css has min-height >= 44px (or explicit height >= 44px)
- All input types have font-size: 16px preventing iOS auto-zoom
- .calendar-nav is 44x44px
- Build passes
</success_criteria>
<output>
After completion, create `.planning/phases/01-input-ux/01-03-SUMMARY.md` using the summary template.
</output>