* fix(analytics): mask session replays by default, chrome-only unmask Invert PostHog session-replay masking from visible-by-default with pattern masking to deny-by-default: every input value is masked wholesale (rrweb maskAllInputs, no maskInputFn) and every text node is masked unless it sits under data-ph-unmask chrome or a table column header (th). Chrome tags live on the shared UI primitives (PageHeader, Label, Button except combobox triggers, TabsTrigger, Badge, Card/Dialog/Sheet titles, tooltips, help popovers, empty states, settings labels), and tagged chrome is still pattern-scrubbed for amounts and person-/organisationsnummer. data-ph-mask beats data-ph-unmask, so call sites that interpolate user data into chrome stay masked; a very-thorough audit swept every unmasked primitive and each found site got a call-site mask. Confirm-dialog wrappers and toasts stay masked centrally: their copy describes user objects by design. Untagged new UI over-masks instead of leaking. Privacy policy, RoPA and decision log updated in the same change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(analytics): tag detail-section chrome merged from main The register-detail primitives landed on main after the replay-masking audit ran: kickers and DefRow labels are static i18n chrome, values stay masked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(analytics): close skeptic and review findings on replay masking Explicit data-ph tags now resolve before the th chrome fallback, so a th nested inside a data-ph-mask container masks correctly (regression test added). Seven missed text-leak sites get call-site masks: delete-invoice and credit-page invoice numbers, IB-correction voucher reference, TIC orgnr (served unnormalized, so the separator-based scrub cannot be relied on), articles search-term empty state, dimension segment labels, and activate-account buttons. The attribute channel is closed with rrweb's blockClass: inputs whose placeholder carries an effective user value (salary overrides, correction description, danger-zone confirms, credit confirm) get ph-no-capture, removing the element from recordings while the prefill UX stays intact; the pivot-th title attribute is dropped. Privacy-policy effective date bumped to 2026-08-17. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
70 lines
2.0 KiB
TypeScript
70 lines
2.0 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">
|
||
{/* data-ph-unmask: kickers are static i18n chrome in session replays. */}
|
||
<h2 data-ph-unmask="" 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,
|
||
)}
|
||
>
|
||
{/* 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>
|
||
}
|