frontend: fix pagination styling, patient-sheet header, message avatars

- Patients pagination: render the prev/next + page-number controls as COSS
  Buttons inside the Pagination structure. PaginationLink drops its
  buttonVariants styling when given a `render` prop, so the controls were
  unstyled and wrapping; using Button restores proper pill/number buttons.
- Patient detail sheet: move the action buttons (Download summary / Transfer /
  Edit / Delete) to their own wrapping row beneath the identity block so the
  patient name is no longer truncated on the narrow sheet.
- Messages thread: add sender/recipient avatars (MessageAvatar) at the bottom
  of each message group, with a spacer to keep stacked bubbles aligned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-27 22:16:12 +03:00
parent 15bf5653b4
commit 31adbab87b
3 changed files with 63 additions and 47 deletions
@@ -42,6 +42,7 @@ import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Button } from "@/components/ui/button";
import {
Message,
MessageAvatar,
MessageContent,
MessageFooter,
MessageHeader,
@@ -200,6 +201,7 @@ export function MessagesView() {
const { t } = useTranslation();
const { data: session } = authClient.useSession();
const myId = session?.user?.id ?? "";
const myInitials = initials(session?.user?.name ?? "");
const router = useRouter();
@@ -694,6 +696,19 @@ export function MessagesView() {
!newDay && (startsGroup ? "mt-4" : "mt-1"),
)}
>
{/* Avatar at the bottom of each run (messenger-style); a
spacer keeps stacked bubbles aligned otherwise. */}
{endsGroup ? (
<MessageAvatar>
<Avatar className="size-8">
<AvatarFallback className="text-[11px]">
{out ? myInitials : initials(m.senderName)}
</AvatarFallback>
</Avatar>
</MessageAvatar>
) : (
<div className="w-8 shrink-0" />
)}
<MessageContent>
{selected.isGroup && !out && startsGroup && (
<MessageHeader>{m.senderName}</MessageHeader>
+28 -23
View File
@@ -238,31 +238,35 @@ export function PatientDetail({
return (
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3">
<Avatar className="size-12">
<AvatarFallback>{patient.initials}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex flex-wrap items-center gap-2">
<span className="truncate font-semibold text-base text-foreground">
{patient.name}
</span>
<Badge variant={statusVariant[patient.status]}>
{t(`patients.status.${patient.status}`)}
</Badge>
</div>
<span className="text-muted-foreground text-sm">{idLine}</span>
{patient.alerts.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1.5">
{patient.alerts.map((alert) => (
<Badge key={alert} variant="outline">
{alert}
</Badge>
))}
<div className="flex flex-col gap-3">
{/* Identity — full width so the name never gets squeezed by the actions. */}
<div className="flex items-start gap-3">
<Avatar className="size-12">
<AvatarFallback>{patient.initials}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-semibold text-base text-foreground">
{patient.name}
</span>
<Badge variant={statusVariant[patient.status]}>
{t(`patients.status.${patient.status}`)}
</Badge>
</div>
)}
<span className="text-muted-foreground text-sm">{idLine}</span>
{patient.alerts.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1.5">
{patient.alerts.map((alert) => (
<Badge key={alert} variant="outline">
{alert}
</Badge>
))}
</div>
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
{/* Actions — their own wrapping row beneath the identity. */}
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={() => printPatientSummary(patient, t)}
size="sm"
@@ -292,6 +296,7 @@ export function PatientDetail({
{onDelete && (
<Button
aria-label={t("patients.delete.action")}
className="ml-auto"
onClick={onDelete}
size="sm"
type="button"
+20 -24
View File
@@ -17,10 +17,8 @@ import {
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
} from "@/components/ui/pagination";
import { listPatients, type Patient } from "@/lib/patients";
import { cn } from "@/lib/utils";
// Rows shown per page on the patients table before paginating.
const PAGE_SIZE = 10;
@@ -290,22 +288,20 @@ export function PatientsView() {
>
<PaginationContent>
<PaginationItem>
<PaginationLink
aria-disabled={safePage === 1}
<Button
aria-label={t("patients.pagination.previous")}
className={cn(
"gap-1 px-2.5",
safePage === 1 && "pointer-events-none opacity-50"
)}
className="gap-1"
disabled={safePage === 1}
onClick={() => setPage(Math.max(1, safePage - 1))}
render={<button type="button" />}
size="default"
size="sm"
type="button"
variant="ghost"
>
<ChevronLeft className="size-4" />
<span className="max-sm:hidden">
{t("patients.pagination.previous")}
</span>
</PaginationLink>
</Button>
</PaginationItem>
{pageWindow(safePage, totalPages).map((p, i) =>
p === null ? (
@@ -314,34 +310,34 @@ export function PatientsView() {
</PaginationItem>
) : (
<PaginationItem key={p}>
<PaginationLink
<Button
aria-current={p === safePage ? "page" : undefined}
aria-label={t("patients.pagination.page", { page: p })}
isActive={p === safePage}
onClick={() => setPage(p)}
render={<button type="button" />}
size="icon-sm"
type="button"
variant={p === safePage ? "outline" : "ghost"}
>
{p}
</PaginationLink>
</Button>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationLink
aria-disabled={safePage === totalPages}
<Button
aria-label={t("patients.pagination.next")}
className={cn(
"gap-1 px-2.5",
safePage === totalPages && "pointer-events-none opacity-50"
)}
className="gap-1"
disabled={safePage === totalPages}
onClick={() => setPage(Math.min(totalPages, safePage + 1))}
render={<button type="button" />}
size="default"
size="sm"
type="button"
variant="ghost"
>
<span className="max-sm:hidden">
{t("patients.pagination.next")}
</span>
<ChevronRight className="size-4" />
</PaginationLink>
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>