feat: Takeout walkthrough, design-system restyle, replacing import

Adds a seven-step in-app guide to exporting subscriptions from Google
Takeout, with the real URLs opened in the system browser.

Restyles the app onto the supplied design system: slate/sky palette,
9-15px type ladder, outline-first controls, tiered radii, borders for
separation and shadows only for elevation. Light and dark are both
designed, with a System/Light/Dark control and a pre-paint script so
the window does not flash light on a dark machine.

Importing now replaces the subscription list rather than merging, per
request. Because that can delete downloaded files, a confirmation
dialog names exactly what will go first.
This commit is contained in:
vincent
2026-08-29 03:00:54 +02:00
parent 11331671c5
commit cac6727072
18 changed files with 1202 additions and 306 deletions
+61 -36
View File
@@ -1,4 +1,5 @@
import type { ChannelWithCount } from "../types";
import { BTN_CHROME, HEADING } from "./ui";
interface Props {
channels: ChannelWithCount[];
@@ -6,56 +7,80 @@ interface Props {
onSelect: (id: string | null) => void;
onOpenSettings: () => void;
totalVideos: number;
totalDownloaded: number;
}
export default function Sidebar({
channels, activeChannel, onSelect, onOpenSettings, totalVideos,
channels, activeChannel, onSelect, onOpenSettings, totalVideos, totalDownloaded,
}: Props) {
const rowBase =
"w-full text-left px-3 py-2 rounded-lg text-sm flex items-center justify-between gap-2 transition-colors cursor-pointer";
const row =
"flex w-full cursor-pointer items-center justify-between gap-2 rounded-lg px-2 py-1.5 " +
"text-[13px] transition-colors";
const inactive =
"text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800";
const active = "bg-slate-900 text-white dark:bg-white dark:text-slate-900";
return (
<aside className="w-64 shrink-0 border-r border-edge flex flex-col bg-ink">
<div className="px-4 py-4 flex items-center gap-2">
<span className="text-xl"></span>
<span className="font-semibold tracking-tight">FlightTube</span>
</div>
<aside
className="flex max-h-[45vh] w-full shrink-0 flex-col overflow-hidden border-b
border-slate-300 bg-white lg:h-screen lg:max-h-none lg:w-[280px]
lg:border-b-0 lg:border-r dark:border-slate-800 dark:bg-slate-900"
>
<header
className="sticky top-0 z-20 flex items-center justify-between gap-2 border-b
border-slate-200 bg-white/95 px-4 py-3 backdrop-blur
dark:border-slate-800 dark:bg-slate-900/95"
>
<span className="flex items-center gap-2 text-[15px] font-semibold tracking-tight">
<span aria-hidden className="text-sky-500"></span>
FlightTube
</span>
<button onClick={onOpenSettings} className={`${BTN_CHROME} cursor-pointer`}>
Settings
</button>
</header>
<nav className="flex-1 overflow-y-auto px-2 pb-2 space-y-0.5">
<button onClick={() => onSelect(null)}
className={`${rowBase} ${
activeChannel === null ? "bg-raised text-white" : "text-muted hover:bg-surface"
}`}>
<nav className="min-h-0 flex-1 overflow-y-auto px-2 py-3">
<button
onClick={() => onSelect(null)}
className={`${row} ${activeChannel === null ? active : inactive}`}
>
<span className="font-medium">All subscriptions</span>
<span className="text-xs tabular-nums opacity-70">{totalVideos}</span>
<span className="shrink-0 font-mono text-[11px] tabular-nums opacity-70">
{totalDownloaded > 0 && `${totalDownloaded}/`}
{totalVideos}
</span>
</button>
{channels.length > 0 && (
<div className="pt-3 pb-1 px-3 text-[11px] uppercase tracking-wider text-muted/70">
Channels
</div>
<h2 className={`${HEADING} px-2 pb-1 pt-4`}>Channels</h2>
)}
{channels.map((c) => (
<button key={c.id} onClick={() => onSelect(c.id)} title={c.title}
className={`${rowBase} ${
activeChannel === c.id ? "bg-raised text-white" : "text-muted hover:bg-surface"
}`}>
<span className="truncate">{c.title}</span>
<span className="text-xs tabular-nums opacity-70 shrink-0">
{c.downloaded_count > 0 && (
<span className="text-emerald-400">{c.downloaded_count}/</span>
)}
{c.video_count}
</span>
</button>
))}
</nav>
<ul className="space-y-0.5">
{channels.map((c) => (
<li key={c.id}>
<button
onClick={() => onSelect(c.id)}
title={c.title}
className={`${row} ${activeChannel === c.id ? active : inactive}`}
>
<span className="truncate">{c.title}</span>
<span className="shrink-0 font-mono text-[11px] tabular-nums opacity-70">
{c.downloaded_count > 0 && `${c.downloaded_count}/`}
{c.video_count}
</span>
</button>
</li>
))}
</ul>
<button onClick={onOpenSettings}
className="m-2 px-3 py-2 rounded-lg text-sm text-muted hover:bg-surface text-left cursor-pointer">
Settings
</button>
{channels.length === 0 && (
<p className="px-2 py-3 text-[11px] leading-snug text-slate-500 dark:text-slate-400">
No subscriptions yet. Open Settings for a step-by-step guide to exporting
them from Google Takeout.
</p>
)}
</nav>
</aside>
);
}