refactor: Restructure document card layout for better visual hierarchy

PROBLEM:
- Document cards had unstructured layout with all elements mixed together
- No clear visual separation between sections
- Poor readability and cluttered appearance
- Actions scattered without logical grouping

SOLUTION:
Completely restructured document cards into 3 distinct sections:

1. HEADER (with light background and border-bottom)
   - Checkbox + Title (h3 with semibold)
   - Status badges (primary + acknowledgement)
   - Quick action buttons (Preview/PDF + Details) on the right
   - Improved: Buttons now have solid backgrounds (bg-sky-100, bg-gray-100) for better visibility

2. BODY (main content area)
   - Description (only if exists, with better line-height)
   - Metadata grid (3 columns on large screens):
     • Folder location (sky background with FolderOpen icon)
     • Author (gray background with User icon)
     • Created date (gray background with Calendar icon)
   - Workflow actions (only for draft/review/approved/rejected states)
   - Better spacing and visual hierarchy

3. FOOTER (conditional, only when needed)
   - Shows only if document has acknowledgement requirement OR is published/archived
   - Acknowledgement actions (Ознакомиться + Ведомость buttons)
   - Workflow for published/archived states (aligned right)
   - Light background matching header

IMPROVEMENTS:
- Clear visual sections with borders and backgrounds
- Metadata in colored pill-shaped cards instead of plain text
- Icons for all metadata fields (folder, user, calendar)
- Removed redundant 'Updated' field (not essential)
- Removed empty description text ('Описание не заполнено')
- Better button hierarchy: primary actions in header, secondary in footer
- Larger, more clickable buttons (px-3 py-1.5 instead of px-2.5 py-1)
- Consistent rounded-lg instead of mix of rounded-full/rounded-lg
- Better hover states (bg-sky-200 instead of bg-sky-100)
- Conditional rendering: footer only appears when necessary

VISUAL DESIGN:
- Header: bg-gray-50/50 with border-bottom
- Body: white background, clean content area
- Footer: bg-gray-50/50 with border-top
- Metadata cards: colored backgrounds (sky-50 for folder, gray-50 for others)
- Better border colors: border-gray-200 instead of border-gray-100

This matches the structure of modern DMS systems like Notion, Confluence, and Google Drive where cards have clear header/body/footer separation.
This commit is contained in:
igor_nadein 2026-02-28 14:15:28 +03:00
parent 11acea9ad0
commit 243fffc126

View File

