import type { ReactNode } from 'react' import { cn } from '../../lib/cn' import { Tag, type TagTone } from '../Tag' import { Text } from '../Text' import { VisuallyHidden } from '../VisuallyHidden' import { formatDate } from '../../lib/format' export interface ChangelogEntry { id: string date: Date title: string /** "v2.14". */ version?: string body?: ReactNode /** "New", "Improved", "Fixed" get their own tones; anything else is neutral. */ tags?: string[] href?: string /** A screenshot or clip. timeline only. */ media?: ReactNode } export interface ChangelogListProps { entries: ChangelogEntry[] /** When the reader last looked. Later entries are marked new. */ unreadSince?: Date /** timeline for a changelog page; compact for a "What's new" popover. */ variant?: 'timeline' | 'compact' label?: string headingLevel?: 'h2' | 'h3' className?: string } const TAG_TONES: Record = { new: 'accent', feature: 'accent', improved: 'neutral', improvement: 'neutral', fixed: 'outline', fix: 'outline', } /** * Product updates, newest first — as a public changelog page, or compact in the * "What's new" panel inside the app. * * Entries newer than the reader's last visit carry an accent dot and the word * "New" for assistive tech, which is the whole point of an in-app changelog: * a list of everything that has ever shipped is an archive, a list with the * unseen ones marked is news. */ export function ChangelogList({ entries, unreadSince, variant = 'timeline', label = 'Changelog', headingLevel: Heading = 'h2', className, }: ChangelogListProps) { const isUnread = (entry: ChangelogEntry) => Boolean(unreadSince && entry.date > unreadSince) const title = (entry: ChangelogEntry, size: string) => entry.href ? ( {entry.title} ) : ( {entry.title} ) const tags = (entry: ChangelogEntry) => entry.tags && entry.tags.length > 0 ? ( {entry.tags.map((tag) => ( {tag} ))} ) : null if (variant === 'compact') { return (
    {entries.map((entry) => { const unread = isUnread(entry) return (
  1. ) })}
) } return (
    {entries.map((entry) => { const unread = isUnread(entry) return (
  1. {entry.version && ( {entry.version} )}
  2. ) })}
) }