Merge branch 'feat/row-click-and-wide-rail' into 'main'

feat(ui): row click → edit drawer + wide-mode left section rail

See merge request 2-6/2-6-4/terravault/ordinis!105
This commit is contained in:
Александр Зимин
2026-05-11 20:51:42 +00:00
2 changed files with 80 additions and 23 deletions
@@ -276,7 +276,9 @@ export const SchemaDrivenForm = ({
const showAllSections = layout === 'sections'
const sectionVisible = (id: TabId) => showAllSections || activeTab === id
// Scroll-to-section для chip click (только в sections mode).
// Scroll-to-section для chip click + rail click. Использует form container
// (scrollable parent = drawer body) вместо window — section header sticky
// считается от form container coordinate space.
const scrollToSection = (id: TabId) => {
document.getElementById(`form-section-${formId ?? 'default'}-${id}`)?.scrollIntoView({
behavior: 'smooth',
@@ -284,14 +286,27 @@ export const SchemaDrivenForm = ({
})
}
return (
<form
id={formId}
onSubmit={handleSubmit(submit)}
className="space-y-4"
>
{/* Section chips strip per handoff (sections mode) OR tab bar (legacy). */}
{showAllSections ? (
// Wide-mode left rail: detection через ResizeObserver. Когда form container
// ≥720px (drawer wide mode = 880px) — показываем 180px nav rail слева
// с section list + counts. Иначе chip strip на top sufficient. Container-
// based вместо viewport-based чтобы narrow drawer на wide viewport не
// рендерил rail (drawer = 520px не fits).
const formContainerRef = useRef<HTMLFormElement>(null)
const [showRail, setShowRail] = useState(false)
useEffect(() => {
if (!showAllSections || !formContainerRef.current) return
const node = formContainerRef.current
const ro = new ResizeObserver((entries) => {
const width = entries[0]?.contentRect.width ?? 0
setShowRail(width >= 720)
})
ro.observe(node)
return () => ro.disconnect()
}, [showAllSections])
// Body content (sections + form fields) — рендерится либо напрямую,
// либо в grid 180px+1fr когда showRail (wide mode).
const sectionsChipStrip = showAllSections ? (
<div className="flex flex-wrap items-center gap-1.5 sticky top-0 bg-surface pb-2 -mt-1 z-10">
{visibleTabs.map((tab) => (
<button
@@ -307,7 +322,40 @@ export const SchemaDrivenForm = ({
</button>
))}
</div>
) : (
) : null
// Left rail для wide mode — sticky nav с section list + per-section
// field counts. Click → scrollToSection (smooth scroll к anchor).
const leftRail = showRail && showAllSections ? (
<aside className="self-start sticky top-0 pt-0.5">
<nav className="flex flex-col gap-0.5">
{visibleTabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => scrollToSection(tab.id as TabId)}
className="flex items-center justify-between gap-2 px-2.5 py-1.5 rounded-sm hover:bg-surface-2 text-body text-ink-2 hover:text-ink text-left transition-colors"
>
<span className="truncate">{tab.label}</span>
<span className="text-mono text-cell text-mute shrink-0">
{filteredBuckets[tab.id as TabId].length}
</span>
</button>
))}
</nav>
</aside>
) : null
return (
<form
id={formId}
onSubmit={handleSubmit(submit)}
ref={formContainerRef}
className={showRail ? 'grid grid-cols-[180px_1fr] gap-6' : ''}
>
{leftRail}
<div className="space-y-4 min-w-0">
{showAllSections ? sectionsChipStrip : (
<Tabs items={visibleTabs} value={activeTab} onValueChange={(id) => setActiveTab(id as TabId)} />
)}
@@ -448,6 +496,7 @@ export const SchemaDrivenForm = ({
</Button>
</FormActions>
)}
</div>
</form>
)
}
@@ -905,10 +905,18 @@ function DictionaryDetail() {
<TableRow
key={r.id}
data-selected={selection.has(r.businessKey)}
className={rowCursor === idx ? 'bg-accent-bg/40 outline outline-1 outline-accent/30' : undefined}
// Row-wide click → open edit drawer (только для canMutate).
// Anonymous users — read-only, click no-op чтобы не путать.
// Bubbling: nested buttons (checkbox, action icons) делают
// stopPropagation чтобы они не открывали drawer случайно.
onClick={canMutate ? () => setEdit({ kind: 'edit', record: r }) : undefined}
className={[
canMutate ? 'cursor-pointer' : '',
rowCursor === idx ? 'bg-accent-bg/40 outline outline-1 outline-accent/30' : '',
].filter(Boolean).join(' ') || undefined}
>
{canMutate && (
<TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<Checkbox
aria-label={`${t('dict.bulk.selectRow')} ${r.businessKey}`}
checked={selection.has(r.businessKey)}
@@ -967,7 +975,7 @@ function DictionaryDetail() {
{new Date(r.validFrom).toLocaleDateString()}
</span>
</TableCell>
<TableCell align="right">
<TableCell align="right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1">
<IconButton
label={t('history.title')}