Files
gravl/.planning/phases/01-input-ux/01-01-PLAN.md
T

9.3 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
01-input-ux 01 execute 1
frontend/src/components/StepperInput.jsx
frontend/src/components/WeightInput.jsx
frontend/src/components/RepsInput.jsx
frontend/src/App.css
true
truths artifacts key_links
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
path provides exports
frontend/src/components/StepperInput.jsx Reusable controlled stepper input component
default StepperInput
path provides exports
frontend/src/components/WeightInput.jsx Weight-specific wrapper (2.5kg steps, kg suffix)
default WeightInput
path provides exports
frontend/src/components/RepsInput.jsx Reps-specific wrapper (1 rep steps)
default RepsInput
path provides contains
frontend/src/App.css Stepper component styles .stepper-wrapper
from to via pattern
frontend/src/components/WeightInput.jsx frontend/src/components/StepperInput.jsx import StepperInput import StepperInput
from to via pattern
frontend/src/components/RepsInput.jsx frontend/src/components/StepperInput.jsx import StepperInput 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.

<execution_context> @/home/intense/.claude/get-shit-done/workflows/execute-plan.md @/home/intense/.claude/get-shit-done/templates/summary.md </execution_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 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:

<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. 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:

/* ============================================
   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.

<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>
After completion, create `.planning/phases/01-input-ux/01-01-SUMMARY.md` using the summary template.