Honor UntilChanged in sync; let SearchSelect own its disabled look

Follow-ups on the add-form fixes:

- syncSelectInputUntilChanged now actually stops mirroring once the user edits
  the target (the "UntilChanged" contract). The old focus-based stop was a
  no-op (wrong removeEventListener reference), so live sync kept clobbering a
  manually-edited Sort name. Track dirty targets in a Set keyed by syncData
  index; programmatic writes don't fire "input", so only real user edits mark a
  target dirty. Drops the dead focus listener.

- SearchSelect now greys itself when disabled, via has-[:disabled]: utilities on
  its container class — the visible "box" is the wrapper <div>, so disabling the
  transparent inner input alone left it looking active. The component owns its
  disabled appearance; callers only toggle the inner control's `disabled`.

- Document the composite-widget disabling approach in CLAUDE.md and the
  SearchSelect docstring.

Extends the e2e tests: sync drops after a manual Sort name edit; disabled
related-game wrapper computes opacity 0.5 (and 1 when re-enabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 18:24:13 +02:00
parent 5bb8b92c05
commit 885e92b775
5 changed files with 76 additions and 39 deletions
+1
View File
@@ -189,5 +189,6 @@ Pytest settings are in `pyproject.toml` under `[tool.pytest.ini_options]` (`DJAN
- **Signals handle side-effects** — do not manually recalculate `Game.playtime` or `Purchase.num_purchases`; the signals in `games/signals.py` do this on save/delete.
- **Button colors**: `blue` (primary action), `red` (destructive), `gray` (secondary), `green` (positive). Icon buttons use `icon=True`.
- **Inline Alpine.js** is used for client-side reactivity in domain components (`GameStatusSelector`, `SessionDeviceSelector`). The pattern is `x-data="{...}"` with `fetchWithHtmxTriggers()` for PATCH API calls.
- **Disabling composite widgets**: a composite widget (e.g. `SearchSelect`) carries its `id` on a wrapper `<div>`, which has no `disabled` state — setting `.disabled` on it is a no-op. Disable the inner control (for `SearchSelect`, the `[data-search-select-search]` input); the **component owns its disabled *look*** via `has-[:disabled]:` utilities on its container class, so callers toggle only the control's `disabled`, never styles.
- **Platform icons** are SVG snippets in `games/templates/icons/<slug>.html`. Add new ones there and reference them by slug in `Platform.icon`.
- **Name compound types explicitly** — if a `tuple`, `dict`, or other compound value is passed between functions or appears in multiple signatures, give it a named type (`TypedDict`, `NamedTuple`, or a `type` alias) rather than repeating the structural annotation. This applies even to small types used in only a few places; the name carries intent that the structure cannot. Examples: `LabeledOption = tuple[str, str]` instead of repeating `tuple[str, str]` for (value, label) pairs; `RangeValues(min, max)` instead of `tuple[str, str]` for range bounds.