feat(ui): shared <Button> primitive + migrate ShareDialog/VideoEditor/ChannelPage proofs

Introduce frontend/src/components/ui/Button.tsx — the one action-button primitive
(variants primary|danger|outline|ghost|glass|danger-outline; sizes xs|sm|md|lg|icon;
icon/iconOnly/label/loading/fullWidth/forwardRef). No focus-visible (already centralized
in index.css @layer base). Migrate the cleanest proof sites off btnCls, and flip the
channel Subscribe button to the outline variant so it matches Full history (the user's ask).
This commit is contained in:
2026-08-28 02:59:21 +02:00
parent 9ae6c47d6c
commit 4892c38466
5 changed files with 202 additions and 57 deletions
+14 -16
View File
@@ -34,6 +34,7 @@ import type { FeedView } from "../lib/feedView";
import { channelYouTubeUrl, formatViews } from "../lib/format";
import { ChannelTagsControl } from "./ChannelTags";
import { useChannelTagActions } from "../lib/useChannelTagActions";
import { Button } from "./ui/Button";
// A dedicated channel page: an "About"-style header (banner, avatar, stats, subscribe) over the
// channel's videos (the catalog filtered to this channel). For an un-subscribed channel it
@@ -441,21 +442,18 @@ export default function ChannelPage({
it) so a large channel's paced tail reads as visible progress rather than a stuck
spinner. */}
{ch?.subscribed && !ch.backfill_done && !me.is_demo && (
<button
<Button
variant="outline"
onClick={() => !ch.deep_requested && deepBackfill.mutate()}
disabled={ch.deep_requested || deepBackfill.isPending}
loading={ch.deep_requested}
icon={<History className="w-4 h-4" />}
title={
ch.deep_requested
? t("channel.fullHistoryLoadingHint")
: t("channel.fullHistory")
}
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm border border-border text-muted hover:text-accent hover:border-accent disabled:opacity-60 disabled:hover:text-muted disabled:hover:border-border transition"
>
{ch.deep_requested ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<History className="w-4 h-4" />
)}
<span className="hidden sm:inline whitespace-nowrap">
{!ch.deep_requested
? t("channel.fullHistory")
@@ -466,7 +464,7 @@ export default function ChannelPage({
})
: t("channel.fullHistoryLoading")}
</span>
</button>
</Button>
)}
{!me.is_demo && (
<button
@@ -504,24 +502,24 @@ export default function ChannelPage({
{!me.is_demo &&
!ch?.blocked &&
(ch?.subscribed ? (
<button
<Button
variant="outline"
onClick={onUnsubscribe}
disabled={unsubscribe.isPending}
title={t("channel.unsubscribe")}
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm border border-border text-muted hover:text-fg disabled:opacity-50"
icon={<Check className="w-4 h-4" />}
>
<Check className="w-4 h-4" />
{t("channel.subscribedState")}
</button>
</Button>
) : (
<button
<Button
variant="outline"
onClick={() => subscribe.mutate()}
disabled={subscribe.isPending}
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm btn-primary disabled:opacity-50"
icon={<Plus className="w-4 h-4" />}
>
<Plus className="w-4 h-4" />
{t("channel.subscribe")}
</button>
</Button>
))}
{/* Tagging is a subscription-scoped action (attach_tag → _user_subscription 404s
otherwise), and personal tags are for your subscriptions — so only offer it on a