{"version":3,"sources":["../../src/changelog/reactions.tsx","../../src/changelog/changelog-entry.tsx","../../src/changelog/changelog-filter.tsx","../../src/changelog/changelog-page.tsx"],"names":["REACTIONS","Reactions","entryId","onReact","className","useT","jsx","cn","resolve","emoji","key","fallback","t","value","ChangelogEntry","R","entry","ref","locale","useLocale","resolvedTitle","useResolvedText","resolvedDescription","formattedDate","d","isoDate","jsxs","MediaSlot","toMediaSlotProps","ChangelogFilter","categories","selected","onSelect","allLabel","resolveT","navLabel","items","y","itemsRef","handleKeyDown","event","idx","dir","next","cat","isSelected","label","el","e","ChangelogPage","entries","controlledCategory","onCategoryChange","direction","internalCategory","setInternalCategory","u","isControlled","handleSelect","filtered","emptyText"],"mappings":"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","file":"index.cjs","sourcesContent":["'use client'\n\nimport { cn, useT } from '@tour-kit/core'\n\nexport type Reaction = '👍' | '😐' | '👎'\n\ninterface ReactionDef {\n  emoji: Reaction\n  key: string\n  fallback: string\n}\n\nconst REACTIONS: ReadonlyArray<ReactionDef> = [\n  { emoji: '👍', key: 'changelog.reaction.thumbs_up', fallback: 'Thumbs up' },\n  { emoji: '😐', key: 'changelog.reaction.neutral', fallback: 'Neutral' },\n  { emoji: '👎', key: 'changelog.reaction.thumbs_down', fallback: 'Thumbs down' },\n]\n\nexport interface ReactionsProps {\n  /** Stable id of the entry these reactions belong to. Forwarded as the first arg of `onReact`. */\n  entryId: string\n  /** Fire-and-forget callback. The component holds no reaction state. */\n  onReact?: (id: string, emoji: Reaction) => void\n  className?: string\n}\n\n/**\n * Three-button reaction row (👍 😐 👎). Stateless: clicks bubble up via\n * `onReact`. Aria-labels resolve through `useT()` with English fallbacks\n * when no `LocaleProvider` (or matching message) is present.\n */\nexport function Reactions({ entryId, onReact, className }: ReactionsProps) {\n  const t = useT()\n\n  return (\n    <div\n      className={cn('tk-reactions', className)}\n      // biome-ignore lint/a11y/useSemanticElements: <fieldset> imposes default browser styling unsuitable for this inline 3-button row; role=\"group\" preserves a11y grouping semantics without a form control.\n      role=\"group\"\n      aria-label={resolve(t, 'changelog.reactions.label', 'Reactions')}\n    >\n      {REACTIONS.map(({ emoji, key, fallback }) => (\n        <button\n          key={emoji}\n          type=\"button\"\n          aria-label={resolve(t, key, fallback)}\n          onClick={() => onReact?.(entryId, emoji)}\n          className={cn('tk-reactions__button', 'focus-visible:ring-2 focus-visible:ring-offset-2')}\n        >\n          <span aria-hidden=\"true\">{emoji}</span>\n        </button>\n      ))}\n    </div>\n  )\n}\n\n/**\n * `useT()` returns the key itself (dev) or `''` (prod) when a message is\n * missing. Fall back to the supplied English string in either case so the\n * UI is meaningful without a `LocaleProvider`.\n */\nfunction resolve(\n  t: (key: string, vars?: Record<string, unknown>) => string,\n  key: string,\n  fallback: string\n): string {\n  const value = t(key)\n  return value === '' || value === key ? fallback : value\n}\n","'use client'\n\nimport { cn, useLocale } from '@tour-kit/core'\nimport { MediaSlot } from '@tour-kit/media'\nimport * as React from 'react'\n\nimport { toMediaSlotProps } from '../lib/media-slot-adapter'\nimport { useResolvedText } from '../lib/use-resolved-text'\nimport type { ChangelogEntry as ChangelogEntryType } from './feed'\nimport { type Reaction, Reactions } from './reactions'\n\nexport interface ChangelogEntryProps {\n  /** A serialized changelog entry. Inherits `AnnouncementConfig` so the same payload can drive a feed and an in-app modal. */\n  entry: ChangelogEntryType\n  /** Forwarded to the inner `<Reactions>` row. Fire-and-forget. */\n  onReact?: (id: string, emoji: Reaction) => void\n  className?: string\n}\n\n/**\n * Single-entry renderer. Title and description resolve through\n * `useResolvedText` so consumers may pass plain strings, i18n keys, or rich\n * `ReactNode` bodies. Date formatting is locale-aware via\n * `Intl.DateTimeFormat`. The card is animated via the `motion-safe:`\n * Tailwind utilities — CSS gates the motion under\n * `prefers-reduced-motion: reduce`.\n */\nexport const ChangelogEntry = React.forwardRef<HTMLElement, ChangelogEntryProps>(\n  function ChangelogEntry({ entry, onReact, className }, ref) {\n    const { locale } = useLocale()\n    const resolvedTitle = useResolvedText(entry.title)\n    const resolvedDescription = useResolvedText(entry.description)\n\n    const formattedDate = React.useMemo(() => {\n      const d = new Date(entry.publishedAt)\n      if (Number.isNaN(d.getTime())) return ''\n      return new Intl.DateTimeFormat(locale || 'en', { dateStyle: 'medium' }).format(d)\n    }, [entry.publishedAt, locale])\n\n    const isoDate = React.useMemo(() => {\n      const d = new Date(entry.publishedAt)\n      return Number.isNaN(d.getTime()) ? undefined : d.toISOString()\n    }, [entry.publishedAt])\n\n    return (\n      <article\n        ref={ref}\n        id={entry.id}\n        className={cn(\n          'tk-changelog-entry',\n          'motion-safe:animate-in motion-safe:fade-in motion-safe:duration-200',\n          className\n        )}\n      >\n        {entry.media && <MediaSlot {...toMediaSlotProps(entry.media)} />}\n        <header className=\"tk-changelog-entry__header\">\n          <h2 className=\"tk-changelog-entry__title\">{resolvedTitle}</h2>\n          <div className=\"tk-changelog-entry__meta\">\n            {isoDate && (\n              <time className=\"tk-changelog-entry__date\" dateTime={isoDate}>\n                {formattedDate}\n              </time>\n            )}\n            {entry.category && <span className=\"tk-changelog-entry__badge\">{entry.category}</span>}\n          </div>\n        </header>\n        {resolvedDescription !== undefined && resolvedDescription !== null && (\n          <div className=\"tk-changelog-entry__body\">{resolvedDescription}</div>\n        )}\n        <Reactions entryId={entry.id} onReact={onReact} />\n      </article>\n    )\n  }\n)\n","'use client'\n\nimport { cn, useT } from '@tour-kit/core'\nimport * as React from 'react'\n\nexport interface ChangelogFilterProps {\n  /** Distinct categories derived from the entry list (no \"All\" sentinel — the filter prepends it). */\n  categories: string[]\n  /** Currently selected category. `null` = \"All\". */\n  selected: string | null\n  /** Fires with the next category (or `null` for \"All\") on click / Enter / Space. */\n  onSelect: (next: string | null) => void\n  className?: string\n}\n\n/**\n * Pure presentational sidebar. Renders an \"All\" reset followed by one button\n * per category. Implements roving tabindex so screen-reader users can cycle\n * options with `ArrowUp` / `ArrowDown` (wrapping at both ends).\n */\nexport function ChangelogFilter({\n  categories,\n  selected,\n  onSelect,\n  className,\n}: ChangelogFilterProps) {\n  const t = useT()\n  const allLabel = resolveT(t, 'changelog.filter.all', 'All')\n  const navLabel = resolveT(t, 'changelog.filter.label', 'Filter by category')\n  const items: Array<string | null> = React.useMemo(() => [null, ...categories], [categories])\n  const itemsRef = React.useRef<Array<HTMLButtonElement | null>>([])\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>, idx: number) => {\n      if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return\n      event.preventDefault()\n      const dir = event.key === 'ArrowDown' ? 1 : -1\n      const next = (idx + dir + items.length) % items.length\n      itemsRef.current[next]?.focus()\n    },\n    [items.length]\n  )\n\n  return (\n    <nav className={cn('tk-changelog-filter', className)} aria-label={navLabel}>\n      <ul className=\"tk-changelog-filter__list\">\n        {items.map((cat, idx) => {\n          const isSelected = cat === selected\n          const label = cat ?? allLabel\n          return (\n            <li key={cat ?? '__all__'}>\n              <button\n                ref={(el) => {\n                  itemsRef.current[idx] = el\n                }}\n                type=\"button\"\n                aria-pressed={isSelected}\n                tabIndex={isSelected ? 0 : -1}\n                onClick={() => onSelect(cat)}\n                onKeyDown={(e) => handleKeyDown(e, idx)}\n                className={cn(\n                  'tk-changelog-filter__item',\n                  'focus-visible:ring-2 focus-visible:ring-offset-2',\n                  isSelected && 'tk-changelog-filter__item--selected'\n                )}\n              >\n                {label}\n              </button>\n            </li>\n          )\n        })}\n      </ul>\n    </nav>\n  )\n}\n\nfunction resolveT(\n  t: (key: string, vars?: Record<string, unknown>) => string,\n  key: string,\n  fallback: string\n): string {\n  const value = t(key)\n  return value === '' || value === key ? fallback : value\n}\n","'use client'\n\nimport { cn, useLocale, useT } from '@tour-kit/core'\nimport * as React from 'react'\n\nimport { ChangelogEntry } from './changelog-entry'\nimport { ChangelogFilter } from './changelog-filter'\nimport type { ChangelogEntry as ChangelogEntryType } from './feed'\nimport type { Reaction } from './reactions'\n\nexport interface ChangelogPageProps {\n  entries: ChangelogEntryType[]\n  /** Controlled selected category. `null` = \"All\". Omit to use uncontrolled internal state. */\n  category?: string | null\n  /** Required to switch the component into controlled mode. */\n  onCategoryChange?: (next: string | null) => void\n  /** Forwarded to every entry's `<Reactions>` row. Fire-and-forget. */\n  onReact?: (id: string, emoji: Reaction) => void\n  className?: string\n}\n\n/**\n * Drop-in changelog page: category-filter sidebar + entry list. Router-agnostic\n * — consumers wire `category`/`onCategoryChange` for URL-sync. Without those\n * props, the component manages an internal `useState` (uncontrolled).\n */\nexport function ChangelogPage({\n  entries,\n  category: controlledCategory,\n  onCategoryChange,\n  onReact,\n  className,\n}: ChangelogPageProps) {\n  const t = useT()\n  const { direction } = useLocale()\n  const [internalCategory, setInternalCategory] = React.useState<string | null>(null)\n  const isControlled = controlledCategory !== undefined\n  const selected = isControlled ? (controlledCategory ?? null) : internalCategory\n\n  const handleSelect = React.useCallback(\n    (next: string | null) => {\n      if (!isControlled) setInternalCategory(next)\n      onCategoryChange?.(next)\n    },\n    [isControlled, onCategoryChange]\n  )\n\n  const categories = React.useMemo(\n    () => Array.from(new Set(entries.flatMap((e) => (e.category ? [e.category] : [])))),\n    [entries]\n  )\n\n  const filtered = React.useMemo(\n    () => (selected ? entries.filter((e) => e.category === selected) : entries),\n    [entries, selected]\n  )\n\n  const emptyText = resolveT(t, 'changelog.empty', 'No changelog entries yet')\n\n  return (\n    <div dir={direction ?? 'ltr'} className={cn('tk-changelog-page', className)}>\n      <ChangelogFilter categories={categories} selected={selected} onSelect={handleSelect} />\n      {filtered.length === 0 ? (\n        <p className=\"tk-changelog-page__empty\">{emptyText}</p>\n      ) : (\n        <ul className=\"tk-changelog-page__list\">\n          {filtered.map((entry) => (\n            <li key={entry.id}>\n              <ChangelogEntry entry={entry} onReact={onReact} />\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n\nfunction resolveT(\n  t: (key: string, vars?: Record<string, unknown>) => string,\n  key: string,\n  fallback: string\n): string {\n  const value = t(key)\n  return value === '' || value === key ? fallback : value\n}\n"]}