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:
1 parent
8aec80cbb7
commit
d5dbaa48ea
15 files changed
+299
-177
No files matched your search
+11
-11
@@ -299,7 +299,7 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
</div>
|
||||
}
|
||||
for _, col := range collections {
|
||||
<div @click="navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block">
|
||||
<div @click="navigateToCollection($el, $event)" data-href={ "/collections/" + col.ID } class="block">
|
||||
<div
|
||||
class="card p-6 rounded-2xl border-l-4 cursor-pointer"
|
||||
data-color={ col.Color }
|
||||
@@ -438,7 +438,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
x-show="$store.bookPicker.isOpen"
|
||||
@keyup.escape.window="$store.bookPicker.close()"
|
||||
@click.self="$store.bookPicker.close()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
class="fixed inset-0 z-[65] flex items-center justify-center p-4"
|
||||
style="display: none; background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
@@ -450,11 +450,11 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="card rounded-2xl w-full max-w-6xl mx-4 my-8"
|
||||
class="card rounded-2xl w-full max-w-6xl flex flex-col max-h-[85vh]"
|
||||
style="display: none; box-shadow: var(--shadow-pop);"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center p-6 border-b"
|
||||
class="flex justify-between items-center p-6 border-b shrink-0"
|
||||
style="border-color: var(--border);"
|
||||
>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">
|
||||
@@ -472,7 +472,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
episodes tab is YOUR subscriptions' episodes (RLS scopes).
|
||||
Pure client-local state — the tab click triggers the
|
||||
hidden grid's htmx load. -->
|
||||
<div class="flex gap-2 px-6 pt-4">
|
||||
<div class="flex gap-2 px-6 pt-4 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
@click="$store.bookPicker.switchScope('books')"
|
||||
@@ -489,7 +489,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 border-b"
|
||||
class="p-4 border-b shrink-0"
|
||||
style="border-color: var(--border);"
|
||||
x-show="$store.bookPicker.scope !== 'episodes'"
|
||||
>
|
||||
@@ -543,7 +543,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</div>
|
||||
<div
|
||||
id="book-picker-grid"
|
||||
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto"
|
||||
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 flex-1 min-h-0 overflow-y-auto"
|
||||
hx-get={ "/api/media-items/search?show_checkbox=true&collection_id=" + collection.ID }
|
||||
hx-trigger="loadBooks"
|
||||
hx-include="#book-picker-filters"
|
||||
@@ -553,7 +553,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
episode list is feed-ordered, facets make no sense here.
|
||||
RLS scopes to YOUR subscriptions server-side. -->
|
||||
<div
|
||||
class="p-4 border-b"
|
||||
class="p-4 border-b shrink-0"
|
||||
style="border-color: var(--border);"
|
||||
x-show="$store.bookPicker.scope === 'episodes'"
|
||||
x-cloak
|
||||
@@ -575,7 +575,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</div>
|
||||
<div
|
||||
id="episode-picker-grid"
|
||||
class="flex flex-col gap-2 p-4 max-h-96 overflow-y-auto"
|
||||
class="flex flex-col gap-2 p-4 flex-1 min-h-0 overflow-y-auto"
|
||||
x-show="$store.bookPicker.scope === 'episodes'"
|
||||
x-cloak
|
||||
hx-get={ "/api/media-items/search?show_checkbox=true&scope=episodes&collection_id=" + collection.ID }
|
||||
@@ -584,7 +584,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 border-t flex justify-between items-center"
|
||||
class="p-4 border-t flex justify-between items-center shrink-0"
|
||||
style="border-color: var(--border);"
|
||||
>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">
|
||||
@@ -611,7 +611,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
<div
|
||||
x-show="showConfirm"
|
||||
@click.self="closeConfirm()"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
||||
class="fixed inset-0 z-[70] flex items-center justify-center p-4"
|
||||
style="display: none; background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
|
||||
Reference in new issue
Block a user