docs(01-input-ux): create phase plan
This commit is contained in:
@@ -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: <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.
|
||||
</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>
|
||||
Reference in New Issue
Block a user