--- 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" --- 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. @/home/intense/.claude/get-shit-done/workflows/execute-plan.md @/home/intense/.claude/get-shit-done/templates/summary.md @.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 Task 1: Create StepperInput.jsx frontend/src/components/StepperInput.jsx 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: ```
{suffix && {suffix}}
``` 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.
File exists at frontend/src/components/StepperInput.jsx with exported default function. Check: grep -n "export default" frontend/src/components/StepperInput.jsx StepperInput.jsx exists, exports default, contains handleDecrement, handleIncrement, handleInputChange logic with min clamping.
Task 2: Create WeightInput.jsx and RepsInput.jsx, add stepper CSS to App.css frontend/src/components/WeightInput.jsx frontend/src/components/RepsInput.jsx frontend/src/App.css Create frontend/src/components/WeightInput.jsx: - Imports StepperInput from './StepperInput' - Renders: <StepperInput value={value} onChange={onChange} step={2.5} min={0} max={null} label="Weight" suffix="kg" disabled={disabled} /> - Props: value, onChange, disabled (default false) - Export default WeightInput Create frontend/src/components/RepsInput.jsx: - Imports StepperInput from './StepperInput' - Renders: <StepperInput value={value} onChange={onChange} step={1} min={0} max={null} label="Reps" suffix="" disabled={disabled} /> - 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. 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 WeightInput.jsx and RepsInput.jsx exist and export defaults. App.css contains .stepper-wrapper block. No existing CSS was removed.
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. - 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 After completion, create `.planning/phases/01-input-ux/01-01-SUMMARY.md` using the summary template.