Ver Fonte

HANDOFF.md: add Phase 17 plan (number-stepper popover + hide native spinners)

Plan-only commit. Phase 17 in §9 Upcoming covers two things: an
app-wide CSS rule that hides the native `<input type="number">` up/down
spinner arrows, and a new vanilla-JS popover that opens on click or
focus of opt-in inputs to let the user step the value by the input's
own `step` attribute (0.5 for day / assignment cells, 0.05 for RTB).
The popover reuses one DOM node, shows +/- buttons plus a range slider
when both min and max are finite, and dispatches a synthetic `change`
on close so the existing debounced save in sprint-planner.js fires
without edits there. Dark-mode-aware per Phase 16. Stepped inputs are
scoped to `[data-day]` / `[data-rtb]` / `[data-assign]` — other number
inputs (week count, reserve percent) stay keyboard-only. §12 resume
prompt updated so a fresh session sees Phase 17 as scheduled.

No code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa há 2 semanas atrás
pai
commit
712bcc559b
1 ficheiros alterados com 196 adições e 7 exclusões
  1. 196 7
      HANDOFF.md

+ 196 - 7
HANDOFF.md

@@ -422,7 +422,194 @@ with a `BOOTSTRAP_ADMIN` audit row.
 
 ### Upcoming
 
-Nothing scheduled.
+- [ ] **Phase 17 — Hide native number spinners + custom 0.5-step stepper popover**
+
+      **Problem.** Three classes of number input in the app all deal in
+      **half-day increments**:
+        1. the Arbeitstage per-worker day cells on `/sprints/{id}`
+           (`[data-day]`, 0..5 step 0.5);
+        2. the task assignment cells on `/sprints/{id}` and
+           `/sprints/{id}/present` (`[data-assign]`, ≥ 0 step 0.5);
+        3. the per-worker RTB cells (`[data-rtb]`, 0..1 step 0.05).
+      Browsers render these as `<input type="number">` with native
+      up/down spinner arrows attached — the arrows are small, visually
+      noisy inside a dense table, and behave inconsistently across
+      Chrome / Firefox / Safari. On touch devices they often do
+      nothing. The team would rather see a clean number on the page
+      and get a tap-friendly stepper on click.
+
+      **Goal.** Hide every native spinner across the app, and for the
+      three half-step (or 0.05-step for RTB) cell types, show a small
+      **popover stepper** when the input is clicked/focused. The
+      popover has a large current-value readout, `−` / `+` buttons
+      that step by the input's own `step` attribute, and — when
+      `min` and `max` are both set — a range slider for quick gross
+      moves. Typing numerically still works; the popover is an
+      alternative input method, not a replacement.
+
+      **Scope (plan).**
+
+      1. **CSS — `assets/css/input.css`.**
+         Hide the native spinner on every `input[type="number"]`. Two
+         rules under `@layer base`:
+         ```css
+         input[type="number"]::-webkit-outer-spin-button,
+         input[type="number"]::-webkit-inner-spin-button {
+             -webkit-appearance: none;
+             margin: 0;
+         }
+         input[type="number"] {
+             -moz-appearance: textfield;
+             appearance: textfield;
+         }
+         ```
+         Applies app-wide — the week-count and reserve-percent inputs
+         on Settings / New-sprint also lose their arrows, which is
+         fine; they're edited rarely and keyboard typing is the usual
+         path.
+
+      2. **Opt-in attribute — `data-stepper`.**
+         Only the half-day cells get the popover. Stamp
+         `data-stepper` on:
+           - `[data-day]` in `views/sprints/show.php` (Arbeitstage
+             per-worker day cells; admin-editable).
+           - `[data-rtb]` in `views/sprints/show.php` and
+             `views/sprints/settings.php` (admin-editable).
+           - `[data-assign]` in `views/sprints/show.php` and the
+             JS-built row in `public/assets/js/sprint-planner.js`
+             (`buildTaskRow`); same data-stepper on the template.
+         Don't stamp it on `n_weeks` / `reserve_fraction` —
+         keyboard-only is right there.
+
+      3. **New JS module — `public/assets/js/number-stepper.js`.**
+         Vanilla JS, ≤ 120 lines, loaded with `defer` from
+         `views/layout.php` and `views/sprints/present.php` **after**
+         `sprint-planner.js`. Exposes no globals; single IIFE.
+         - On `document`, delegate `click` + `focus` on
+           `input[data-stepper]` → open the popover anchored to that
+           input. Only one popover open at a time; clicking another
+           stepper input moves it.
+         - Popover DOM (built once, reused):
+           ```
+           <div class="stepper-popover" hidden role="dialog"
+                aria-label="Set value">
+             <button data-stepper-dec aria-label="Decrease">−</button>
+             <output data-stepper-value>0</output>
+             <button data-stepper-inc aria-label="Increase">+</button>
+             <input type="range" data-stepper-range hidden>
+           </div>
+           ```
+           Styled with Tailwind utility classes in the template (or a
+           `@layer components` block in `input.css` so they compile).
+           Rounded card, shadow-lg, 1px border; `dark:bg-slate-800`
+           etc. for Phase 16 compatibility.
+         - Reads `min`, `max`, `step` from the bound input. When
+           both `min` and `max` are finite numbers, un-hide the
+           `<input type="range">` and wire it to set the value on
+           `input` event. Otherwise only the +/− buttons show.
+         - `+` / `−` mutate `input.value` via a small pure helper
+           `clampToStep(current, delta, step, min, max)` — step up
+           or down by exactly `step`, clamp into `[min, max]` when
+           defined, and quantise with
+           `Math.round(v / step) * step` (with a tiny float-epsilon
+           tolerance to avoid `0.30000000000000004` artefacts).
+         - After every value mutation, dispatch a synthetic `input`
+           event (for live recompute in sprint-planner.js) and —
+           only on popover close — dispatch `change` (to fire the
+           existing debounced save pipeline).
+         - Close on: outside-click, Escape (focus returns to the
+           input), Tab that leaves the popover + input, and a second
+           click on a *different* stepper input (auto-reopens
+           there).
+         - Position: below the input unless the input sits in the
+           bottom ~25% of the viewport, then above. Horizontal clamp
+           to viewport with a 4px margin.
+         - Keyboard while focused on the input: ArrowUp / ArrowDown
+           also step by `step` (replaces the native spinner's
+           behaviour that we just disabled). That way keyboard
+           users don't lose a shortcut.
+
+      4. **Integration with existing save logic.**
+         `sprint-planner.js` already listens for
+         `blur change` on `[data-day]` / `[data-rtb]` /
+         `[data-assign]`. The stepper dispatches `change` on
+         popover-close (and mirrors the value into `input.value`
+         before dispatching). No edits required in `sprint-planner.js`
+         except verifying that it reads `.value` every time the
+         handler fires — spot-check current code:
+         `recomputeRow()` and `queueCell()` both do `Number($el.val())`,
+         so we're fine.
+
+      5. **Dark mode.**
+         The popover uses Tailwind utility classes with `dark:`
+         siblings matching Phase 16 conventions
+         (`bg-white dark:bg-slate-800`, border `slate-200 /
+         slate-600`, text `slate-900 / slate-100`, hover `slate-100
+         / slate-700`). Range slider uses
+         `accent-slate-600 dark:accent-slate-400` so the Tailwind
+         `accent-color` picks up theme-appropriate thumb colour.
+
+      6. **Edge cases.**
+         - **Read-only cells (non-admin users).** Non-admin users
+           see `<span class="font-mono">` rather than an `<input>`
+           for these cells (see `show.php`). No stepper needed; the
+           attribute simply doesn't get stamped.
+         - **Firefox range-input support.** `<input type="range">`
+           with float `step=0.5` is well-supported; quantisation
+           already handled in `clampToStep()`.
+         - **Safari momentum scroll mid-popover.** Outside-click
+           handler uses `pointerdown` so a scroll gesture that
+           starts inside the popover doesn't immediately close it.
+         - **iOS tap-then-type.** Tapping a number input opens the
+           popover; the native keyboard does NOT auto-appear. To
+           type, focus the input and start typing — still works.
+         - **Batch paste of non-numeric text.** Input remains a
+           native `type="number"`, so browsers reject non-numeric
+           pastes as today.
+         - **Concurrent popover + jQuery UI drag.** The worker /
+           task row drag handlers (`handle` span) are separate from
+           number inputs — no conflict.
+
+      7. **Tests.** No PHPUnit — pure CSS + vanilla JS over existing
+         markup. Add a handful of cheap pure-JS unit assertions as
+         a *sanity matrix* only if `clampToStep` is extracted to
+         be testable without the DOM. Practically: skip, same
+         pattern as Phases 10 / 13 / 14 / 15 / 16.
+         ACCEPTANCE.md gains a
+         "Phase 17 — Number stepper popover" section with five
+         scenarios:
+           (a) no native spinner arrows visible on any number input;
+           (b) clicking a day cell pops the stepper; +/−
+               increments by 0.5; value persists after blur;
+           (c) RTB cell pops the stepper; +/− increments by 0.05;
+           (d) task assignment cell in both `/sprints/{id}` and
+               `/sprints/{id}/present` pops the stepper; no min
+               no range slider shown;
+           (e) Escape closes the popover and returns focus;
+               outside-click closes too; dark mode matches the
+               theme.
+
+      **Out of scope.**
+      - Touching non-half-step inputs (`n_weeks`, `reserve_fraction`)
+        beyond hiding the native arrows.
+      - Keyboard-shortcut stepping from anywhere on the page —
+        ArrowUp/Down is scoped to the focused input only.
+      - Replacing `<input type="number">` with a contenteditable
+        div or a third-party picker library.
+      - Undo/redo stack per cell.
+      - Long-press or drag-to-scroll on the buttons for fast
+        stepping. +/− buttons only step by one `step` per click.
+
+      **Spec alignment.**
+      - §3 layout: add `number-stepper.js` to the `public/assets/js/`
+        block.
+      - §4 / §5 / §7: no changes.
+      - §6 Routes: unchanged.
+      - §11 phpunit count: stays 88.
+      - Strict CSP (Phase 11) stays intact — standard `<script src>`,
+        no inline handlers.
+      - Dark mode (Phase 16): popover uses `dark:` variants
+        throughout.
 
 ## 10. Residual known gaps / deferred items
 
@@ -489,12 +676,14 @@ Tell Claude:
 
 > Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
 > Read `HANDOFF.md`, the git log, and `ACCEPTANCE.md`. Phases 1–16
-> are shipped (see §9) — nothing is currently scheduled. Outstanding
-> items are in §10 (mostly a human-run acceptance walkthrough in the
-> running container, plus the jQuery UI dark-mode cosmetic gap noted
-> there). If I ask you to plan or work a new phase, follow the
-> maintenance rule in §14 — commit code, then commit a HANDOFF.md
-> update separately that marks the new work shipped with its SHA.
+> are shipped (see §9); Phase 17 (hide native number spinners + add
+> a custom 0.5-step stepper popover on day / RTB / assignment cells)
+> is planned but not implemented — scope in §9 Upcoming. Other
+> outstanding items are in §10 (human-run acceptance walkthrough,
+> jQuery UI dark-mode cosmetic gap). If I ask you to work Phase 17,
+> follow the maintenance rule in §14 — commit code, then commit a
+> HANDOFF.md update separately that moves the entry from Upcoming →
+> Shipped with its SHA.
 
 Claude should verify what's described here against actual repo state
 before acting — nothing here is load-bearing once it grows stale.