Bladeren bron

HANDOFF.md: mark Phase 17 shipped

Move the Phase 17 entry from §9 Upcoming → Shipped with SHA
`b457896`. Reset §9 Upcoming to the "Nothing scheduled"
template. Add `public/assets/js/number-stepper.js` to the §3
`public/assets/js/` block. Append `0d738b2` (Phase 16 ship
marker, missed in that commit's own HANDOFF pass), `712bcc5`
(Phase 17 plan), and `b457896` (Phase 17 code) to the §13 git
history. Rewrite §12's resume prompt — Phases 1–17 shipped, no
scheduled work — mirroring the "post-Phase-16" template from
the `0d738b2` commit. Test count stays at 88 per §11; no new
§10 gaps introduced by Phase 17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa 2 weken geleden
bovenliggende
commit
735aa4f280
1 gewijzigde bestanden met toevoegingen van 74 en 197 verwijderingen
  1. 74 197
      HANDOFF.md

+ 74 - 197
HANDOFF.md

@@ -58,6 +58,7 @@ per-cell audit trail.
 │       └── js/
 │       └── js/
 │           ├── theme-init.js       # Phase 16: synchronous dark-class set from localStorage (no FOUC)
 │           ├── theme-init.js       # Phase 16: synchronous dark-class set from localStorage (no FOUC)
 │           ├── app.js              # site-wide; data-href click handler + hamburger menu + theme toggle
 │           ├── app.js              # site-wide; data-href click handler + hamburger menu + theme toggle
+│           ├── number-stepper.js   # Phase 17: custom popover stepper for [data-stepper] number inputs
 │           ├── sprint-planner.js   # /sprints/{id} Arbeitstage + task list
 │           ├── sprint-planner.js   # /sprints/{id} Arbeitstage + task list
 │           └── sprint-settings.js  # /sprints/{id}/settings
 │           └── sprint-settings.js  # /sprints/{id}/settings
 ├── src/
 ├── src/
@@ -420,196 +421,71 @@ with a `BOOTSTRAP_ADMIN` audit row.
       dark, admin-pages contrast sweep, private-window
       dark, admin-pages contrast sweep, private-window
       localStorage denial fallback).
       localStorage denial fallback).
 
 
