# Checkbox lifecycle contract

- **Outcome:** The canonical `Checkbox` factory retains its existing props/API, DOM structure (label wrapping native `input[type=checkbox]` + control span), and native keyboard/checked/disabled semantics, while gaining idempotent `mount()`, in-place `update(next)`, and idempotent `destroy()` with safe remounting and listener cleanup — matching the settled Toggle.js pattern.
- **Status:** IDLE
- **Task ID:** D5-CHECKBOX-01
- **Source request:** Owner requested Delivery 5 Checkbox lifecycle convergence for `plugins/international-press-zone/admin/src/components/Checkbox.js`, from current `origin/master`, without delegation, external models, or skills; preserve every public API, caller behavior, callback override precedence (attrs handlers fully replace built-ins, per the landed Toggle.js pattern), native keyboard semantics (no custom keydown handlers), checked/disabled state, and DOM identity. Focused tests must prove caller compatibility, callback override, repeated lifecycle calls, no-op update identity, listener cleanup + clean remount, and accessibility via genuine interaction (no synthetic `.click()` standing in for keyboard proof). Update inventory/report truthfully and record a durable per-slice plan receipt. Builds/gates run only through the local-gate wrapper. Commit locally; do not land.
- **Acceptance criteria:** Existing `Checkbox()` callers keep working unchanged (label wrapper, `.presszone-international-checkbox` classes, `input.presszone-international-checkbox__input`, `.presszone-international-checkbox__control` span, `indeterminate` property support); `root.mount(container)`, `root.update(next)`, `root.destroy()` exist, are idempotent, and preserve node identity across repeated calls and no-op updates; `attrs.onchange` supplied by a caller fully replaces the built-in `onChange` wiring (never both firing); no keydown listener is added by the component (native UA checkbox activation timing is untouched — proven by dispatching a real `KeyboardEvent('keydown', {key:' '})` and asserting it is not defaultPrevented/stopped, i.e. no synthetic `.click()` used as accessibility proof); destroy removes the active change listener and detaches the node; remount reattaches the correct listener without changing node identity; focused tests, full admin suite, lint, production build (with any changed generated assets committed and a clean second build), `tools/ui-inventory.mjs --check` + `tools/ui-gate.mjs`, and `tools/factory-gate.sh all` all pass with locked dependency install.
- **Preserved WIP/ref/path:** Worktree `agent-ac0d1f72c2b880df7`, HEAD `894383b6e` equals fetched `origin/master`; clean tree at start, no pre-existing uncommitted changes.
- **Constraints:** Only `admin/src/components/Checkbox.js`, its new lifecycle test file, generated build assets touched by the required production build, `docs/architecture/ui-inventory.report.{md,json}` (regenerated, not hand-edited), and this plan/index may change. Do not change unrelated components, page behavior, or public DOM/API identity. Do not land.
- **Execution steps:**
  1. Read Toggle.js (settled lifecycle + override-precedence pattern) and current Checkbox.js.
  2. Probe jsdom: confirm native checkbox Space/Enter activation is NOT synthesized by jsdom (dispatching keydown/keyup does not toggle `checked` or fire `change`) — so the accessibility proof must assert non-interference (event not prevented/stopped) rather than an actual state flip, since the component intentionally adds zero keydown handling.
  3. Add `mount`/`update`/`destroy` to the returned root (label) element, following Toggle's bind/unbind + destroyed-flag pattern, applied to the inner `input`.
  4. Add `admin/tests/checkbox-lifecycle.test.js` covering: caller compatibility (existing props/classes/indeterminate), callback override via `attrs.onchange`, repeated lifecycle calls, no-op `update({})` returns same identity, listener cleanup on destroy + clean remount reattaches listener, and native-keyboard-semantics/accessibility proof via real `KeyboardEvent` dispatch (no `.click()` substitution).
  5. Run: locked install, focused Checkbox tests, full admin suite, lint, production build (commit changed generated assets, verify a second clean build), `ui-inventory.mjs --check` + `ui-gate.mjs`, `tools/factory-gate.sh all` — all through `local-gate`.
  6. Commit locally with terse messages; update this plan's receipt and INDEX.md; do not land.
