fix(inbox): stop the onboarding card rendering twice below xl (#944)

* fix(inbox): stop the onboarding card rendering twice below xl

The Dokumentinkorg workspace is a 3-pane master-detail grid that
collapses to a single stacked column below the xl breakpoint. With an
empty inbox the "Så funkar dokumentinkorgen" onboarding card rendered
in both the list pane (compact) and the main preview pane, because the
preview pane was never hidden when stacked. So on viewports under
1280px (narrow or split windows, smaller laptops) the same card showed
twice.

Hide the preview and fields panes below xl when the inbox is empty so
the list's compact card is the single onboarding surface. At xl+ the
3-pane layout is unchanged: quiet empty list plus one centered card.

Also align both loading skeletons (WorkspaceSkeleton and the
e/[sector]/[slug] route skeleton) to the same xl breakpoint; they
previously collapsed at md/lg, causing a 3-pane to 1-pane snap between
768 and 1280px while the workspace loaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(inbox): use existing hasAnyItem for empty-state checks

Address CodeRabbit review: the empty-inbox condition was re-derived as
`items.length === 0` in three spots (list pane, preview pane, fields
rail) while `hasAnyItem = items.length > 0` already exists. Reuse
`!hasAnyItem` in all three so "empty" has a single source of truth.
Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-09 11:05:40 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 8dde46ad96
commit b1f85bc33e
2 changed files with 28 additions and 17 deletions
@@ -114,8 +114,8 @@ function FullScreenWorkspaceSkeleton() {
</div>
<Skeleton className="h-8 w-28 shrink-0" />
</header>
<div className="flex-1 grid grid-cols-1 md:grid-cols-[240px_minmax(0,1fr)_320px] lg:grid-cols-[280px_minmax(0,1fr)_340px] min-h-0">
<aside className="border-r overflow-hidden bg-muted/20 pt-3">
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[280px_minmax(0,1fr)_340px] min-h-0">
<aside className="border-b xl:border-b-0 xl:border-r overflow-hidden bg-muted/20 pt-3">
<div className="px-3 pb-3 space-y-2 border-b">
<Skeleton className="h-8 w-full" />
<div className="flex flex-wrap gap-1">
@@ -140,8 +140,8 @@ function FullScreenWorkspaceSkeleton() {
))}
</ul>
</aside>
<main className="overflow-hidden bg-muted/10 hidden md:block" />
<aside className="border-l overflow-hidden hidden md:block" />
<main className="overflow-hidden bg-muted/10 hidden xl:block" />
<aside className="border-l overflow-hidden hidden xl:block" />
</div>
</div>
</div>
@@ -165,8 +165,8 @@ function WorkspaceSkeleton() {
</div>
<Skeleton className="h-8 w-28 shrink-0" />
</header>
<div className="flex-1 grid grid-cols-1 md:grid-cols-[240px_minmax(0,1fr)_320px] lg:grid-cols-[280px_minmax(0,1fr)_340px] min-h-0">
<aside className="border-r overflow-hidden bg-muted/20 pt-3">
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[280px_minmax(0,1fr)_340px] min-h-0">
<aside className="border-b xl:border-b-0 xl:border-r overflow-hidden bg-muted/20 pt-3">
<div className="px-3 pb-3 space-y-2 border-b">
<Skeleton className="h-8 w-full" />
<div className="flex flex-wrap gap-1">
@@ -191,8 +191,8 @@ function WorkspaceSkeleton() {
))}
</ul>
</aside>
<main className="overflow-hidden bg-muted/10 hidden md:block" />
<aside className="border-l overflow-hidden hidden md:block" />
<main className="overflow-hidden bg-muted/10 hidden xl:block" />
<aside className="border-l overflow-hidden hidden xl:block" />
</div>
</div>
</div>
@@ -910,13 +910,13 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
</div>
</div>
)}
{items.length === 0 ? (
{!hasAnyItem ? (
// On desktop the preview pane is always visible alongside this
// column, so showing the onboarding card here would duplicate it.
// On mobile the layout is a master-detail toggle and the user is
// stuck on the list view until they pick a row: without a card
// here they'd have no way to reach the explainer at all. So:
// compact card on mobile only, quiet empty state on desktop.
// Below xl the panes stack into one feed, so the sibling preview
// and fields panes are hidden when the inbox is empty (see their
// classNames): this list is the only onboarding surface there.
// So: compact card on mobile only, quiet empty state on desktop.
showOnboarding ? (
<>
<div className="xl:hidden">
@@ -965,9 +965,15 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
)}
</aside>
{/* Document preview (hero) */}
{/* Document preview (hero). When the inbox is empty there is nothing
to preview and no row can be selected, so below xl (stacked feed)
this pane is hidden: the list's compact onboarding card is the
single onboarding surface, avoiding a duplicated card. */}
<main
className="xl:overflow-hidden bg-muted/10 relative xl:block min-h-[55vh] xl:min-h-0"
className={cn(
'xl:overflow-hidden bg-muted/10 relative xl:block min-h-[55vh] xl:min-h-0',
!hasAnyItem && 'hidden xl:block'
)}
>
{selected ? (
<DocumentPreview docUrl={docUrl} docMime={docMime} isProcessing={!!selected.isPlaceholder} />
@@ -999,9 +1005,14 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
{/* Fields rail. Below xl it stacks below the preview as part of the
single vertical feed (top border for separation). At xl+ it's the
third pane with a left border. */}
third pane with a left border. With an empty inbox no row can be
selected, so below xl it is hidden to keep the stacked empty state
to just the list column. */}
<aside
className="border-t xl:border-t-0 xl:border-l xl:overflow-y-auto pt-4 xl:block pb-4"
className={cn(
'border-t xl:border-t-0 xl:border-l xl:overflow-y-auto pt-4 xl:block pb-4',
!hasAnyItem && 'hidden xl:block'
)}
>
{selected ? (
<FieldsRail