Bring every record detail page onto the register-detail document grammar from #1624 (DetailSection/DefRow, one status element per the list pages' chips-mark-exceptions rule, one primary next step plus Förhandsgranska visible and everything else behind a ⋯ overflow menu, line tables on the dry-table idiom with the headline total in the serif): - invoices/[id] (11 cards, 13-button toolbar): Kund | Detaljer rows, Fakturarader table + totals, Anteckningar, Betalning, Påminnelser, Utskickshistorik (InvoiceDeliveryHistory flattened); title carries the doc type, related documents become link rows - supplier-invoices/[id], bookkeeping/[id] (serif title instead of font-mono, JournalEntryAttachments variant="section", CorrectionChain flattened), invoices/[id]/credit, assets/[id]/dispose (form as Fönster rows), salary employees/[id] (edit form behind Redigera in a dialog, Ingående saldon collapsed), salary runs/[id] + run panels (Betalfil, Skattebetalning, AGI, förmåner, override) and the payslip page - DetailSection gains an optional help slot (convention 7) Styling/structure only: no API, fetch, validation, state, dialog or permission change; every action stays reachable. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 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,
|
||
help,
|
||
aside,
|
||
children,
|
||
className,
|
||
}: {
|
||
kicker: string
|
||
/** Section help behind a "?" right after the kicker (convention 7). Pass a <HelpPopover>. */
|
||
help?: React.ReactNode
|
||
/** 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">
|
||
{/* data-ph-unmask: kickers are static i18n chrome in session replays. */}
|
||
<h2 data-ph-unmask="" className="flex items-center gap-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
||
{kicker}
|
||
{help}
|
||
</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,
|
||
)}
|
||
>
|
||
{/* data-ph-unmask on the label only: values (children) are user data
|
||
and stay masked in session replays. */}
|
||
<div data-ph-unmask="" 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>
|
||
}
|