41424a1650
Opening a row on the customers, invoices, verifikat, supplier-invoice, supplier, article and salary-run lists flashed three unrelated layouts: the segment's list-shaped loading.tsx (or, for suppliers and articles, the Hem-shaped dashboard fallback) during the RSC round trip, then the client page's bare centred spinner in an h-64 box while it fetched, then the content with a full layout change. Two flashes per click on the most travelled drill-down path. - components/common/DetailPageSkeleton.tsx: back link + title row + card grid + line block, the silhouette of a document/register detail page; InvoiceEditorSkeleton for the editor routes (same shape the Ny faktura dialog shows while its chunk loads). - loading.tsx for every [id] segment (customers, invoices, invoices/edit, invoices/credit, bookkeeping, supplier-invoices, suppliers, articles, salary/runs) and for the two list segments that had none (suppliers, articles, cloned from customers/loading.tsx). - The client pages render the same skeleton while they fetch instead of the centred Loader2, so the RSC fallback to client fallback handoff is invisible. The reference-data gates are already gone (A1 to A6); this only covers the primary-entity fetch. - app/(dashboard)/__tests__/detail-loading-states.test.ts pins both. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface DetailPageSkeletonProps {
|
|
/** Number of summary cards under the title row. */
|
|
cards?: 2 | 3
|
|
/** Full-width variant for MainContainer's wide routes (max-w-7xl). */
|
|
wide?: boolean
|
|
className?: string
|
|
}
|
|
|
|
/**
|
|
* Silhouette of a register/document detail page: back link, title row with
|
|
* action pills, then a card grid. Used both as the route-level loading.tsx of
|
|
* the [id] segments and as the client page's own loading state, so the
|
|
* handoff from the RSC fallback to the client fetch is a no-op visually
|
|
* instead of skeleton -> centred spinner -> content (three unrelated layouts
|
|
* on the most-travelled drill-down path).
|
|
*/
|
|
export function DetailPageSkeleton({ cards = 2, wide = false, className }: DetailPageSkeletonProps) {
|
|
return (
|
|
<div className={cn('space-y-8', className)} aria-busy="true" aria-live="polite">
|
|
{/* Back link */}
|
|
<Skeleton className="h-4 w-24" />
|
|
|
|
{/* Title (24px) + status pill + primary actions (pills) */}
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Skeleton className="h-8 w-56" />
|
|
<Skeleton className="h-6 w-20 rounded-full" />
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Skeleton className="h-9 w-28 rounded-full" />
|
|
<Skeleton className="h-9 w-24 rounded-full" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card grid */}
|
|
<div
|
|
className={cn(
|
|
'grid gap-6',
|
|
cards === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2',
|
|
wide && 'lg:grid-cols-[minmax(0,1fr)_280px]',
|
|
)}
|
|
>
|
|
{Array.from({ length: cards }, (_, i) => (
|
|
<div key={i} className="space-y-3 rounded-lg border border-border p-5">
|
|
<Skeleton className="h-3 w-28" />
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-4/5" />
|
|
<Skeleton className="h-4 w-3/5" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Line/table block */}
|
|
<div>
|
|
<div className="flex h-10 items-center gap-4 border-b border-border px-4">
|
|
<Skeleton className="h-3 w-24" />
|
|
<Skeleton className="ml-auto h-3 w-14" />
|
|
</div>
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="flex items-center justify-between gap-4 border-b border-border px-4 py-3 last:border-b-0">
|
|
<Skeleton className="h-4 w-48" />
|
|
<Skeleton className="h-4 w-20" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Silhouette of the invoice editor (single-column snabbflöde): heading, the
|
|
* customer/details block and the lines block. Matches the fallback the
|
|
* "Ny faktura" dialog shows while the editor chunk loads.
|
|
*/
|
|
export function InvoiceEditorSkeleton({ className }: { className?: string }) {
|
|
return (
|
|
<div className={cn('space-y-4', className)} aria-busy="true" aria-live="polite">
|
|
<Skeleton className="h-8 w-1/3" />
|
|
<Skeleton className="h-32 w-full rounded-lg" />
|
|
<Skeleton className="h-32 w-full rounded-lg" />
|
|
</div>
|
|
)
|
|
}
|