refactor(register): customer, supplier and article detail as documents, not card piles (#1624)

The three register detail pages rendered a handful of facts as a grid of
sparse bordered cards floating in an empty page. They now read as one
flowing document: serif entity name over a quiet type/status kicker,
quiet ghost actions top right (delete turns terracotta only on hover),
and hairline-kickered definition sections (KONTAKT, KUNDUPPGIFTER,
PRIS, BOKFORING, BETALNINGSUPPGIFTER, FAKTUROR) with aligned label/value
rows in a constrained column (max-w-2xl, supplier max-w-3xl for its
invoice table). Sections land with the standard stagger-enter.

New shared primitive components/ui/detail-section.tsx (DetailSection,
DefRow, DefEmpty) carries the grammar. Empty values render a muted
en dash for facts that matter (email, phone, expense account) and are
omitted row-wise otherwise; a section with nothing to say is omitted.

Behavior preserved: edit/deactivate/delete flows, confirm dialogs,
personal-number reveal + AttnLine, account-activation retry, viewer
lock states, routing and all existing i18n keys. New def_* label keys
added line-wise to both sv.json and en.json; customer invoice_count
gained ICU plural (1 faktura, not 1 fakturor).

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-16 17:28:45 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 51539b93ed
commit a977a67063
7 changed files with 556 additions and 539 deletions
+66
View File
@@ -0,0 +1,66 @@
import { cn } from '@/lib/utils'
/**
* Register-detail document grammar (customers, suppliers, articles).
*
* A detail page is one flowing document, not a pile of cards: each group of
* facts is introduced by an uppercase hairline kicker and set as aligned
* label/value rows. The kicker's hairline is the only rule; groups are
* separated by whitespace, never borders (Living Paper, design.md).
*/
export function DetailSection({
kicker,
aside,
children,
className,
}: {
kicker: string
/** Optional right-aligned element on the kicker line: a count, a quiet action. */
aside?: React.ReactNode
children: React.ReactNode
className?: string
}) {
return (
<section className={className}>
<div className="flex items-baseline justify-between gap-4 border-b border-border pb-2">
<h2 className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
{kicker}
</h2>
{aside}
</div>
<div className="mt-3">{children}</div>
</section>
)
}
export function DefRow({
label,
children,
className,
}: {
label: string
children: React.ReactNode
className?: string
}) {
return (
<div
className={cn(
'grid grid-cols-[8rem_1fr] gap-x-6 py-2 text-sm sm:grid-cols-[10rem_1fr]',
className,
)}
>
<div className="text-muted-foreground">{label}</div>
<div className="min-w-0">{children}</div>
</div>
)
}
/**
* Muted placeholder for a value that matters but is not filled in. The en dash
* is the literal rendered value (same vocabulary as the orders list), which is
* why it is allowed to be a dash at all.
*/
export function DefEmpty() {
return <span className="text-muted-foreground">{'–'}</span>
}