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
+53
View File
@@ -0,0 +1,53 @@
import { useCallback, useEffect, useState } from "react";
export type Appearance = "system" | "light" | "dark";
export const APPEARANCE_MODES: Appearance[] = ["system", "light", "dark"];
const KEY = "flighttube.appearance";
const media = window.matchMedia("(prefers-color-scheme: dark)");
function stored(): Appearance {
try {
const v = localStorage.getItem(KEY);
return v === "light" || v === "dark" || v === "system" ? v : "system";
} catch {
return "system";
}
}
const effective = (mode: Appearance) =>
mode === "system" ? (media.matches ? "dark" : "light") : mode;
/**
* Three states, not two. "System" keeps following the OS if it changes
* mid-session; light and dark are explicit overrides.
*/
export function useAppearance() {
const [mode, setMode] = useState<Appearance>(stored);
const [shown, setShown] = useState<"light" | "dark">(() => effective(stored()));
const apply = useCallback((m: Appearance) => {
const next = effective(m);
document.documentElement.classList.toggle("dark", next === "dark");
setShown(next);
}, []);
useEffect(() => {
apply(mode);
try {
localStorage.setItem(KEY, mode);
} catch {
/* storage blocked */
}
}, [mode, apply]);
useEffect(() => {
const onChange = () => {
if (mode === "system") apply(mode);
};
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, [mode, apply]);
return { mode, setMode, shown };
}