Commit Graph
532 Commits
Author SHA1 Message Date
peter eda6ef4766 improvement(feed): give the compact row's title the room back
Three from UAT: the row's cap goes 1280 -> 1472 (+15%), since with the
rail and filters hidden there was a lot of page left over and the fixed
columns mean the extra width can only go to the title; the views column
drops the word and keeps it in the tooltip (a table doesn't repeat its
unit every line), taking that column 144 -> 64; and the view switcher
names the current mode where the toolbar has room, falling back to the
icon alone below xl. Title: 431 -> 511 at 1440, 706 at 1900.
2026-07-17 01:58:14 +02:00
peter ed08aa87f0 fix(feed): stop the compact row eating its own title on a smaller window
Every column but the title is shrink-0, so once the fixed widths outgrow
the row the title gives all of it: measured clientWidth 0 on every row at
a 1000px window — a table of channel, duration, views and date with no
titles. Columns now drop from the right as space runs out, least
important first, in the order the user named: title, then channel, then
the rest. Measured at 1000px: title 0 -> 351.

I'd written this off as "E15's problem, bites below ~640px" — wrong by
600px, because I only ever measured at 1440.
2026-07-17 00:55:46 +02:00
peter 638aa5750e fix(feed): only tooltip the channel name where it's actually cut off
I set the attribute unconditionally because the one-line row truncates
hard, and never looked at the other four views — where the name fits and
the tooltip just repeated what was already on screen. It now shares the
title's clamped check.
2026-07-17 00:50:40 +02:00
peter 30804dc7dd fix(feed): name the row's status markers once, and reliably
Dropping the sr-only twin last round swapped a double announcement for
none: a bare span's title isn't dependably surfaced, so a screen reader
got no hint a row was live, a stream, or saved. role="img" + aria-label
names each marker once and keeps the hover.
2026-07-17 00:47:40 +02:00
peter e52fba8b07 fix(feed): don't tell people to click an indicator that isn't a button
The saved pip borrowed the bookmark button's label, so it read "Saved —
click to remove" on a plain span: hover, be told to click, click, nothing.
It gets its own "Saved" string. The status markers drop their sr-only
twin too — with a title already carrying the meaning, a screen reader
that surfaces title text said it twice.
2026-07-17 00:45:06 +02:00
peter 3de7e0ba0e fix(feed): stop the compact row's meta sliding about
The cell was a flex row, so whatever sat left pushed the rest: a 36:36
and a 3:31:09 put the marker beside them at different x, and a live video
— which has no duration at all — pulled it left again. The column edges
lined up, which is all I had checked; the data inside them did not.

Each datum now has a fixed slot in a grid, the duration right-aligned as
a number should be. The chip becomes an icon, about a letter wide: it is
the least important thing in the row, and three states have to stay apart
by SHAPE — in the youtube scheme --accent is red, so a live dot and a
stream dot would be the same dot.
2026-07-17 00:41:33 +02:00
peter 26c7ed7471 fix(nav): stop back-to-top dying on the first visit to a lazy page
It looked <main> up when it bound, which the lazy pages won. On a first
visit to the channel page or Plex the chunk is still loading, so the
Suspense fallback is on screen, there is no <main> to find, and the effect
gave up — its dep never changes again, so the button stayed dead for that
whole visit and only worked once the chunk was cached.

It now reads the scroller off the scroll event instead, captured on the
document: scroll doesn't bubble but it does capture, so there is nothing
to look up and nothing to race. Also 50% bigger, as asked (44 -> 64).
2026-07-17 00:14:31 +02:00
peter 7dda44c487 fix(feed): size the compact row's columns for Hungarian too
Every width was measured in the English UI, but "101.7K megtekintés" is
139px against "101.7K views" at 79 — so a Hungarian user's view count
ellipsed itself in a column whose job is to show it. The channel column
gives back the room: its names truncate at any width we can afford (12
of 102 at 128 vs 5 at 160), and a clipped name still reads where a
clipped number does not.

