fix: use CSS table layout for column alignment

Replaced flex containers with display: table/table-row/table-cell.
This guarantees column widths are shared between header and all rows,
fixing the misalignment. Subject column gets remaining width, all
others use fixed pixel widths. Resize handles on header cells still work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 22:06:12 +02:00
parent b2fb69ffc5
commit d7a5b5ba1d

View File

@@ -882,132 +882,143 @@ function TicketWorkbenchContent() {
</div>
) : (
<>
{/* Column header */}
<div className={cn(
"sticky top-0 z-10 flex border-b border-border bg-muted/50",
density === "compact" ? "min-h-7" : "min-h-8"
)}>
<div className="w-9 shrink-0" />
{availableColumns.filter((c) => c.visible).map((col) => (
<div
key={col.key}
className="relative flex shrink-0 items-center gap-1 border-r border-border/60 px-3 last:border-r-0"
style={{ width: col.key === "subject" ? undefined : col.width, flex: col.key === "subject" ? 1 : undefined, minWidth: col.key === "subject" ? 200 : undefined }}
>
<span className="text-[11px] font-semibold uppercase text-muted-foreground truncate">
{col.label}
</span>
{/* Resize handle */}
{/* Table layout for consistent column alignment */}
<div style={{ display: "table", width: "100%" }}>
{/* Column header */}
<div className={cn(
"sticky top-0 z-10 border-b border-border bg-muted/50",
density === "compact" ? "min-h-7" : "min-h-8"
)} style={{ display: "table-row" }}>
<div style={{ display: "table-cell", width: 36 }} />
{availableColumns.filter((c) => c.visible).map((col) => (
<div
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/40"
onMouseDown={(e) => handleColumnResize(col.key, e)}
/>
</div>
))}
<div className="w-12 shrink-0" />
</div>
{filteredTickets.map((ticket) => {
const selected = ticket.id === selectedId;
const ownerName = ticket.owner_id
? users.find((u) => u.id === ticket.owner_id)?.username ?? "assigned"
: null;
return (
<button
key={ticket.id}
type="button"
onClick={() => setSelectedId(ticket.id)}
onDoubleClick={() => router.push(`/tickets/${ticket.id}`)}
className={cn(
"flex items-center border-b border-border/80 text-left transition-colors",
density === "compact" ? "min-h-9" : "min-h-12",
selected
? "bg-accent/80 shadow-[inset_3px_0_0_var(--primary)]"
: "hover:bg-accent/45"
)}
>
{/* Checkbox */}
<span className="flex w-9 shrink-0 items-center justify-center" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={batchIds.has(ticket.id)}
onChange={() => toggleBatchId(ticket.id)}
className="h-3.5 w-3.5 rounded border-border accent-primary"
key={col.key}
className="relative border-r border-border/60 px-3 align-middle last:border-r-0"
style={{ display: "table-cell", width: col.key === "subject" ? undefined : col.width }}
>
<span className="text-[10px] font-semibold uppercase text-muted-foreground/60 truncate block">
{col.label}
</span>
<div
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/40"
onMouseDown={(e) => handleColumnResize(col.key, e)}
/>
</span>
{availableColumns.filter((c) => c.visible).map((col) => {
if (col.key.startsWith("cf.")) {
const cfKey = col.key.slice(3);
const cfValue = ticket.custom_fields?.find((v) => v.custom_field?.key === cfKey || v.custom_field?.name === cfKey)?.value;
return (
<span key={col.key} className="shrink-0 truncate px-3 text-sm text-foreground" style={{ width: col.width }}>
{cfValue ?? "—"}
</span>
);
}
switch (col.key) {
case "id":
</div>
))}
<div style={{ display: "table-cell", width: 48 }} />
</div>
{filteredTickets.map((ticket) => {
const selected = ticket.id === selectedId;
const ownerName = ticket.owner_id
? users.find((u) => u.id === ticket.owner_id)?.username ?? "assigned"
: null;
return (
<div
key={ticket.id}
role="button"
tabIndex={0}
onClick={() => setSelectedId(ticket.id)}
onDoubleClick={() => router.push(`/tickets/${ticket.id}`)}
onKeyDown={(e) => { if (e.key === "Enter") router.push(`/tickets/${ticket.id}`); }}
className={cn(
"cursor-pointer border-b border-border/80",
density === "compact" ? "" : "",
selected
? "bg-accent/80 shadow-[inset_3px_0_0_var(--primary)]"
: "hover:bg-accent/45"
)}
style={{ display: "table-row" }}
>
<div className="flex items-center justify-center" style={{ display: "table-cell", width: 36, verticalAlign: "middle" }} onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={batchIds.has(ticket.id)}
onChange={() => toggleBatchId(ticket.id)}
className="h-3.5 w-3.5 rounded border-border accent-primary"
/>
</div>
{availableColumns.filter((c) => c.visible).map((col) => {
const cellStyle = {
display: "table-cell" as const,
width: col.key === "subject" ? undefined : col.width,
verticalAlign: "middle" as const,
padding: density === "compact" ? "4px 12px" : "8px 12px",
};
if (col.key.startsWith("cf.")) {
const cfKey = col.key.slice(3);
const cfValue = ticket.custom_fields?.find((v) => v.custom_field?.key === cfKey || v.custom_field?.name === cfKey)?.value;
return (
<span key={col.key} className="shrink-0 px-3 font-mono text-xs font-semibold text-muted-foreground" style={{ width: col.width }}>
{formatTicketId(ticket.id)}
</span>
<div key={col.key} className="truncate text-sm text-foreground" style={cellStyle}>
{cfValue ?? "—"}
</div>
);
case "subject":
return (
<span key={col.key} className="min-w-0 flex-1 px-3" style={{ minWidth: 200 }}>
<span className="block truncate text-sm font-semibold text-foreground">
{ticket.subject}
</span>
{density === "comfortable" && (
<span className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
{ownerName ?? "Unassigned"}
<span className="h-1 w-1 rounded-full bg-border" />
Created {relativeTime(ticket.created_at)}
}
switch (col.key) {
case "id":
return (
<div key={col.key} className="font-mono text-xs font-semibold text-muted-foreground" style={cellStyle}>
{formatTicketId(ticket.id)}
</div>
);
case "subject":
return (
<div key={col.key} className="min-w-[200px]" style={cellStyle}>
<span className="block truncate text-sm font-semibold text-foreground">
{ticket.subject}
</span>
)}
</span>
);
case "status":
return (
<span key={col.key} className="shrink-0 px-3" style={{ width: col.width }}>
<TicketStatusBadge status={ticket.status} />
</span>
);
case "queue":
return (
<span key={col.key} className="shrink-0 truncate px-3 text-sm font-medium text-muted-foreground" style={{ width: col.width }}>
{queueName(queues, ticket.queue_id)}
</span>
);
case "owner":
return (
<span key={col.key} className="shrink-0 truncate px-3 text-sm text-foreground" style={{ width: col.width }}>
{ownerName ?? "—"}
</span>
);
case "created":
return (
<span key={col.key} className="shrink-0 px-3 text-xs text-muted-foreground" style={{ width: col.width }}>
{relativeTime(ticket.created_at)}
</span>
);
case "updated":
return (
<span key={col.key} className="shrink-0 px-3 text-xs text-muted-foreground" style={{ width: col.width }}>
{relativeTime(ticket.updated_at)}
</span>
);
default:
return <span key={col.key} className="shrink-0 px-3" style={{ width: col.width }} />;
}
})}
<span className="flex w-12 shrink-0 justify-end px-2 text-muted-foreground">
<ChevronRightIcon className="h-4 w-4" />
</span>
</button>
);
})}
{density === "comfortable" && (
<span className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
{ownerName ?? "Unassigned"}
<span className="h-1 w-1 rounded-full bg-border" />
Created {relativeTime(ticket.created_at)}
</span>
)}
</div>
);
case "status":
return (
<div key={col.key} style={cellStyle}>
<TicketStatusBadge status={ticket.status} />
</div>
);
case "queue":
return (
<div key={col.key} className="truncate text-sm font-medium text-muted-foreground" style={cellStyle}>
{queueName(queues, ticket.queue_id)}
</div>
);
case "owner":
return (
<div key={col.key} className="truncate text-sm text-foreground" style={cellStyle}>
{ownerName ?? "—"}
</div>
);
case "created":
return (
<div key={col.key} className="text-xs text-muted-foreground" style={cellStyle}>
{relativeTime(ticket.created_at)}
</div>
);
case "updated":
return (
<div key={col.key} className="text-xs text-muted-foreground" style={cellStyle}>
{relativeTime(ticket.updated_at)}
</div>
);
default:
return <div key={col.key} style={cellStyle} />;
}
})}
<div className="flex justify-end px-2 text-muted-foreground" style={{ display: "table-cell", width: 48, verticalAlign: "middle" }}>
<ChevronRightIcon className="h-4 w-4" />
</div>
</div>
);
})}
</div>
</>
)}
</div>