import { useEffect, useState, type ReactNode } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { invoke } from "@tauri-apps/api/core"; import { useGalleryStore } from "../store"; import { getChangelogForVersion, type ChangelogSection } from "../changelog"; import { Tooltip } from "./Tooltip"; // Shown in the tooltip so the user can see the link's destination before // clicking. The actual navigation is handled by the open_changelog_url command. const CHANGELOG_URL = "https://github.com/JezzWTF/phokus/blob/main/CHANGELOG.md"; // Per-section accent. These all use the standard colour scale, which the // subtle-light theme re-maps to readable dark shades via CSS variables — so no // `light-theme:` overrides are needed (or wanted) here. See the theme note in // index.css: `bg-white`/`text-white` deliberately become *dark* in light mode, // so neutral surfaces must use the gray scale and trust the remap. const SECTION_STYLES: Record = { Added: { label: "text-emerald-300", dot: "bg-emerald-400/80" }, Changed: { label: "text-sky-300", dot: "bg-sky-300/80" }, Fixed: { label: "text-amber-300", dot: "bg-amber-400/80" }, Removed: { label: "text-rose-300", dot: "bg-rose-400/80" }, Security: { label: "text-violet-300", dot: "bg-violet-400/80" }, }; const NEUTRAL_STYLE = { label: "text-gray-300", dot: "bg-gray-400/70" }; // Render the small amount of inline markdown the changelog uses: **bold** and // `code`. Anything else passes through as plain text. function renderInline(text: string): ReactNode[] { const nodes: ReactNode[] = []; const regex = /\*\*(.+?)\*\*|`([^`]+)`/g; let lastIndex = 0; let key = 0; let match: RegExpExecArray | null; while ((match = regex.exec(text)) !== null) { if (match.index > lastIndex) { nodes.push(text.slice(lastIndex, match.index)); } if (match[1] !== undefined) { nodes.push( {match[1]} , ); } else if (match[2] !== undefined) { nodes.push( {match[2]} , ); } lastIndex = regex.lastIndex; } if (lastIndex < text.length) { nodes.push(text.slice(lastIndex)); } return nodes; } function Section({ section }: { section: ChangelogSection }) { const style = SECTION_STYLES[section.title] ?? NEUTRAL_STYLE; const [open, setOpen] = useState(true); return (
{open ? (
    {section.items.map((item, index) => (
  • {item.lead ? ( <> {item.lead} {item.body ? — {renderInline(item.body)} : null} ) : ( renderInline(item.body) )}

  • ))}
) : null}
); } export function WhatsNewModal() { const whatsNewOpen = useGalleryStore((s) => s.whatsNewOpen); const closeWhatsNew = useGalleryStore((s) => s.closeWhatsNew); const appVersion = useGalleryStore((s) => s.appVersion); const entry = getChangelogForVersion(appVersion); useEffect(() => { if (!whatsNewOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") closeWhatsNew(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [whatsNewOpen, closeWhatsNew]); return ( {whatsNewOpen ? ( event.stopPropagation()} initial={{ opacity: 0, scale: 0.97, y: 8 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.98, y: 6 }} transition={{ duration: 0.16 }} >

What's new

Phokus v{entry?.version ?? appVersion ?? "—"}

{entry?.date ?

Released {entry.date}

: null}
{entry && entry.sections.length > 0 ? ( entry.sections.map((section) =>
) ) : (

Release notes for this version aren't available in-app. See the full changelog on GitHub.

)}
) : null}
); }