The channel name also carries a tooltip now — it truncates hardest in
the fixed column and had no way to be read in full.
2026-07-16 23:59:49 +02:00
peter 5433a60ac9 fix(feed): size the badge column against the library's worst case
A 60-video sample topped out at 2:00:58, so the column was 8px short of
an 11-hour stream that is also saved — and the library holds one. That row
would wrap to two lines in the one layout that promises single ones.

The stacked layouts' textBlock now builds after the one-line row returns,
since only they use it.
2026-07-16 23:54:21 +02:00
peter f5649fc0b9 improvement(feed): make the compact row a real table
Actions lead the row: the title is what you read, so that is the shortest
pointer trip to them. Every column after it is a fixed width, which is
what puts the same datum on the same vertical line in every row — a CSS
grid cannot do it, since VirtualFeed renders each row as its own subtree.
Widths come from measuring the real data, not from taste.

Metadata rises to the channel name's size, and the duration drops its
chip: over a thumbnail that background carries contrast against the
artwork, but in a text column it only made one data column look unlike
its neighbours.
2026-07-16 23:46:34 +02:00
peter a78d0faa69 fix(feed): keep the small card's action row inside the card
Six buttons need 188px but the small card's text column bottoms out near
160 (its 180px column less padding), so the reset button — the only one
that is conditional, hence the only one anyone noticed — hung outside the
card by up to 15px. The buttons go tighter there (154px), and the row can
wrap as a backstop rather than clip. AddToPlaylist and DownloadButton now
take the button style as a prop so all six stay one size.

Tile actions also pin to the tile's bottom edge, so they land in the same
place whether the title above ran to one line or three.
2026-07-16 23:34:46 +02:00
peter 5436a81d10 refactor(feed): read the view spec's column bounds in one place 2026-07-16 04:23:43 +02:00
peter 6f1afbbb13 fix(feed): re-measure the bare row's first-paint estimate
Making it a single line halved its height (82 -> 46), which left the
virtualizer sizing a 60-item page ~2160px too tall.
2026-07-16 04:20:59 +02:00
peter f8a6ce69c7 fix(feed): give the small card the icon-only thumbnail controls too
Its column bottoms out at 180px, so its image is narrower than a row's
(measured 170px) while still carrying the 194px labelled pair — the same
overflow the row fix was for. The rule is the thumbnail's size class, not
the family: only the large card has room for labels.
2026-07-16 04:18:31 +02:00
peter ace4a144e8 fix(feed): stop the layout juddering when content lands on the scroll threshold
The feed takes its column count from main's clientWidth and its
thumbnails are aspect-ratio boxes, so the two feed each other once the
content height sits within a scrollbar's width of the viewport: the bar
appears, 15px goes, thumbs shorten, the content fits, the bar leaves,
and round again — every frame. Measured: scrollbar width alternating
0/15 and clientWidth 987/972 on consecutive frames. Reserving the gutter
holds clientWidth constant and breaks it.
2026-07-16 04:14:27 +02:00
peter f108c93c02 improvement(feed): rework the row views from UAT feedback
The bare row becomes a single line — title truncating into the space
left, then channel, badges, meta and the actions each holding a fixed
column — and it drops the 896px reading cap, since with the actions
right-aligned there is no gap for the extra width to stretch. The
actions get a reserved slot: the reset button only exists for videos
with watch state, so without one the meta sat 32px further right on
some rows than others.

Rows and tiles get a permanent container instead of a hover-only fill.
They were transparent at rest and painted an opaque bg on hover, so
running the pointer down the list kept covering and re-revealing the
ambient backdrop — which reads as the background flickering.

