refactor(feed): let the caller decide where the mode's name fits
The xl breakpoint was a fact about the FEED's toolbar, hard-coded into the switcher that E4's managers are meant to reuse — their rows are far emptier and would have hidden the label for no reason. It's a prop now, defaulting to icon-only. The two views spans collapse into one builder, which also fixes a regression I'd just added: splitting them put the exact-count tooltip on the number alone, so hovering the word gave nothing.
This commit is contained in:
@@ -656,6 +656,9 @@ export default function Feed({
|
|||||||
options={FEED_VIEWS.map((id) => ({ id, label: t("feed.view." + id), icon: VIEW_ICON[id] }))}
|
options={FEED_VIEWS.map((id) => ({ id, label: t("feed.view." + id), icon: VIEW_ICON[id] }))}
|
||||||
onChange={setView}
|
onChange={setView}
|
||||||
label={t("feed.viewLabel")}
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
// 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,
|
// 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.
|
// 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")}` : "";
|
// One span either way, so the exact-count tooltip covers all of whatever is rendered: the stacked
|
||||||
const viewsCount = video.view_count != null && (
|
// layouts read it as prose and need the word, the row's column doesn't — a table doesn't repeat
|
||||||
<span title={viewsTitle}>{formatViews(video.view_count)}</span>
|
// its unit on every line.
|
||||||
);
|
const viewsCell = (withWord: boolean) =>
|
||||||
const views = video.view_count != null && (
|
video.view_count != null && (
|
||||||
<span title={viewsTitle}>
|
<span title={`${video.view_count.toLocaleString()} ${t("card.views")}`}>
|
||||||
{formatViews(video.view_count)} {t("card.views")}
|
{formatViews(video.view_count)}
|
||||||
</span>
|
{withWord && ` ${t("card.views")}`}
|
||||||
);
|
</span>
|
||||||
|
);
|
||||||
|
const views = viewsCell(true);
|
||||||
const published = (
|
const published = (
|
||||||
<>
|
<>
|
||||||
{relativeTime(video.published_at)}
|
{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
|
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. */}
|
took this from 144 and handed the difference to the title. */}
|
||||||
<div className="shrink-0 w-16 text-sm text-muted text-right tabular-nums truncate hidden lg:block">
|
<div className="shrink-0 w-16 text-sm text-muted text-right tabular-nums truncate hidden lg:block">
|
||||||
{viewsCount}
|
{viewsCell(false)}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 w-48 text-sm text-muted truncate hidden xl:block">{published}</div>
|
<div className="shrink-0 w-48 text-sm text-muted truncate hidden xl:block">{published}</div>
|
||||||
{/* Thumb draws this over the image; with no image it goes on the row itself. */}
|
{/* Thumb draws this over the image; with no image it goes on the row itself. */}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Check, ChevronDown, type LucideIcon } from "lucide-react";
|
import { Check, ChevronDown, type LucideIcon } from "lucide-react";
|
||||||
|
import clsx from "clsx";
|
||||||
import { useDismiss } from "../lib/useDismiss";
|
import { useDismiss } from "../lib/useDismiss";
|
||||||
|
|
||||||
export interface ViewOption<T extends string> {
|
export interface ViewOption<T extends string> {
|
||||||
@@ -20,11 +21,17 @@ export default function ViewSwitcher<T extends string>({
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
|
labelClassName = "hidden",
|
||||||
}: {
|
}: {
|
||||||
value: T;
|
value: T;
|
||||||
options: readonly ViewOption<T>[];
|
options: readonly ViewOption<T>[];
|
||||||
onChange: (v: T) => void;
|
onChange: (v: T) => void;
|
||||||
label: string;
|
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);
|
const [open, setOpen] = useState(false);
|
||||||
// Which item holds the roving tabindex; seeded to the active one when the menu opens.
|
// Which item holds the roving tabindex; seeded to the active one when the menu opens.
|
||||||
@@ -103,9 +110,7 @@ export default function ViewSwitcher<T extends string>({
|
|||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<CurrentIcon className="w-4 h-4 shrink-0" />
|
<CurrentIcon className="w-4 h-4 shrink-0" />
|
||||||
{/* Name the current mode where the toolbar has room; below xl it's a busy row (show chips,
|
<span className={clsx("text-sm whitespace-nowrap", labelClassName)}>{current.label}</span>
|
||||||
content chips, source, count, sort) and the icon has to carry it alone. */}
|
|
||||||
<span className="hidden xl:inline text-sm whitespace-nowrap">{current.label}</span>
|
|
||||||
<ChevronDown className={`w-3 h-3 shrink-0 transition-transform ${open ? "rotate-180" : ""}`} />
|
<ChevronDown className={`w-3 h-3 shrink-0 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
|
|||||||
Reference in New Issue
Block a user