Buttons that rest on surfaceElevated used surfaceHover for their hover
fill: a 6% white wash meant for rows that rest transparent. Over an
elevated button it lands DARKER than the rest fill — on a modal's dark
ground the Cancel/Save/footer buttons went near-black under the pointer.
QbzTheme gains surfaceElevatedHover (a lift of surfaceElevated, derived
so every palette has it), its ambient half-alpha twin, and the
elevatedFill / elevatedHoverFill pair an ambient-aware control paints.
Every site with the old pairing (65 across modals, toolbars, selects,
circle actions, chips and cards) now hovers to the lift.
cc
Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus
22 shared opt-in hosts, shared navigation history with Back/Forward, Albums
as the default library tab, native paged Local Library (artists/tracks),
lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and
artwork sized to the physical frame, static skeletons, 64px transport bar,
Settings touch reflow and a persistent Immersive exit.
Desktop keeps its prior behavior: every shared component defaults to the
desktop path and only opts into Kiosk behavior through explicit properties
(origin-relative ScrollMemory, viewport-bounded artwork requests).
Regression coverage: kiosk navigation and artwork Node tests added to the
runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n
strings for all eight locales.
Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and
1280x800 and a Desktop A/B with seven pixel-identical captures.
cc
The port of the block Slint repeats in 27 views (NavState.restore-scope /
scroll-restore / report-scroll). A .slint file cannot hand a component a
reference to a Flickable, which is why the reference inlines it; QML can, so
controls/ScrollMemory.qml exists once and 31 containers mount it.
Two divergences from the reference, both forced by the same fact — tabs are not
history entries in this port:
1. THE SCOPE IS NAMED BY THE VIEW, not derived from the entry by a scope_for
table. Slint can derive it because its entries carry the tab
(Favorites { tab }); here an entry is a bare route id, so a derived scope
would read "library" for all five Library tabs and pour the Albums grid's
offset into whatever tab mounted. Letting the reporter name its own scope
buys tab granularity for free, and the mismatch case degrades the right way:
an unmatched scope restores NOTHING instead of restoring the wrong thing.
2. THE RESTORE RE-ASSERTS instead of firing once. A Qt item view resets
contentY to 0 whenever its model is replaced, and several pages assign their
model from a binding whose inputs arrive over the network after the mount —
the album page rebuilds listCells from three deferred rails, so it hands its
ListView a new array up to four times in its first seconds and snaps back to
the top over a restore that already ran correctly. The offset is held and
re-applied until a 2.5 s deadline, or until the user takes over (a wheel, a
drag or a flick raises `moving`, and from then the position is theirs).
Diagnostics are off by default: QT_LOGGING_RULES="qbz.nav.scroll.info=true" for
the QML half, RUST_LOG=qbz=debug for the stamp/arm half.
cc
Accumulated qt-frontend work from the 2026-08-11/12 sessions (cc),
uncommitted until now: the app-wide dynamic background in both modes
(AmbientField with GPU/Canvas arms, AppShell layering, ambient palette
pipeline), the wave-1 clip/batch sweep across the tree (alpha batches
449 -> 327, reproducible), the rounded-cover and icon-tint arms, and
scripts/qt-batches.sh (batches + presents/s sampler).
The GPU behaviour of these pieces was finalized in the follow-up pulse
commits; see qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/
GPU-COST-INVESTIGATION.md for the full campaign.
km
The port had a 67-line settings row showing two counters and a header saying
the Manage chevron was "NOT shipped — this port has no Blacklist Manager view
for it to open". The Slint has 1,114 lines of manager plus the consumer surfaces
that make the feature mean anything.
- src/artist_blacklist.rs is a 1:1 port of the reference's per-user lifecycle,
including the fail-open asymmetry (is_enabled reads TRUE when there is no
session, everything else false/empty) and the byte-identical NO_SESSION_ERR.
src/reco_dismiss_qt.rs carries the reco-dismissal store. Both are bound in
ALL THREE session-activation paths — the offline one included, because the
reference records that Tauri never bound it offline and blacklisted artists
leaked into offline surfaces — and torn down on logout, or the next account
inherits them.
- The manager view: three tabs (bordered, ADR-008 — no pills), search, sort,
the enable toggle, the disabled warning banner, the four body branches per
tab in their load-bearing order, windowed lists, and the clear-all confirm.
Reached from Settings only, exactly like the Slint; no sidebar entry.
- Every one of the ten store mutations publishes TWICE — its own document and
publish_settings() — because the Settings counters live in a different
document and nav_qt::back() re-runs no per-view load. Without it,
Manage -> unblock -> Back showed pre-mutation counters. The Slint gets this
free from a shared global.
- The search field is driven imperatively, not by a binding: QbzLineEdit's X
used to destroy the binding, after which the box went blank while the list
stayed filtered.
- Consumer surfaces, which is where the feature actually lives: the album card
and album list row gained "Block this album", the artist card gained the
3-arg "Not interested", the album header menu gained Block/Unblock swapping
on state, and the artist page gained the Show/Blacklist flip plus the amber
hidden banner with its "Show artist" button. album_qt and artist_qt now seed
isAlbumBlocked / isBlacklisted at document build time — without the seed an
already-blocked album offered "Block this album" and the first click
UN-blocked it, the same lie the pin glyph told before isPinned existed.
- search_qt passed &Default::default() for both blacklist snapshots, so nothing
was filtered anywhere and a greyed row still played. It now passes the real
snapshots at both call sites.
- The 17 toasts the spec had dropped for want of a toast host are wired: that
premise stopped being true when toast_qt landed, and a Clear All that failed
with NO_SESSION_ERR was completely silent. Their msgids are in all eight
catalogues; the five album-axis ones existed only in es and nl.
Also fixed in passing, pre-existing on this branch and unrelated to the
blacklist: ArtistView's library tab read `libTracks` unqualified from a child
of the object that declares it, which QML resolves against the component root,
not the enclosing object — so the "Tracks" heading threw a ReferenceError and
never rendered. Found by forcing the artist route in the offscreen smoke, which
no earlier gate did.
cc