Card actions now sit on the bottom edge rather than wherever the title
ends, and the small thumbnails carry icon-only controls: the labelled
Continue / Restart are sized for a card's image and were clipped at
176px.
2026-07-16 04:14:27 +02:00
peter de1b32ba6f refactor(feed): tidy the inline badge classes and the no-thumb guard 2026-07-16 03:28:56 +02:00
peter 944dedb218 fix(feed): keep the duration, live state and resume bar in the bare row
Dropping Thumb dropped everything it draws ON the image — duration, the
live/stream badges, the saved marker and the resume bar. Those are
metadata, not decoration, so they move into the meta line and the bar
onto the row. Splits the spec's overloaded compact flag into thumb/dense,
which meant one thing per family and nothing on its own.
2026-07-16 03:25:05 +02:00
peter 78f21eb46b feat(feed): add the small-card, compact-row and tile views
Tiles answer the row view's wasted space: the actions move under the meta
into vertical slack the thumbnail already reserves, which frees ~168px of
width, so the block survives in a narrow column and the list can go
multi-column. Rows stay single-column on purpose — reading down one edge
is why that view exists.

The column count now comes from a per-view minimum width, so every
multi-column view reflows on the mechanism the card grid already used.
2026-07-16 03:17:30 +02:00
peter 1a94411a03 fix(feed): clamp the view menu's roving index to the option list
Guards the shared switcher against a caller whose option list shrinks
while the menu is open, which would leave every item un-tabbable.
2026-07-16 02:44:22 +02:00
peter 67f14d145f fix(feed): open the view menu on the active item, group its radio items
Seeding the roving index from an effect only scheduled the update, so the
focus effect still read the previous index and focused the wrong item for
one render — a screen reader announced both. Seed it where the menu opens
instead. The modes are one exclusive choice, so they also need a group to
scope their checked-ness to.
2026-07-16 02:42:06 +02:00
peter 805441ac58 fix(feed): give the view switcher the keyboard model its menu role promises
Arrows/Home/End move a roving focus and Escape hands focus back to the
trigger — the menu unmounts under the focused item, which stranded focus
on <body>. Outside-click deliberately does not refocus: that would steal
focus from whatever was just clicked.
2026-07-16 02:36:56 +02:00
peter eae65456ac feat(feed): pick the feed's view from its own toolbar
The view mode moves out of Settings into a switcher next to sort, on the
feed and channel pages alike. It leaves the Settings prefs draft for its
own eager save: a toolbar click must not mark that page dirty and trip
its leave-page guard.

lib/feedView.ts owns the vocabulary and maps the old grid/list prefs
forward on read, so nobody's saved choice is lost.
2026-07-16 02:29:32 +02:00
peter 303c896d87 chore(release): 0.44.0 — peek-open panels, active-filter chips, channel filter, sync chip 2026-07-16 01:21:55 +02:00
peter 34fd88faf6 fix(channel): "Show in my feed" adds to the picked channels, not replaces
Sprint 5 made the channel filter multi-select, but this handler still wrote a
single-element array — so arriving from a second channel page silently dropped
the first. Union the id in (deduped, so repeating it for an already-picked
channel is a no-op).
2026-07-16 01:18:23 +02:00
peter 36581f2380 fix(channel): "Show in my feed" lands on the feed, not where you came from
It called closeChannel() before setPage("feed"). closeChannel runs
history.back(), and the popstate that restores the page fires AFTER the
synchronous setPage — so the restore won, dropping you back on whatever page the
channel was opened from. Opening from the feed hid it: the page it restored
happened to be the feed.

setPage already closes an open channel page and pushes a clean history entry, so
the closeChannel() call was both redundant and the bug.
2026-07-16 01:13:08 +02:00
peter 0d10e0f0a5 feat(filters): filter the feed by several channels at once (OR)
Pick any number of channels; they OR together and every other filter applies
across the lot — "the last week's unwatched from these three".

`channelId`/`channelName` are replaced outright by `channelIds: string[]`, no
legacy reader (standing rule: no back-compat unless asked — a saved view written
against the old field simply loses its channel filter). The wire and the share
URL move with it: repeated `channel_ids` params, `?channel=a,b`. Backend ORs via
`Video.channel_id.in_()`; the count endpoint shares the same params object.

