diff --git a/src/components/WhatsNewModal.tsx b/src/components/WhatsNewModal.tsx index 5c60357..0e8565e 100644 --- a/src/components/WhatsNewModal.tsx +++ b/src/components/WhatsNewModal.tsx @@ -2,10 +2,30 @@ 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 { + getChangelogForVersion, + type ChangelogEntry, + type ChangelogItem, + type ChangelogSection, +} from '../changelog' import { Tooltip } from './Tooltip' import { ChevronRightIcon, CloseIcon } from './icons' +// The modal adapts its layout to the size of the release: small releases keep +// the compact single-column list, while big ones switch to a two-pane layout +// with a section nav rail so no one pane is a marathon scroll. Preview both in +// UI Lab via `?changelog=unreleased` + the demo panel's "Open What's new modal". +type WhatsNewLayout = 'single' | 'rail' + +// Above this many total bullets, the single column gets unpleasantly long. +const RAIL_THRESHOLD = 20 + +function chooseLayout(entry: ChangelogEntry | null): WhatsNewLayout { + if (!entry || entry.sections.length < 2) return 'single' + const total = entry.sections.reduce((sum, section) => sum + section.items.length, 0) + return total > RAIL_THRESHOLD ? 'rail' : 'single' +} + // 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' @@ -25,6 +45,10 @@ const SECTION_STYLES: Record = { const NEUTRAL_STYLE = { label: 'text-gray-300', dot: 'bg-gray-400/70' } +function sectionStyle(title: string) { + return SECTION_STYLES[title] ?? NEUTRAL_STYLE +} + // 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[] { @@ -58,8 +82,44 @@ function renderInline(text: string): ReactNode[] { return nodes } -function Section({ section }: { section: ChangelogSection }) { - const style = SECTION_STYLES[section.title] ?? NEUTRAL_STYLE +function Item({ item, dot }: { item: ChangelogItem; dot: string }) { + return ( +
  • + +

    + {item.lead ? ( + <> + {item.lead} + {item.body ? — {renderInline(item.body)} : null} + + ) : ( + renderInline(item.body) + )} +

    +
  • + ) +} + +function ItemList({ + items, + dot, + className = '', +}: { + items: ChangelogItem[] + dot: string + className?: string +}) { + return ( + + ) +} + +function CollapsibleSection({ section }: { section: ChangelogSection }) { + const style = sectionStyle(section.title) const [open, setOpen] = useState(true) return ( @@ -88,23 +148,7 @@ function Section({ section }: { section: ChangelogSection }) { transition={{ duration: 0.18 }} className="overflow-hidden" > - + ) : null} @@ -112,12 +156,61 @@ function Section({ section }: { section: ChangelogSection }) { ) } +// 'rail' layout — a section nav rail on the left, the selected section's items +// on the right. +function RailSections({ sections }: { sections: ChangelogSection[] }) { + const [active, setActive] = useState(0) + const section = sections[Math.min(active, sections.length - 1)] + const style = sectionStyle(section.title) + + return ( +
    + +
    +

    + {section.title} +

    + +
    +
    + ) +} + +const MODAL_WIDTHS: Record = { + single: 'w-[min(88vw,560px)]', + rail: 'w-[min(90vw,780px)]', +} + 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) + const layout = chooseLayout(entry) useEffect(() => { if (!whatsNewOpen) return @@ -128,6 +221,8 @@ export function WhatsNewModal() { return () => window.removeEventListener('keydown', handleKeyDown) }, [whatsNewOpen, closeWhatsNew]) + const hasSections = entry !== null && entry.sections.length > 0 + return ( {whatsNewOpen ? ( @@ -140,7 +235,7 @@ export function WhatsNewModal() { transition={{ duration: 0.15 }} > event.stopPropagation()} initial={{ opacity: 0, scale: 0.97, y: 8 }} animate={{ opacity: 1, scale: 1, y: 0 }} @@ -169,16 +264,22 @@ export function WhatsNewModal() { -
    - {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. -

    - )} -
    + {hasSections && layout === 'rail' ? ( + + ) : ( +
    + {hasSections ? ( + entry.sections.map((section) => ( + + )) + ) : ( +

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

    + )} +
    + )}