+- [x] **Phase 17 — Hide native number spinners + custom 0.5-step
+      stepper popover** (`b457896`). Three classes of number input —
+      day cells, RTB cells, task assignment cells — deal in half-day
+      increments (or 0.05 for RTB). Browsers rendered each as
+      `<input type="number">` with tiny native up/down spinner
+      arrows: visually noisy in a dense table, inconsistent across
+      Chrome / Firefox / Safari, useless on touch. Hidden app-wide
+      via a two-rule `@layer base` block in `assets/css/input.css`
+      (`-webkit-*-spin-button { appearance: none; }` +
+      `-moz-appearance: textfield`). Week-count and reserve-percent
+      inputs lose arrows too — fine, keyboard typing is their usual
+      path. For the three opt-in cell types, a new
+      `public/assets/js/number-stepper.js` (~180 lines, single IIFE,
+      vanilla JS, no jQuery dep) delegates `click` + `focusin` on
+      `document` to `input[data-stepper]`; on match, lazily builds a
+      single `.stepper-popover` DOM node (role="dialog", `−` /
+      `<output>` / `+` / hidden `<input type="range">`) and anchors
+      it next to the bound input. Reads `step` / `min` / `max` off
+      the input (default step=1); when both min and max parse as
+      finite, un-hides the range slider and wires it; otherwise
+      `[data-assign]` gets just the +/− buttons. `clampToStep(current,
+      delta, step, min, max)` pure helper — adds delta, clamps to
+      [min,max] when finite, quantises via `Math.round(next/step) *
+      step` with a ~1e-9 epsilon tolerance so `0.6 + 0.05 = 0.65`
+      lands cleanly. Every mutation mirrors into `input.value` and
+      dispatches a bubbling synthetic `input` event, so
+      sprint-planner.js's existing recomputeRow / row-total handlers
+      fire live. On popover close (outside `pointerdown`, Escape,
+      Tab-away, or clicking a *different* stepper input) the helper
+      dispatches `change` — the existing debounced save pipeline
+      (PATCH `/sprints/{id}/week-cells`, `/workers/{sw_id}`, or
+      `/tasks/{id}/assignments`) fires once, same audit semantics as
+      typed edits. ArrowUp / ArrowDown while focused on the input
+      step by `step` (restores the shortcut the CSS reset just
+      disabled). Position: below the input with a 4px gap unless
+      the input sits in the lower 25% of the viewport, then above;
+      horizontal clamp to viewport with 4px margins. Outside-click
+      uses `pointerdown` (not `click`) so a Safari scroll gesture
+      starting inside the popover doesn't dismiss it. `data-stepper`
+      stamped on the admin-branch `[data-day]` / `[data-rtb]` /
+      `[data-assign]` inputs in `views/sprints/show.php`, the
+      `[data-rtb]` input in `views/sprints/settings.php`, the
+      `[data-assign]` input in `views/sprints/present.php`, and the
+      JS-built assignment cells in `sprint-planner.js::buildTaskRow`
+      — non-admin `<span>` branches stay clean. Popover styled in
+      `assets/css/input.css` under `@layer components` with
+      Tailwind `@apply` on the Phase 16 palette (bg-white /
+      dark:bg-slate-800 + slate-100/200/600/700 hover/border/text
+      siblings + accent-slate-600/400 on the range). Both
+      `views/layout.php` and `views/sprints/present.php` load the
+      new module via `<script src="/assets/js/number-stepper.js"
+      defer>` after sprint-planner.js — strict CSP stays intact, no
+      inline handlers, no new external hosts. Zero PHPUnit — pure
+      CSS + vanilla JS over existing markup, same pattern as Phases
+      10 / 13 / 14 / 15 / 16. Tests at 88 / 208. ACCEPTANCE.md
+      gains a "Phase 17 — Number stepper popover" section with the
+      five scenarios from the plan (no native arrows on any number
+      input, day-cell stepper at 0.5 step, RTB stepper at 0.05
+      step, task-assignment stepper on both show and present views
+      with no range slider because no max, Escape + outside-click +
+      dark-mode polish).
+
 ### Upcoming
 ### Upcoming
 
 
-- [ ] **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.
+Nothing scheduled.
 
 
 ## 10. Residual known gaps / deferred items
 ## 10. Residual known gaps / deferred items
 
 
@@ -675,15 +551,13 @@ vendor/bin/phpunit
 Tell Claude:
 Tell Claude:
 
 
 > Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
 > 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); 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.
+> Read `HANDOFF.md`, the git log, and `ACCEPTANCE.md`. Phases 1–17
+> 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.
 
 
 Claude should verify what's described here against actual repo state
 Claude should verify what's described here against actual repo state
 before acting — nothing here is load-bearing once it grows stale.
 before acting — nothing here is load-bearing once it grows stale.
@@ -691,6 +565,9 @@ before acting — nothing here is load-bearing once it grows stale.
 ## 13. Git history (as of this handoff)
 ## 13. Git history (as of this handoff)
 
 
 ```
 ```
+b457896 Phase 17: hide native number spinners + custom 0.5-step stepper popover
+712bcc5 HANDOFF.md: add Phase 17 plan (number-stepper popover + hide native spinners)
+0d738b2 HANDOFF.md: mark Phase 16 shipped
 94b2841 Phase 16: dark-mode toggle + light-mode contrast cleanup
 94b2841 Phase 16: dark-mode toggle + light-mode contrast cleanup
 0d7124a HANDOFF.md: add Phase 16 plan (dark-mode toggle + light-mode contrast)
 0d7124a HANDOFF.md: add Phase 16 plan (dark-mode toggle + light-mode contrast)
 d4738d7 HANDOFF.md: note buildTaskRow owner-dropdown hotfix on Phase 10
 d4738d7 HANDOFF.md: note buildTaskRow owner-dropdown hotfix on Phase 10