Two things the migration exposed:
- SavedViewsWidget fed DB blobs straight into serializers that now index an
  array — the FeedFilters type says nothing about what an older build wrote, so
  the blob is coerced where it enters. It white-screened the app before this.
- Chips carried channelName to label themselves. Without it they resolve names
  from the channel cache, so they now fall back to the id, not a shared
  "This channel" that would render N identical chips while the cache loads.
2026-07-16 00:54:22 +02:00
peter 82e351bc62 improvement(channel): bring the actions to the tabs, list every channel
Channel page: the actions sat in the header's top-right corner, a page-width
mouse trip from anything else you'd click. There are only ever two tabs, so
they ride the tab row now, right next to About.

Channel picker: drop the 50-row cap — 320 plain buttons cost nothing to render
(the feed virtualises thousands), so the whole list scrolls in the same box. The
cap was caution, not a measurement. The box gets the app's usual edge-fade to
hint at more, and a line saying "Priority first, then A-Z" — the backend orders
by `priority DESC, lower(title)`, which reads as random to anyone who hasn't set
a priority.
2026-07-16 00:32:53 +02:00
peter 452fbd43de fix(filters): stop the channel list squashing its own rows flat
The picker's list is a flex column with a max height, so its fifty buttons were
flex children free to shrink: each collapsed to ~12px and clipped its own text
into an unreadable smudge instead of the box scrolling. shrink-0 on the rows.

Colour, contrast (9.03) and opacity all measured fine — the height was the
broken part, and no colour check was ever going to see that.
2026-07-16 00:16:32 +02:00
peter 3caef7f6f9 feat(filters): make the feed's channel filter reachable
`filters.channelId` was settable by exactly one thing: a "?channel=" URL from a
shared view. The panel still rendered a CHANNEL card whose only control was
"remove" — a remove button for a filter the UI could not apply.

Two ways in now. The panel gets a real Channel group (searchable picker,
first-class alongside date/tags/language, so the chip row surfaces it for free);
normalizeLayout appends it to saved layouts, so nobody loses their arrangement.
And a subscribed channel's page gets "Show in my feed", which applies the filter
and keeps the rest of your filters — the point is "this channel, the way I
normally read", which the channel page itself can't answer (it runs show=all,
scope=all over the whole catalog).

The picker owns its channel query so a few-hundred-entry list loads when the
panel is actually opened, not on every feed visit, and says "showing 50 of N"
rather than quietly stopping.
2026-07-16 00:11:30 +02:00
peter 0ab1d0b057 fix(header): show the sync chip only where it means something, trim its copy
The chip is about YouTube subscription syncing, so it only earns its place on
the feed those subscriptions fill and the manager where you curate them. On
Plex — a different source entirely — plus playlists, downloads and stats it was
just noise.

Its tooltips were written for a roomy sidebar block and read like documentation
on a pill: the counts explanation went 120 chars -> 48, the full-history one
142 -> 46, and pause/resume drop "background".
2026-07-15 23:55:37 +02:00
peter e642b356a9 fix(header): sit the sync chip with the pills, shorten its tooltip
Pinning it to the header's right edge read fine on a narrow window and stranded
it on a wide one — it's a status, but it's still part of the pill row. It now
follows the search bar.

Its tooltip was the long explanation of the counts, which is redundant next to a
popover that spells them out and overflowed the viewport from the right edge.
The chip gets the counts themselves; the explanation moves onto the popover's
counts row, where there's room for it.
2026-07-15 23:48:36 +02:00
peter 104183118d feat(header): move the sync status into a chip on the header
The rail spent a two-row block plus a divider on per-user sync status. It's
global, not per-module, so it now rides the right end of the floating header as
a pill: the headline count at a glance, an accent dot when something wants you
(paused / missing history), and the counts, state, the channel-manager link and
pause in a popover.

