feat(editor): match redesign prototype layout — TopBar title + tab counts + InfoPanel actions
Per /Users/zimin/Downloads/redesign/screens/03-editor-records.png редизайн
prototype editor layout отличается от current MVP:
1. TopBar absorbs editor title + version (was в PageHeader)
2. PageHeader удалён — title и breadcrumb теперь только в TopBar
3. Tab labels получили counts (Записи 127, Связи 6, Поля 12)
4. Action buttons распределены per role:
- Time-travel + AOI → InfoPanel bottom (close к data context)
- Schema edit + +Запись → tab toolbar right (close к tab actions)
Changes:
src/components/layout/TopBar.tsx
- Breadcrumb получил subtitle slot — mono '<name> · v<version>'
- useBreadcrumb fetches dict detail для editor route '/dictionaries/$name'
- Render: '← Справочники / Космические аппараты satellites · v1.0.0'
src/api/queries.ts
- useDictionaryDetail принимает name: string | undefined + enabled flag
- Guard для editor breadcrumb когда route не matches dict
src/routes/dictionaries.$name.tsx
- Removed <PageHeader> — title/breadcrumb теперь в TopBar
- EditorInfoPanel получает actions prop:
onTimeTravel + timeTravelActive + onAoi + aoiActive
- EditorTabBar получает counts (per-tab badge) + actions slot
(right-aligned Schema + +Запись buttons)
- extractOutgoingFkCount helper для relations count
src/components/editor/EditorInfoPanel.tsx
- actions prop с Time-travel + AOI buttons
- Rendered в panel выше Relations section, между metadata и FK list
- aria-pressed когда active (time-travel или AOI с filter)
src/components/editor/EditorTabBar (inline in route)
- counts prop: Partial<Record<EditorTab, number>>
- actions prop: ReactNode для right-aligned cluster
- inline count badge — text-mono text-mute next to label
Tests: 116 pass. TS strict clean.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ClockCounterClockwiseIcon, MapTrifoldIcon } from '@phosphor-icons/react'
|
||||
import { Badge } from '@/ui'
|
||||
import { useDictionaryDependents } from '@/api/queries'
|
||||
import type { DictionaryDetail } from '@/api/client'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* EditorInfoPanel — left rail с dict metadata per handoff prototype Screen 2.
|
||||
@@ -23,9 +25,17 @@ import type { DictionaryDetail } from '@/api/client'
|
||||
export type EditorInfoPanelProps = {
|
||||
detail: DictionaryDetail
|
||||
recordCount: number
|
||||
/** Optional action buttons (Time-travel / AOI filter) — bottom of panel
|
||||
* per redesign prototype. Toggle handlers come from route's state. */
|
||||
actions?: {
|
||||
onTimeTravel?: () => void
|
||||
timeTravelActive?: boolean
|
||||
onAoi?: () => void
|
||||
aoiActive?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export function EditorInfoPanel({ detail, recordCount }: EditorInfoPanelProps) {
|
||||
export function EditorInfoPanel({ detail, recordCount, actions }: EditorInfoPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
const { data: refByRaw } = useDictionaryDependents(detail.name)
|
||||
|
||||
@@ -64,6 +74,44 @@ export function EditorInfoPanel({ detail, recordCount }: EditorInfoPanelProps) {
|
||||
</MetaCell>
|
||||
</div>
|
||||
|
||||
{/* Actions — bottom buttons per redesign prototype: Time-travel + AOI */}
|
||||
{actions && (
|
||||
<div className="space-y-2 pt-2 border-t border-line-2">
|
||||
{actions.onTimeTravel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={actions.onTimeTravel}
|
||||
aria-pressed={actions.timeTravelActive}
|
||||
className={cn(
|
||||
'w-full h-8 rounded-md border text-cell flex items-center justify-center gap-1.5 transition-colors',
|
||||
actions.timeTravelActive
|
||||
? 'border-accent bg-accent-bg text-accent'
|
||||
: 'border-line bg-surface text-ink-2 hover:border-line-2 hover:bg-surface-2',
|
||||
)}
|
||||
>
|
||||
<ClockCounterClockwiseIcon weight="regular" size={14} />
|
||||
{t('timeTravel.button', { defaultValue: 'Time-travel…' })}
|
||||
</button>
|
||||
)}
|
||||
{actions.onAoi && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={actions.onAoi}
|
||||
aria-pressed={actions.aoiActive}
|
||||
className={cn(
|
||||
'w-full h-8 rounded-md border text-cell flex items-center justify-center gap-1.5 transition-colors',
|
||||
actions.aoiActive
|
||||
? 'border-accent bg-accent-bg text-accent'
|
||||
: 'border-line bg-surface text-ink-2 hover:border-line-2 hover:bg-surface-2',
|
||||
)}
|
||||
>
|
||||
<MapTrifoldIcon weight="regular" size={14} />
|
||||
{t('aoi.button', { defaultValue: 'AOI фильтр…' })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Relations */}
|
||||
{(outgoingNames.length > 0 || incomingNames.length > 0) && (
|
||||
<Section
|
||||
|
||||
Reference in New Issue
Block a user