@ -499,26 +499,28 @@ export default function DocumentsPage() {
return (
<article
key={doc.id}
className={`rounded-xl border p-4 transition ${
className={`rounded-xl border transition ${
isSelected
? "border-sky-300 bg-sky-50"
: "border-gray-100 bg-white hover:bg-gray-50"
? "border-sky-300 bg-sky-50/50"
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
}`}
>
<div className="flex items-start gap-3">
{/* Header */}
<div className="flex items-start gap-3 border-b border-gray-100 bg-gray-50/50 px-4 py-3">
{/* Checkbox */}
<input
type="checkbox"
checked={isSelected}
onChange={() => selection.toggleDocument(doc.id)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/>
{/* Title & Status */}
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2">
<p className="text-sm font-semibold text-gray-900">
{doc.title}
</p>
<h3 className="mb-1.5 text-sm font-semibold text-gray-900">
{doc.title}
</h3>
<div className="flex flex-wrap items-center gap-2">
<DocumentStatusBadge
status={doc.status}
statusCode={doc.status_code}
@ -526,46 +528,22 @@ export default function DocumentsPage() {
{/* Acknowledgement Badge */}
{doc.acknowledgement_required && (
doc.is_acknowledged ? (
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20">
<CheckCircle size={12} />
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-green-600/20">
<CheckCircle size={10} />
Ознакомлен
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-600/20">
<AlertCircle size={12} />
<span className="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-amber-600/20">
<AlertCircle size={10} />
Требуется ознакомление
</span>
)
)}
</div>
<p className="mb-3 text-sm text-gray-700">
{doc.description || "Описание не заполнено"}
</p>
{/* Workflow Buttons */}
<div className="mb-3">
<DocumentWorkflowButtons
documentId={doc.id}
currentStatus={doc.status_code}
onStatusChange={loadDocuments}
/>
</div>
<div className="grid grid-cols-1 gap-2 text-xs text-gray-500 sm:grid-cols-2">
{doc.folder_path && (
<p className="flex items-center gap-1 text-sky-600">
<FolderOpen size={12} />
{doc.folder_path}
</p>
)}
<p>Автор: {authorName}</p>
<p>Создано: {formatDate(doc.created_at)}</p>
<p>Обновлено: {formatDate(doc.updated_at)}</p>
</div>
</div>
<div className="flex shrink-0 flex-wrap gap-2">
{/* Quick Actions - Right Side */}
<div className="flex shrink-0 gap-1.5">
{doc.file_url && (
<>
{doc.file_name?.toLowerCase().endsWith(".pdf") ? (
@ -576,9 +554,10 @@ export default function DocumentsPage() {
name: doc.file_name || doc.title,
})
}
className="inline-flex items-center gap-1 rounded-full bg-sky-50 px-2.5 py-1 text-xs text-sky-700 ring-1 ring-sky-100 hover:bg-sky-100"
className="inline-flex items-center gap-1.5 rounded-lg bg-sky-100 px-3 py-1.5 text-xs font-medium text-sky-700 transition hover:bg-sky-200"
title="Открыть PDF"
>
<Eye size={12} />
<Eye size={14} />
PDF
</button>
) : (
@ -589,25 +568,82 @@ export default function DocumentsPage() {
name: doc.file_name || doc.title,
})
}
className="inline-flex items-center gap-1 rounded-full bg-sky-50 px-2.5 py-1 text-xs text-sky-700 ring-1 ring-sky-100 hover:bg-sky-100"
className="inline-flex items-center gap-1.5 rounded-lg bg-sky-100 px-3 py-1.5 text-xs font-medium text-sky-700 transition hover:bg-sky-200"
title="Просмотреть файл"
>
<Eye size={12} />
<Eye size={14} />
Просмотр
</button>
)}
</>
)}
{/* Acknowledge Button - for documents requiring acknowledgement */}
<button
onClick={() => setSelectedDocument(doc)}
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-700 transition hover:bg-gray-200"
title="Подробная информация"
>
<FileText size={14} />
Детали
</button>
</div>
</div>
{/* Body */}
<div className="px-4 py-3">
{/* Description */}
{doc.description && (
<p className="mb-3 text-sm leading-relaxed text-gray-700">
{doc.description}
</p>
)}
{/* Metadata Grid */}
<div className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{/* Folder */}
{doc.folder_path && (
<div className="flex items-center gap-2 rounded-lg bg-sky-50 px-3 py-2">
<FolderOpen size={14} className="shrink-0 text-sky-600" />
<span className="truncate text-xs text-sky-700">{doc.folder_path}</span>
</div>
)}
{/* Author */}
<div className="flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-2">
<User size={14} className="shrink-0 text-gray-500" />
<span className="truncate text-xs text-gray-700">{authorName}</span>
</div>
{/* Created */}
<div className="flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-2">
<Calendar size={14} className="shrink-0 text-gray-500" />
<span className="text-xs text-gray-700">{formatDate(doc.created_at)}</span>
</div>
</div>
{/* Workflow Actions */}
{doc.status_code !== 'published' && doc.status_code !== 'archived' && (
<div className="mb-3">
<DocumentWorkflowButtons
documentId={doc.id}
currentStatus={doc.status_code}
onStatusChange={loadDocuments}
/>
</div>
)}
</div>
{/* Footer - только если есть дополнительные действия */}
{(doc.acknowledgement_required || doc.status_code === 'published' || doc.status_code === 'archived') && (
<div className="flex flex-wrap items-center gap-2 border-t border-gray-100 bg-gray-50/50 px-4 py-2.5">
{/* Acknowledge Button */}
{doc.acknowledgement_required && !doc.is_acknowledged && (
<button
onClick={() => setSelectedDocument(doc)}
className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800 ring-1 ring-amber-300 hover:bg-amber-200"
className="inline-flex items-center gap-1.5 rounded-lg bg-amber-100 px-3 py-1.5 text-xs font-medium text-amber-800 ring-1 ring-amber-300 transition hover:bg-amber-200"
>
<CheckCircle size={12} />
<CheckCircle size={14} />
Ознакомиться
</button>
)}
{/* Acknowledgements Report Button - for authors/admins */}
{/* Acknowledgements Report */}
{doc.acknowledgement_required && (
<button
onClick={() =>
@ -616,20 +652,25 @@ export default function DocumentsPage() {
documentTitle: doc.title,
})
}
className="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2.5 py-1 text-xs text-blue-700 ring-1 ring-blue-200 hover:bg-blue-100"
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-100 px-3 py-1.5 text-xs font-medium text-blue-700 ring-1 ring-blue-200 transition hover:bg-blue-200"
title="Посмотреть кто ознакомился"
>
<Users size={14} />
Ведомость
</button>
)}
<button
onClick={() => setSelectedDocument(doc)}
className="inline-flex items-center gap-1 rounded-full bg-gray-50 px-2.5 py-1 text-xs text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100"
>
Детали
</button>
{/* Workflow for published/archived */}
{(doc.status_code === 'published' || doc.status_code === 'archived') && (
<div className="ml-auto">
<DocumentWorkflowButtons
documentId={doc.id}
currentStatus={doc.status_code}
onStatusChange={loadDocuments}
/>
</div>
)}
</div>
</div>
)}
</article>
);
})}