Frees the rail's whole top block. Also drops SyncStatus's two now-dead layouts:
"bar" was a leftover of the top bar removed in 0.42.0, and "rail" lost its only
caller here — the component is chip-only, ~60 lines lighter, and the popover
reuses useDismiss.
2026-07-15 22:42:31 +02:00
peter c440a043f2 fix(feed): no active-filter chips on channel pages
A channel page renders the Feed from its own baseline — channel-scoped,
whole-catalog, show-all, live included — so measuring it against the feed's
defaults labelled all five as "filters you applied" when they are just the page
being itself. It also has no filter sidebar (App hides it), and revealing what
the collapsed panel hides is the chip row's only job.
2026-07-15 22:30:07 +02:00
peter 644d65a3fd feat(feed): removable active-filter chips above the feed
With the filter panel now collapsed to a tab, there was no way to see what was
narrowing the feed without opening it. Every applied filter is now a chip you
can drop on its own, with a Clear all when more than one is on.

The panel's count badge and the chip row read the SAME list (lib/useActiveFilters),
so they can't disagree about what's applied; that list also owns the reset both
"Clear all"s share. Replaces the hand-summed activeCount in Sidebar.
2026-07-15 22:11:44 +02:00
peter 837b48cbfa refactor(feed): lift the sort vocabulary out of Feed into lib/feedSort
The active-filter chips need to label a sort, and parseSort/SORT_MAP were
module-private in Feed. Pure move, no behaviour change.
2026-07-15 22:03:40 +02:00
peter 3596d83acd fix(panels): stop the tab bleeding through the card's rounded corner
The tab stays mounted under the open card on purpose, but the card's 17px corner
radius cuts away its own pixels and the tab's square docked corner sits right
under that arc — showing a hairline of tab glass. Fade the tab out while the
card is open; opacity-0 rather than invisible/hidden, so it stays hoverable and
focusable (that is the whole reason it stays mounted).
2026-07-15 21:44:22 +02:00
peter 532a1c2842 fix(nav): give the rail and the side panel a dead zone between them
The peek handlers sat on the rail's layout slot, which is 80px wide while the
card it holds is only 56px — so the card's own margin was a live hover zone with
nothing visible in it. Leaving the filter tab or panel to the left put the
cursor straight into it and the rail popped open unprompted; the gap between the
two hover zones was literally 0px. Moving the handlers onto the card leaves a
16px no man's land (x=69..85) where neither opens.
2026-07-15 21:38:22 +02:00
peter 79d9bca290 feat(panels): peek-open side panels with a docked tab
The shared SidePanel now follows the nav rail's model, so Filters, Plex and
Playlists all inherit it: unpinned it sits as a small tab docked flush against
the rail (replacing the full-height vertical strip) and expands into the card on
hover or keyboard focus without pushing content; pinned it stays expanded
in-flow. The collapse toggle became pin/unpin, and panels now default to
unpinned. The tab carries the active-filter count and an accent ring.

The tab stays mounted under the open card on purpose: swapping it out from under
the pointer drops the hover that opened the card, and unmounting it while
focused strands keyboard focus.

Measured: feed content starts at 123px instead of 336px when unpinned.
2026-07-15 20:57:21 +02:00
peter 11451c1af2 refactor(nav): extract the peek-open hook for reuse
The rail's hover/focus-to-peek logic — including the portal-proof focus tracking
(React-tree onFocus/onBlur plus a safety net for portals that unmount while
focused) — is about to be needed verbatim by the side panels. Lift it to
useHoverFocusWithin now that a second consumer exists. No behaviour change.
2026-07-15 20:57:21 +02:00
peter 9d9c6894f4 chore(release): 0.43.1 — sidebar no longer latches open after a portaled menu 2026-07-15 20:29:41 +02:00
peter bcec462ef6 fix(nav): stop the rail latching open after a portaled menu closes
Opening the language switcher (or the account popover) and then dismissing it
left the unpinned rail stuck expanded. Those menus are portaled and unmount
while focused, and browsers fire no blur when a focused node is removed — so the
wrapper's onBlur never arrived and `focused` stayed true forever.

