redesign: ticket detail sidebar — flat, modern, no boxed sections

- Status selector: colored capsule with ring inset (visual, prominent)
- Assignment: simple label+select, no bordered dl/dt/dd grid
- Details: clean justify-between lines, no box container
- Custom fields: flat spacing, no bordered wrapper
- Removed PropertyRow component (no longer used)
- Removed heavy rounded-md border border-border bg-background/60 boxes

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

View File

@@ -747,34 +747,31 @@ export default function TicketDetailPage({
</footer>
</main>
<aside className="hidden min-h-0 overflow-y-auto border-l border-border bg-card/78 backdrop-blur xl:block">
<div className="space-y-5 p-5">
<aside className="hidden min-h-0 overflow-y-auto border-l border-border/50 bg-card/90 xl:block">
<div className="space-y-6 p-5">
{/* Status — prominent, visual */}
<section>
<div className="mb-2 flex items-center justify-between">
<h2 className="text-xs font-semibold uppercase text-muted-foreground">Status</h2>
<CircleIcon
className="h-3.5 w-3.5"
style={{ color: currentStatusColor }}
/>
</div>
<div className="relative">
<button
onClick={() => setStatusSelectOpen(!statusSelectOpen)}
className="flex w-full items-center gap-3 rounded-md border border-border bg-background/70 px-3 py-2.5 text-sm shadow-sm transition-colors hover:bg-accent"
className={cn(
"flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-all",
"ring-1 ring-inset",
)}
style={{
backgroundColor: `${currentStatusColor}12`,
color: currentStatusColor,
boxShadow: `inset 0 0 0 1px ${currentStatusColor}40`,
}}
type="button"
>
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: currentStatusColor }}
/>
<span className="flex-1 text-left font-semibold text-foreground">
{currentStatusLabel}
</span>
<ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: currentStatusColor }} />
<span className="flex-1 text-sm font-semibold">{currentStatusLabel}</span>
<ChevronDownIcon className="h-4 w-4 opacity-60" />
</button>
{statusSelectOpen && (
<div className="absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-xl">
<div className="absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-lg border border-border bg-popover shadow-lg">
{statusOptions.map((status) => {
const isCurrent = status === ticket.status;
return (
@@ -783,19 +780,14 @@ export default function TicketDetailPage({
onClick={() => handleStatusSelect(status)}
disabled={isCurrent}
className={cn(
"flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors",
isCurrent
? "bg-accent text-muted-foreground"
: "text-foreground hover:bg-accent"
"flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm transition-colors",
isCurrent ? "bg-accent/50 text-muted-foreground" : "text-foreground hover:bg-accent"
)}
type="button"
>
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: STATUS_COLORS[status] ?? STATUS_COLORS.new }}
/>
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: STATUS_COLORS[status] ?? STATUS_COLORS.new }} />
{statusLabel(status)}
{isCurrent && <span className="ml-auto text-xs">current</span>}
{isCurrent && <span className="ml-auto text-[10px]">current</span>}
</button>
);
})}
@@ -805,198 +797,124 @@ export default function TicketDetailPage({
</section>
{preview && (
<section className="rounded-md border border-border bg-background/70 p-3 shadow-sm">
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-foreground">
<BotIcon className="h-4 w-4 text-primary" />
Automation preview
<section className="rounded-lg border border-border bg-accent/20 p-3">
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
<BotIcon className="h-3.5 w-3.5 text-primary" /> Automation preview
</div>
<p className="text-xs text-muted-foreground">
Changing to{" "}
<span className="font-semibold text-foreground">
{pendingStatus ? statusLabel(pendingStatus) : ""}
</span>
<p className="mt-1 text-[11px] text-muted-foreground">
Changing to <span className="font-semibold text-foreground">{pendingStatus ? statusLabel(pendingStatus) : ""}</span>
</p>
<div className="my-3 space-y-1.5">
{preview.prepared_scrips.length > 0 ? (
preview.prepared_scrips.map((scrip) => (
<div
key={scrip.scripId}
className="flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs text-foreground"
>
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
{scrip.scripName}
<div className="my-2 space-y-1">
{preview.prepared_scrips.length > 0
? preview.prepared_scrips.map((scrip) => (
<div key={scrip.scripId} className="flex items-center gap-2 text-[11px] text-foreground">
<span className="h-1.5 w-1.5 rounded-full bg-primary" /> {scrip.scripName}
</div>
))
) : (
<p className="rounded border border-border bg-muted/40 px-2 py-1.5 text-xs text-muted-foreground">
No scrips will fire
</p>
)}
: <p className="text-[11px] text-muted-foreground">No scrips will fire</p>}
</div>
<div className="flex items-center gap-2">
<button
onClick={handleApplyStatus}
disabled={applyLoading}
className="rounded-md bg-primary px-2.5 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
type="button"
>
{applyLoading ? "Applying..." : "Apply change"}
</button>
<button
onClick={handleCancelStatus}
disabled={applyLoading}
className="rounded-md px-2.5 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
type="button"
>
Cancel
<div className="mt-2 flex gap-2">
<button onClick={handleApplyStatus} disabled={applyLoading} className="rounded-md bg-primary px-2.5 py-1 text-[11px] font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50" type="button">
{applyLoading ? "Applying..." : "Apply"}
</button>
<button onClick={handleCancelStatus} disabled={applyLoading} className="rounded-md px-2.5 py-1 text-[11px] font-medium text-muted-foreground hover:bg-accent hover:text-foreground" type="button">Cancel</button>
</div>
</section>
)}
{previewError && (
<div className="rounded-md border border-destructive/20 bg-destructive/10 p-3">
<p className="text-xs text-destructive">{previewError}</p>
</div>
)}
{previewError && <p className="text-xs text-destructive">{previewError}</p>}
{scripResults && (
<section className="rounded-md border border-border bg-background/70 p-3 shadow-sm">
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-foreground">
<CheckCircle2Icon className="h-4 w-4 text-emerald-600" />
Scrip results
</div>
<div className="space-y-1.5">
<section className="rounded-lg border border-border bg-accent/20 p-3">
<div className="flex items-center gap-2 text-xs font-semibold text-foreground"><CheckCircle2Icon className="h-3.5 w-3.5 text-emerald-500" /> Scrip results</div>
<div className="mt-2 space-y-1">
{scripResults.map((result) => (
<div
key={result.scripId}
className={cn(
"flex items-center gap-2 text-xs",
result.success ? "text-emerald-700 dark:text-emerald-300" : "text-destructive"
)}
>
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
result.success ? "bg-emerald-600" : "bg-destructive"
)}
/>
{result.message}
<div key={result.scripId} className={cn("flex items-center gap-2 text-[11px]", result.success ? "text-emerald-600 dark:text-emerald-400" : "text-destructive")}>
<span className={cn("h-1.5 w-1.5 rounded-full", result.success ? "bg-emerald-500" : "bg-destructive")} /> {result.message}
</div>
))}
</div>
<button
onClick={() => setScripResults(null)}
className="mt-2 text-xs font-medium text-muted-foreground hover:text-foreground"
type="button"
>
Dismiss
</button>
<button onClick={() => setScripResults(null)} className="mt-1 text-[10px] text-muted-foreground hover:text-foreground" type="button">Dismiss</button>
</section>
)}
<Separator />
{/* Assignment — no bordered boxes */}
<section>
<h2 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">
Assignment
</h2>
<dl className="overflow-hidden rounded-md border border-border bg-background/60">
<div className="grid grid-cols-[92px_minmax(0,1fr)] gap-3 border-b border-border px-3 py-2.5">
<dt className="text-[11px] font-semibold uppercase text-muted-foreground">Owner</dt>
<dd className="min-w-0">
<h2 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Assignment</h2>
<div className="space-y-3">
<div>
<label className="mb-1 block text-[10px] font-medium text-muted-foreground">Owner</label>
<select
value={ticket.owner_id ?? ""}
onChange={(event) => void handleOwnerChange(event.target.value)}
disabled={fieldSaving === "owner"}
className="h-8 w-full rounded-md border border-input bg-card px-2 text-sm text-foreground outline-none transition-colors focus:border-ring disabled:opacity-60"
className="h-8 w-full rounded-md border border-input bg-transparent px-2.5 text-sm text-foreground outline-none focus:border-ring disabled:opacity-50"
aria-label="Owner"
>
<option value="">Unassigned</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.username}
</option>
))}
{users.map((user) => (<option key={user.id} value={user.id}>{user.username}</option>))}
</select>
</dd>
</div>
<div className="grid grid-cols-[92px_minmax(0,1fr)] gap-3 border-b border-border px-3 py-2.5">
<dt className="text-[11px] font-semibold uppercase text-muted-foreground">Team</dt>
<dd className="min-w-0">
<div>
<label className="mb-1 block text-[10px] font-medium text-muted-foreground">Team</label>
<select
value={ticket.team_id ?? ""}
onChange={(event) => void handleTeamChange(event.target.value)}
disabled={fieldSaving === "team"}
className="h-8 w-full rounded-md border border-input bg-card px-2 text-sm text-foreground outline-none transition-colors focus:border-ring disabled:opacity-60"
className="h-8 w-full rounded-md border border-input bg-transparent px-2.5 text-sm text-foreground outline-none focus:border-ring disabled:opacity-50"
aria-label="Team"
>
<option value="">No team</option>
{teams.map((team) => (
<option key={team.id} value={team.id}>
{team.name}
</option>
))}
{teams.map((team) => (<option key={team.id} value={team.id}>{team.name}</option>))}
</select>
</dd>
</div>
<PropertyRow label="Priority" value="Not set" />
</dl>
</div>
</section>
{/* Details — simple key-value lines */}
<section>
<h2 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">
Details
</h2>
<dl className="overflow-hidden rounded-md border border-border bg-background/60">
<PropertyRow label="Queue" value={queue?.name || ticket.queue_id} />
<PropertyRow
label="Created"
value={formatDistanceToNow(new Date(ticket.created_at), { addSuffix: true })}
/>
<PropertyRow
label="Updated"
value={formatDistanceToNow(new Date(ticket.updated_at), { addSuffix: true })}
/>
<h2 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Details</h2>
<div className="space-y-2">
<div className="flex items-baseline justify-between gap-2 text-sm">
<span className="text-muted-foreground">Queue</span>
<span className="text-foreground">{queue?.name || ticket.queue_id}</span>
</div>
<div className="flex items-baseline justify-between gap-2 text-sm">
<span className="text-muted-foreground">Created</span>
<span className="text-foreground">{formatDistanceToNow(new Date(ticket.created_at), { addSuffix: true })}</span>
</div>
<div className="flex items-baseline justify-between gap-2 text-sm">
<span className="text-muted-foreground">Updated</span>
<span className="text-foreground">{formatDistanceToNow(new Date(ticket.updated_at), { addSuffix: true })}</span>
</div>
{ticket.resolved_at && (
<PropertyRow
label="Resolved"
value={formatDistanceToNow(new Date(ticket.resolved_at), { addSuffix: true })}
/>
<div className="flex items-baseline justify-between gap-2 text-sm">
<span className="text-muted-foreground">Resolved</span>
<span className="text-foreground">{formatDistanceToNow(new Date(ticket.resolved_at), { addSuffix: true })}</span>
</div>
)}
</dl>
</div>
</section>
{/* Custom fields — flat, no heavy borders */}
<section>
<h2 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">
Custom fields
</h2>
<h2 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Custom fields</h2>
{queueFields.length === 0 ? (
<div className="rounded-md border border-border bg-background/60 px-3 py-3 text-sm text-muted-foreground">
No fields are assigned to this queue.
</div>
<p className="text-xs text-muted-foreground">No fields assigned.</p>
) : (
<div className="overflow-hidden rounded-md border border-border bg-background/60">
<div className="space-y-3">
{queueFields.map((assignment) => {
const field = assignment.custom_field;
const fieldId = assignment.custom_field_id;
const options = Array.isArray(field?.values)
? field.values.map((value) => String(value))
: [];
const currentValue =
ticket?.custom_fields?.find((item) => item.custom_field_id === fieldId)?.value ?? "";
const options = Array.isArray(field?.values) ? field.values.map((v) => String(v)) : [];
const currentValue = ticket?.custom_fields?.find((item) => item.custom_field_id === fieldId)?.value ?? "";
const isEditing = editingFieldId === fieldId;
const draftValue = customFieldDrafts[fieldId] ?? currentValue;
const isSaving = customFieldSaving === fieldId;
return (
<div
key={assignment.id}
className="grid gap-1.5 border-b border-border px-3 py-2.5 last:border-b-0"
>
<label className="text-[11px] font-semibold uppercase text-muted-foreground">
{field?.name ?? fieldId}
</label>
<div key={assignment.id}>
<label className="mb-1 block text-[10px] font-medium text-muted-foreground">{field?.name ?? fieldId}</label>
{isEditing ? (
<div className="flex items-center gap-1.5">