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>
67 lines
1.7 KiB
TypeScript
67 lines
1.7 KiB
TypeScript
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>
|
||
}
|