e46598c47d
Per user feedback: «по кнопке что нового должно открываться модальное окно с изменениями». UpdateBanner кнопка «Что нового» (т9n key `updateBanner.docs`) ранее открывала `/docs/` в новой вкладке — generic documentation page, не contextual для «что именно в этом обновлении». Теперь — открывает `WhatsNewModal` (тот же modal что показывает 🎁 button в TopBar) — public changelog с release notes сразу. ## Refactor - **`WhatsNewModal.tsx` (NEW)** — extracted controlled-mode modal с release notes. Props: `open`, `onClose`. Internal logic читает changelog, tracks lastSeen в localStorage, на open mark'ает latest как seen. - **`WhatsNewButton.tsx`** — refactored: trigger button + use `WhatsNewModal`. Поведение unchanged (🎁 icon с unread badge → click → modal). - **`UpdateBanner.tsx`** — replaced `window.open(DOCS_URL)` на `setWhatsNewOpen(true)` + render `<WhatsNewModal>`. DOCS_URL constant removed (больше не используется). ## UX intent Когда update detected: 1. User видит orange banner «Доступна новая версия v2.16.0» 2. Hover «Что нового» button → opens modal 3. User читает release notes (что добавлено, исправлено) ДО reload 4. Click «Обновить» → reload Раньше «Что нового» вёл к generic docs page — user не знал ЧТО изменилось, только что update есть. Modal даёт structured answer. ## Test plan - [x] `pnpm tsc --noEmit` clean - [x] `pnpm build` clean - [x] `pnpm vitest run useChangelog.test` — 11/11 pass (parser logic unchanged) - [ ] Manual: симулировать update banner (mock useAppVersion `updateAvailable=true`), click «Что нового» → modal opens - [ ] Manual: 🎁 button в TopBar также opens same modal - [ ] Manual: close modal → red dot disappears на 🎁 button (localStorage updated)
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
import { useMemo, useState } from 'react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { GiftIcon } from '@phosphor-icons/react'
|
||
import {
|
||
useChangelog,
|
||
readLastSeenVersion,
|
||
isVersionNewer,
|
||
} from './useChangelog'
|
||
import { WhatsNewModal } from './WhatsNewModal'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
/**
|
||
* «Что нового» button в TopBar.
|
||
*
|
||
* <p>🎁 GiftIcon с red unread badge counter (entries newer than
|
||
* {@code localStorage.ord-whatsnew-last-seen}). Click → {@link WhatsNewModal}.
|
||
*
|
||
* <p>Modal extracted в own component — также используется в
|
||
* {@link UpdateBanner} «Что нового» button (когда новая версия detected).
|
||
*/
|
||
export function WhatsNewButton() {
|
||
const { t } = useTranslation()
|
||
const [open, setOpen] = useState(false)
|
||
const entries = useChangelog()
|
||
|
||
// useState lastSeen — re-compute unread после modal close. localStorage
|
||
// write (внутри WhatsNewModal на open) не trigger'ит React re-render
|
||
// отдельно, поэтому tracking через useEffect on open close в modal.
|
||
// В button мы читаем initially — на close button перестроится через
|
||
// обычный rerender chain.
|
||
const lastSeen = useMemo(() => readLastSeenVersion(), [open])
|
||
const unreadCount = useMemo(
|
||
() => entries.filter((e) => isVersionNewer(e.version, lastSeen)).length,
|
||
[entries, lastSeen],
|
||
)
|
||
|
||
const label = t('whatsnew.button', { defaultValue: 'Что нового' })
|
||
|
||
return (
|
||
<>
|
||
<button
|
||
type="button"
|
||
title={label}
|
||
aria-label={label}
|
||
onClick={() => setOpen(true)}
|
||
className={cn(
|
||
'relative size-7 rounded-md text-ink-2 hover:text-ink hover:bg-surface-2',
|
||
'inline-flex items-center justify-center transition-colors',
|
||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||
)}
|
||
>
|
||
<GiftIcon size={14} weight={unreadCount > 0 ? 'fill' : 'regular'} />
|
||
{unreadCount > 0 && (
|
||
<span
|
||
aria-label={t('whatsnew.unread', {
|
||
count: unreadCount,
|
||
defaultValue: `${unreadCount} новых`,
|
||
})}
|
||
className="absolute -top-0.5 -right-0.5 inline-flex items-center justify-center min-w-[14px] h-3.5 px-1 rounded-full bg-mars text-on-mars text-[9px] font-semibold leading-none"
|
||
>
|
||
{unreadCount > 9 ? '9+' : unreadCount}
|
||
</span>
|
||
)}
|
||
</button>
|
||
|
||
<WhatsNewModal open={open} onClose={() => setOpen(false)} />
|
||
</>
|
||
)
|
||
}
|