fix(ui): user-test round 4 — five browser-reported control defects

Every one of these was silent at the code level and loud in a real
browser; each fix carries its own tripwire (render pin, unit pin, or
CSP-safe rewrite) so the class cannot quietly return.

1. Theme picker dead since the CSP migration (eb57b720): the Appearance
   panel's theme buttons emitted `@click="selectTheme(opt.Name)"` as
   LITERAL text (templ gotcha #1 — quoted attribute values are never
   interpolated; `opt` is a Go loop var), so Alpine threw "Undefined
   variable: opt" on every click and no PUT /api/auth/theme ever fired.
   The wood-paneling sibling always used the correct expression form,
   which is why the panel half-worked. Fixed to the interpolated form;
   TestThemePickerHandlersAreInterpolated pins that real theme names
   reach the HTML and the raw identifier never does.

2. Filter-builder op dropdown "does nothing": the field ingest
   hardcoded `ops: []` into every field's meta while only mirroring the
   ops into the global label registry — opsFor() returned [] for EVERY
   field, so the op menu rendered zero items: an invisible menu, no
   console error (user tried author/publisher/title/subtitle, all
   equally dead). FieldMeta.ops is now the wire's op objects, opsFor()
   is a standalone exported helper, and opsForField unit tests pin the
   round-trip. Verified live: the field menu opens/closes while the op
   menu stayed shut, both with identical wiring — the difference was
   the data, not the wiring.

3. AND/OR connective chip dead: the chip carried an `if` STATEMENT in
   the Alpine attribute (`@click="if (row.chipClickable) ..."`) which
   the CSP build's evaluator rejects as a statement — the whole
   expression silently never ran. Moved the clickability check into a
   toggleConnective(row) component method; the attribute is now a plain
   method call.

4. Pencil vs card navigation race: navigateToCollection guarded with
   `event.target instanceof HTMLElement`, which is FALSE for the
   pencil glyph's SVGElement — a click landing on the glyph sailed past
   the guard and navigated while the htmx edit-modal request still
   fired (the reported "modal flashes, then the builder page"). The
   guard now uses target.closest("button") (works on SVGElement) and
   takes $event explicitly instead of the deprecated window.event.
   .icon-btn is already a 36px square, so no hit-area change needed.

5. Add Items modal under the sidebar + taller than the window: the
   picker overlay sat at z-50 UNDER the app-sidebar's z-60 (its left
   edge rendered beneath the nav), and the card had no height bound —
   header + tabs + filters + grid + footer overflow short windows with
   the footer unreachable (fixed overlay, no page scroll). The picker
   and create-modal overlays move to z-[65], both confirm dialogs to
   z-[70], and the picker card is now a flex column capped at
   max-h-[85vh] whose grids flex-fill (they grow on tall windows,
   shrink on short ones). The ladder is documented at .app-sidebar in
   input.css: sidebar 60 / overlays 65 / confirm 70 / toasts 9999.

Found by clicking every control the way a user does (panel clicks +
computed-style reads + screenshots) after the previous sweep trusted
DOM-text snapshots, which show hidden menus' text and cannot see
occlusion, stacking, or geometry at all.
This commit is contained in:
John O'Keefe committed 2026-10-31 13:14:05 -04:00
1 parent 8aec80cbb7
commit d5dbaa48ea
15 files changed
+299 -177

No files matched your search

+2 -2
View File
@@ -146,7 +146,7 @@ templ Header(user User, currentPath string) {
for _, opt := range ThemeOptions {
<button
type="button"
@click="selectTheme(opt.Name)"
@click={ "selectTheme('" + opt.Name + "')" }
class="theme-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
x-show={ "themeMode === '" + string(opt.Mode) + "'" }
data-theme={ opt.Name }
@@ -238,7 +238,7 @@ templ Header(user User, currentPath string) {
native window.confirm; fixed 2026-10-30, user report). -->
<div
id="confirm-modal"
class="hidden fixed inset-0 z-[60] flex items-center justify-center p-4"
class="hidden fixed inset-0 z-[70] flex items-center justify-center p-4"
style="background-color: var(--surface-overlay);"
>
<div class="card w-full max-w-sm" style="box-shadow: var(--shadow-pop);">