You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Three user-reported UI items.
## 1. The scan-phase progress bar looked finished
**Diagnosis first, because the fix is smaller than the report
suggests:** the
indeterminate sweep was already on `main` and works. The only render
path that
produces "a solid teal line that looks like a finished 100% bar" is the
`prefers-reduced-motion: reduce` fallback, which set `width: 100%;
opacity: 0.6`
on the sweep segment - a full-width teal line, visually identical to a
completed
determinate fill. Windows' "Show animations in Windows" toggle maps
straight to
that media query in WebView2, so anyone with OS animations off saw a
"finished-looking" bar for the entire scan.
**Confirmed empirically, not just reasoned about.** Driving the real
component in
headless Chrome (CDP, seeding a `scanning` state into the live pinia
store) and
measuring the segment against the track:
| CSS | `prefers-reduced-motion` | segment width | animation |
| --- | --- | --- | --- |
| old (`main`) | `no-preference` | 40% of track | sweep, 1.3s |
| old (`main`) | `reduce` | **100% of track** | `none` |
| new | `no-preference` | 40% of track | sweep, 1.2s |
| new | `reduce` | 40% of track | opacity pulse, 2.4s |
The 100% row is the bug, reproduced exactly. (Also rules out the
alternatives: a
failed animation would leave the segment static at the LEFT 40%, and a
broken
`w-2/5` would give zero width. Neither matches the report.)
The fallback now keeps the segment **partial-width** - so it can never
read as
100% - and swaps the travelling sweep for a slow, gentle opacity pulse
that still
says "working" without moving anything across the screen.
The default sweep is also retuned: 1.2s loop, `will-change: transform`
(it was
already transform-only, so the compositor can run it off the main thread
while
the scan hammers the disk), and rounded ends so it reads as a travelling
pill
rather than a fill edge. The determinate upload bar is untouched.
Both paths use the existing `bg-teal-600 dark:bg-teal-400` pair on the
`bg-teal-100 dark:bg-teal-900/40` track, so light and dark are
unchanged.
**Testability note:** only the template half of "never renders as a full
bar" is
unit-testable - jsdom does not apply scoped CSS, so the reduced-motion
branch
cannot be asserted in vitest (hence the browser measurement above). The
new test
covers what is observable: the sweep is a partial-width element carrying
no
inline width, and the determinate fill (the only branch with a width) is
absent.
## 2. Navigation cleanup
- **Restore now comes before Settings** in the top nav. Restore is a
primary
user-facing surface; Settings is the configuration drawer and reads
last.
- **About left the top nav** and is now the fourth tab in the Settings
sub-bar,
alongside Accounts / Sources / Rules. It is a configuration surface, so
that is
where it belongs. `Settings.vue` embeds the existing `About.vue` (moved,
not
duplicated), and its headings are demoted a level (h1 -> h2, inner h2 ->
h3) so
the tab content nests correctly under the Settings h1 and matches the
Rules
tab's section-heading pattern.
**On deep links: `/about` is unchanged rather than redirected.** The
repo's
convention is flat per-tab paths - `/accounts`, `/sources` and `/rules`
all
render `Settings.vue` with a `tab` prop, and there is no nested
`/settings/*`
anywhere - so `/about` now resolves to `Settings.vue` with `props: {
tab:
"about" }`. That satisfies the deep-link requirement better than a
redirect
would, because the URL never changes and there is no new path to
redirect to.
Deep-link surfaces actually checked, not assumed:
- `src-tauri/src/tray.rs` only ever emits `tray:navigate` for
`settings`/`activity`/`restore`, and no frontend listener for that event
exists at all - nothing on the Rust side navigates to About.
- `router-first-run.test.ts` asserts `firstRunTarget("/about")` is null;
the
first-run guard only special-cases `/` and `/activity`, so that holds.
- `nav.about` is now dead and removed from `en-US.json`; the tab reuses
the
already-present `settings.tabs.about`.
## 3. Sticky shell chrome
The progress bar, paused banner and nav are now ONE sticky block
(`sticky top-0 z-30`) instead of three normal-flow siblings, so none of
them can
be scrolled out of the way on a long view - a 10k-row Activity list used
to hide
the running-backup bar entirely.
**Approach chosen: sticky header, NOT an inner scroll container.** The
other
option - making `<main>` the scroller with `overflow-y-auto` - would
break the
Restore browser: `useVirtualList` windows off WINDOW scroll and
`window.innerHeight`, so moving the scroll into an inner element would
stop its
scroll events firing and freeze the list on its first window. Sticky
also keeps
its space in normal flow, so no compensating top padding is needed and
the
layout at scroll 0 is byte-identical to before (no layout shift).
`z-30` clears page content and Restore's existing `sticky bottom-0 z-10`
action
bar while staying under the `z-50` modal overlays. The header carries an
explicit
`bg-zinc-50 dark:bg-zinc-950` because `PausedBanner`'s dark fill is
semi-transparent (`bg-amber-950/40`) and would otherwise let scrolled
content
show through.
Measured in headless Chrome at `scrollY=900`, both color schemes: header
`position: sticky`, `top: 0px`, `z-index: 30`,
`getBoundingClientRect().top === 0`
(pinned), background resolving to zinc-50 / zinc-950 per scheme,
**zero** inner
scroll containers (no double scrollbar) and identical document scroll
height in
both schemes.
## Tests
`ui/`: 442 passed (40 files), `prettier --check src` clean, `eslint` 0
errors
(17 pre-existing unused-key warnings), `vue-tsc --noEmit` clean. New
coverage:
the Settings About-tab branch (offered in the sub-bar, navigates to
`/about`,
renders the About surface and not the Rules form), the nav order and the
absent
top-nav About link, Settings staying lit on `/about`, the partial-width
scan
sweep, and the sticky header (all three chrome pieces inside it, no
inner
scroller, no compensating padding).
Browser verification above used the vite dev server, so the Rust backend
was not
running - the progress bar was driven by seeding its store directly. The
determinate upload bar still has no end-to-end check, since that needs a
real
backup run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JLB3E2Jm7knNJd37fVpH8X
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0 commit comments