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
+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>