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:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
51539b93ed
commit
a977a67063
@@ -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>
|
||||
}
|
||||
Reference in New Issue
Block a user