diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index ad3328f..ba928d2 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -656,6 +656,9 @@ export default function Feed({ options={FEED_VIEWS.map((id) => ({ id, label: t("feed.view." + id), icon: VIEW_ICON[id] }))} onChange={setView} label={t("feed.viewLabel")} + // This toolbar is packed — show/content chips, source, count, sort — so the mode's name + // only earns its place once the window is wide. + labelClassName="hidden xl:inline" /> diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx index 4155e51..be47893 100644 --- a/frontend/src/components/VideoCard.tsx +++ b/frontend/src/components/VideoCard.tsx @@ -356,15 +356,17 @@ function VideoCard({ // Two shapes for the count: the stacked layouts read it as prose ("1.5K views · 2 min ago"), so // they need the word; the row's column doesn't — a table doesn't repeat its unit on every line, // and the tooltip has the exact figure anyway. Same tooltip either way. - const viewsTitle = video.view_count != null ? `${video.view_count.toLocaleString()} ${t("card.views")}` : ""; - const viewsCount = video.view_count != null && ( - {formatViews(video.view_count)} - ); - const views = video.view_count != null && ( - - {formatViews(video.view_count)} {t("card.views")} - - ); + // One span either way, so the exact-count tooltip covers all of whatever is rendered: the stacked + // layouts read it as prose and need the word, the row's column doesn't — a table doesn't repeat + // its unit on every line. + const viewsCell = (withWord: boolean) => + video.view_count != null && ( + + {formatViews(video.view_count)} + {withWord && ` ${t("card.views")}`} + + ); + const views = viewsCell(true); const published = ( <> {relativeTime(video.published_at)} @@ -478,7 +480,7 @@ function VideoCard({ holds the widest bare form ("101.7K" at 48) in either language — dropping the word took this from 144 and handed the difference to the title. */}
- {viewsCount} + {viewsCell(false)}
{published}
{/* Thumb draws this over the image; with no image it goes on the row itself. */} diff --git a/frontend/src/components/ViewSwitcher.tsx b/frontend/src/components/ViewSwitcher.tsx index 45c8c38..ca16bda 100644 --- a/frontend/src/components/ViewSwitcher.tsx +++ b/frontend/src/components/ViewSwitcher.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { Check, ChevronDown, type LucideIcon } from "lucide-react"; +import clsx from "clsx"; import { useDismiss } from "../lib/useDismiss"; export interface ViewOption { @@ -20,11 +21,17 @@ export default function ViewSwitcher({ options, onChange, label, + labelClassName = "hidden", }: { value: T; options: readonly ViewOption[]; onChange: (v: T) => void; label: string; + /** Where the current mode's NAME is visible beside the icon — a Tailwind display class, e.g. + * `"hidden xl:inline"`. The caller sets it because only it knows how crowded its own toolbar is: + * the feed's is packed (show chips, content chips, source, count, sort) and can't spare the room + * until xl, while an emptier one could show it always. Defaults to icon-only. */ + labelClassName?: string; }) { const [open, setOpen] = useState(false); // Which item holds the roving tabindex; seeded to the active one when the menu opens. @@ -103,9 +110,7 @@ export default function ViewSwitcher({ className="shrink-0 inline-flex items-center gap-1 pl-2 pr-1 py-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition" > - {/* Name the current mode where the toolbar has room; below xl it's a busy row (show chips, - content chips, source, count, sort) and the icon has to carry it alone. */} - {current.label} + {current.label} {open && (