Merge branch 'feat/catalog-relative-time-flat-sidebar' into 'main'

feat(layout): catalog relative time + chevron + flat sidebar per redesign (4)

See merge request 2-6/2-6-4/terravault/ordinis!78
This commit is contained in:
Александр Зимин
2026-05-11 16:27:18 +00:00
2 changed files with 31 additions and 36 deletions
@@ -10,7 +10,6 @@ import {
Webhook, Webhook,
ScrollText, ScrollText,
Search, Search,
GitBranch,
type LucideIcon, type LucideIcon,
} from 'lucide-react' } from 'lucide-react'
import { useAuth } from 'react-oidc-context' import { useAuth } from 'react-oidc-context'
@@ -63,6 +62,9 @@ export function Sidebar() {
// sidebar'а — overkill. Skip badge. // sidebar'а — overkill. Skip badge.
const reviewsCount = 0 const reviewsCount = 0
// Flat list per redesign/compact.html — no section labels, /graph removed
// (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная →
// Справочники → Мои черновики → На ревью → Outbox → Webhooks → Аудит → Поиск.
const sections: NavSection[] = [ const sections: NavSection[] = [
{ {
items: [ items: [
@@ -73,14 +75,6 @@ export function Sidebar() {
icon: Database, icon: Database,
badge: dictsCount > 0 ? dictsCount : undefined, badge: dictsCount > 0 ? dictsCount : undefined,
}, },
{ to: '/search', label: t('nav.search'), icon: Search },
// Graph view — Stage 3.3 placeholder. Когда d3-force готов.
{ to: '/graph', label: t('nav.graph'), icon: GitBranch },
],
},
{
label: t('nav.section.workflow'),
items: [
{ {
to: '/my-drafts', to: '/my-drafts',
label: t('nav.myDrafts'), label: t('nav.myDrafts'),
@@ -95,14 +89,10 @@ export function Sidebar() {
badge: reviewsCount > 0 ? reviewsCount : undefined, badge: reviewsCount > 0 ? reviewsCount : undefined,
authRequired: true, authRequired: true,
}, },
],
},
{
label: t('nav.section.admin'),
items: [
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
{ to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true }, { to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true },
{ to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true }, { to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true },
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
{ to: '/search', label: t('nav.search'), icon: Search },
], ],
}, },
] ]
@@ -309,6 +299,7 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
const myDraftsCount = myDrafts.data?.totalElements ?? 0 const myDraftsCount = myDrafts.data?.totalElements ?? 0
const reviewsCount = 0 const reviewsCount = 0
// Flat list per redesign — same as desktop Sidebar above.
const sections: NavSection[] = [ const sections: NavSection[] = [
{ {
items: [ items: [
@@ -319,13 +310,6 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
icon: Database, icon: Database,
badge: dictsCount > 0 ? dictsCount : undefined, badge: dictsCount > 0 ? dictsCount : undefined,
}, },
{ to: '/search', label: t('nav.search'), icon: Search },
{ to: '/graph', label: t('nav.graph'), icon: GitBranch },
],
},
{
label: t('nav.section.workflow'),
items: [
{ {
to: '/my-drafts', to: '/my-drafts',
label: t('nav.myDrafts'), label: t('nav.myDrafts'),
@@ -340,14 +324,10 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
badge: reviewsCount > 0 ? reviewsCount : undefined, badge: reviewsCount > 0 ? reviewsCount : undefined,
authRequired: true, authRequired: true,
}, },
],
},
{
label: t('nav.section.admin'),
items: [
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
{ to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true }, { to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true },
{ to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true }, { to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true },
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
{ to: '/search', label: t('nav.search'), icon: Search },
], ],
}, },
] ]
@@ -423,6 +423,8 @@ function DictionaryListTable({ rows }: { rows: DictionaryDefinition[] }) {
<th scope="col" className="text-cap text-mute text-left px-3 py-2 hidden xl:table-cell"> <th scope="col" className="text-cap text-mute text-left px-3 py-2 hidden xl:table-cell">
{t('dict.col.updated', { defaultValue: 'изменён' })} {t('dict.col.updated', { defaultValue: 'изменён' })}
</th> </th>
{/* Chevron col — affordance что row кликабелен (открывает editor) */}
<th scope="col" aria-hidden className="w-8" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -441,15 +443,24 @@ function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) {
const refBy = useMemo(() => uniqueRefBy(refByRaw ?? []), [refByRaw]) const refBy = useMemo(() => uniqueRefBy(refByRaw ?? []), [refByRaw])
const incomingCount = refBy.length const incomingCount = refBy.length
// Relative time per redesign prototype: 12мин / 2ч / 3д / 1мес.
// Stale absolute date (YYYY-MM-DD) скрывал scale (5 минут vs 3 месяца —
// в формате 05/06/2026 это одинаково "далёкая дата").
const updatedLabel = useMemo(() => { const updatedLabel = useMemo(() => {
const date = new Date(d.updatedAt) const date = new Date(d.updatedAt)
return isNaN(date.getTime()) if (isNaN(date.getTime())) return '—'
? '—' const diffSec = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000))
: date.toLocaleDateString(undefined, { if (diffSec < 60) return `${diffSec}с`
year: 'numeric', const diffMin = Math.floor(diffSec / 60)
month: '2-digit', if (diffMin < 60) return `${diffMin}мин`
day: '2-digit', const diffH = Math.floor(diffMin / 60)
}) if (diffH < 24) return `${diffH}ч`
const diffD = Math.floor(diffH / 24)
if (diffD < 30) return `${diffD}д`
const diffMo = Math.floor(diffD / 30)
if (diffMo < 12) return `${diffMo}мес`
const diffY = Math.floor(diffMo / 12)
return `${diffY}г`
}, [d.updatedAt]) }, [d.updatedAt])
const handleClick = () => { const handleClick = () => {
@@ -509,10 +520,14 @@ function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) {
<td className="px-3 py-2 text-right text-mono text-mute hidden lg:table-cell"> <td className="px-3 py-2 text-right text-mono text-mute hidden lg:table-cell">
{incomingCount > 0 ? incomingCount : '—'} {incomingCount > 0 ? incomingCount : '—'}
</td> </td>
{/* updated */} {/* updated — relative time */}
<td className="px-3 py-2 text-mono text-mute tabular-nums hidden xl:table-cell whitespace-nowrap"> <td className="px-3 py-2 text-mono text-mute tabular-nums hidden xl:table-cell whitespace-nowrap">
{updatedLabel} {updatedLabel}
</td> </td>
{/* Chevron — кликабельность row affordance */}
<td aria-hidden className="px-2 py-2 text-mute text-mono text-right">
</td>
</tr> </tr>
) )
} }