fix(ui): E31 S2 — the tab names the object, the channel header wraps, the player fits a short window

- The browser tab names the place and the object: PageHeading reports the
  view's own title (lib/documentTitle), so a channel, playlist or any Plex
  view reads "<object> · <module> · Siftlode" however it was opened; the
  Feed is named too; a season adds its show (tabTitle).
- The channel page's tab + action row wraps and lets the tag lane fold into
  "+N": at 1024 it ran past the edge and the page scrolled sideways.
- The YouTube player card's width is capped from the window height less the
  chrome measured under the video: at 133% zoom (685px) it cut its channel row.
- Plex certificate words (Not Rated, Approved, Passed) are named through the
  vocabulary on the info badge, show page, collection, filter and chips.
- DESIGN.md: the zoom target is 125% and 133%, not 200% (decision log).
This commit is contained in:
2026-10-01 21:17:20 +02:00
parent 2543d44860
commit ec22eefd31
20 changed files with 265 additions and 46 deletions
+12 -4
View File
@@ -2,23 +2,31 @@ import type { TFunction } from "i18next";
import type { PlexTrackNaming } from "./api";
// E30: the closed vocabularies the server stores in ENGLISH — a TMDB crew job or department, an
// auto-tag, a built-in download profile, a YouTube topic — named in the reader's language. The data
// auto-tag, a built-in download profile, a YouTube topic, a Plex certificate's word ("Not Rated"; a
// code such as "PG-13" reads the same everywhere and falls through as stored) — named in the reader's language. The data
// stays language-neutral (it is filtered and matched on); only the NAME is translated, here, from the
// `vocab` locale area. A word the area does not hold shows as stored, so a new server value is never
// hidden — and the backend's own lists are held key-for-key to the area by backendIds.contract.
type Group = "crewJob" | "department" | "autoTag" | "profile" | "topic";
type Group = "crewJob" | "department" | "autoTag" | "profile" | "topic" | "certificate";
/** A vocabulary word's key: its letters and digits ("Health & Fitness" → "HealthFitness"). */
export function vocabKey(word: string): string {
return word.replace(/[^A-Za-z0-9]/g, "");
}
/** The i18n key of one word of `group` ("" when the word has no letters or digits) — for a caller
* whose `t` is not i18next's own (a pure builder handed a narrow `t`). */
export function vocabI18nKey(group: Group, word: string): string {
const key = vocabKey(word);
return key ? `vocab.${group}.${key}` : "";
}
/** One stored English word of `group`, in the reader's language (`fallback`, by default the word
* itself, when the area does not hold it). */
export function vocab(t: TFunction, group: Group, word: string, fallback = word): string {
const key = vocabKey(word);
return key ? t(`vocab.${group}.${key}`, { defaultValue: fallback }) : fallback;
const key = vocabI18nKey(group, word);
return key ? t(key, { defaultValue: fallback }) : fallback;
}
/** A download profile's name: a built-in one is seeded under an English name and named here; a user's