Keep the React-tree onFocus/onBlur (portals bubble through it, which is what
correctly holds the rail open while its own menu has focus) and add a safety net
for the one case they miss: when focus has landed nowhere, clear it.
2026-07-15 20:24:40 +02:00
peter 44926f1c23 chore(release): 0.43.0 — nav overlay sidebar, account chrome, opt-in glass tuner 2026-07-14 21:41:44 +02:00
peter 6a9bf948f9 feat(appearance): opt-in glass tuner toggle in Settings
Graduate the floating glass tuner from a localhost-only dev tool to a
user-facing opt-in: it's now gated on a per-user `theme.showTuner` pref (off by
default) with a "Show the glass tuner" switch in Settings → Appearance, riding
the existing theme draft-save. Softened the dev-only framing. Behaviour and look
are unchanged; the full preset system is a separate epic.
2026-07-14 21:39:53 +02:00
peter ad1cc4586a feat(nav): slim hover-expand sidebar, role tooltip, logout button
The rail now sits slim by default and expands to a labelled overlay on hover
or keyboard focus without pushing content; the manual toggle became a pin
(pinned = the old in-flow expanded rail). The header no longer reserves a fixed
504px left offset — it's absolutely positioned inside the content column, so it
tracks the actual left zone and reclaims the dead gap. The role badge is gone;
role now shows via a title tooltip on the username, and a logout quick-button
joins the language/about cluster.
2026-07-14 21:39:30 +02:00
peter 27b9e83af6 docs(release): expand v0.42.0 notes (header, side panels, i18n, notifications) 2026-07-13 05:13:52 +02:00
peter facb3ac9cb feat(ui): unified floating side panels for filters and playlists
Replace the full-height filter rails with one shared floating glass SidePanel
used by Feed, Plex and Playlists. Each collapses to a slim tab beside the nav;
its sections are reorderable "islands" (drag / hide / per-group collapse) with a
per-panel saved layout; the body hides its scrollbar with a soft top/bottom edge
fade. The nav rail becomes a matching floating rounded card.

- New shared SidePanel / PanelGroup / PanelGroups + useScrollFade hook.
- Generalise the feed-only sidebarLayout into a per-panel panelLayout (feed keeps
  its storage key; plex/playlists get their own; persisted per account).
- Lift the Playlists rail to App level (selected playlist is now App state) so it
  floats and collapses like the filter rails; remove the old CollapsedFilterRail.
- The floating top header's fixed offset accounts for the panels' margins.
2026-07-13 05:13:40 +02:00
peter c2d914bacb feat(notifications): coalesce identical notifications with a repeat count
Fold a burst of identical notifications (same level+title+message within a
short window) into one entry with a ×N count instead of stacking copies — tames
a crash-retry loop or a repeatedly-failing poll. Shown in the toast and centre.
2026-07-13 05:13:18 +02:00
peter 07024de6c2 feat(i18n): retire the German interface language
Only English and Hungarian remain. Drops the de locale files, the LANGUAGES
entry, the Google-locale mapping in auth, and the German welcome-message
template. A legacy "de" preference falls back to English gracefully. Content-
language detection and subtitle languages are untouched (media, not UI).
2026-07-13 05:13:08 +02:00
peter 076a2fbe6f fix(header): restore inner scroll broken by the floating-header wrapper
The wrapper that pads content below the fixed header lacked min-h-0, so as a
flex child with min-height:auto it grew to its content's height instead of
staying bounded to the viewport. That let the whole document scroll (the fixed
header/nav stayed put while content ran off-screen). Add min-h-0 so the wrapper
stays viewport-height and the inner <main> keeps its own overflow scroll.
2026-07-13 02:39:43 +02:00