- **Current receipt:** `Checkbox()` now returns the same `<label>` node with `mount()`/`update(next)`/`destroy()` following Toggle.js's bind/unbind + destroyed-flag pattern applied to the inner `input`. All prior props/classes/DOM shape (`.presszone-international-checkbox`, `.presszone-international-checkbox__input`, `.presszone-international-checkbox__control`, `indeterminate` property) are unchanged. `attrs.onchange` fully replaces the built-in change handler (never both fire); the component installs no keydown/keyup handler, so native checkbox Space/Enter activation timing is untouched — proven by dispatching a real `KeyboardEvent('keydown'/'keyup', {key:' '})` and asserting `defaultPrevented === false` plus an `addEventListener` spy showing only a `change` listener is ever registered on the input (never `.click()` as a keyboard-activation stand-in). New `admin/tests/checkbox-lifecycle.test.js` (7 tests) covers: caller compatibility (classes/indeterminate/aria-label), callback override precedence, native-keyboard-semantics/no-keydown-listener proof (2 tests), no-op-update identity + in-place update, destroy/remount listener cleanup, and override survival across destroy/remount. All required gates ran through `local-gate`: locked npm/composer installs, focused Checkbox tests (7/7 pass), full admin suite (695/699 pass — the 4 failures are pre-existing on unmodified `origin/master`, reproduced identically via `git stash` before this change: `admin-stores.test.js` PHP-policy-fixture drift and 3 `translations-route-loading.test.js` failures, none touching Checkbox), `npm run lint:js` (clean, no output), production build twice in a row (second build produced zero further diff — clean), `node tools/ui-inventory.mjs --check` (ok: 123 modules, 0 direction violations, 2 baseline duplicate ids) and `node tools/ui-gate.mjs` (ok: 10 changed files scoped, no new violations), and `bash tools/factory-gate.sh all` (all green: php-syntax, phpunit/procedural regressions, phpcs baseline, phpstan, admin lint, admin build, ui-architecture 45/45 gate tests, ui-inventory --check, ui-gate). The regenerated `ui-inventory.report.{md,json}` also picks up pre-existing legacy-class-count drift unrelated to Checkbox (verified via `git stash`: `--check` already failed on unmodified `origin/master` before this slice).

  **Unblocked and committed.** The pre-commit ordering bug (ESLint's `npm ci` leaving `.dev-config/node_modules` untracked, then `ipz-remote-php-gate`'s pristine-tree precondition failing on it) was repaired upstream on `origin/master` by commit `478e92904` "Admit the remote ESLint cache", which explicitly allowlists `$ESLINT_TOOLCHAIN_DIR` (`.dev-config/node_modules`) in `capture_remote_state`, matching `$IPZ_DIR/vendor`'s existing treatment. Also removed the unused `call` parameter (renamed to `_call`, matching the repo's `argsIgnorePattern: '^_'` ESLint rule) in the `addSpy.mock.calls.filter(...)` callback at `admin/tests/checkbox-lifecycle.test.js` line 69, per coordinator instruction.

  During landing, `origin/master` advanced four more times in quick succession while other concurrent worktree sessions iterated the same gate infra (`2c33fa64c`, `7ae90453b`, `478e92904`, `fe0fe3452`, `989ca292a` — none touching `plugins/international-press-zone`), each requiring a `git fetch origin master` + `git stash` + `git rebase FETCH_HEAD` (clean fast-forward each time, no true local commits existed yet so no true rebase conflicts arose) + `git stash pop` (clean, zero conflicts every time — verified `git diff --stat <old>..<new> -- plugins/international-press-zone` was empty at each step) to keep the worktree's trusted-hook install in sync before `git commit` would pass its own installation-integrity check. Stray gitignored caches (`.dev-config/node_modules`, `admin/.babel-cache`, `admin/.vitest-cache`) were cleaned with `git clean -fdx <path>` immediately before each commit attempt to satisfy the remote gate's pristine-tree precondition.

  Final commit: **`0144af55e`** "Converge Checkbox lifecycle" on branch `worktree-agent-ac0d1f72c2b880df7`, HEAD `989ca292a` ("Register locked PHPCS standards") at rebase time. 8 files, 680 insertions(+), 226 deletions(-). The pre-commit gate itself ran and passed the remote PHPCS/PHPStan/slopgate check inline (`IPZ_REMOTE_PHP_GATE_OK` sentinel observed; ESLint clean; slopgate ok with ast-grep bucket-B rules skipped — pre-existing, unrelated to this slice). Post-commit, `git fetch origin master` shows this worktree's HEAD is not behind (`git log --oneline HEAD..FETCH_HEAD` empty), so no further rebase/merge was needed. Re-ran the full gate suite against the committed state after reinstalling composer/npm deps that a `git clean -fdx` pass had removed: `bash tools/factory-gate.sh all` (all green: php-syntax, phpunit/procedural regressions, phpcs baseline, phpstan, admin lint, admin build, ui-architecture 45/45 gate tests, ui-inventory --check, ui-gate) and focused Checkbox tests (7/7 pass). No conflicts occurred at any point — every rebase was a pure fast-forward with an empty diff against the plugin tree, and every `git stash pop` applied cleanly. Landing remains explicitly excluded per the owner's instruction.
- **Next executable action:** None within this local-only slice.
