Coloriginz Design Systemshadcn custom registry

@col/page-states

Versie: 1.0.0 Toegevoegd: 2026-08-06

Herkomst

Vier apps schreven elk hun eigen laden/leeg/fout-drieluik. growerportal2/src/components/ui/error-state.tsx (22 regels, gebruikt in 7 content-componenten) plus de .empty-state/.page-header/.filter-bar CSS-recepten in src/app/globals.css leverden de icoon-in-cirkel-vorm. floriday api/src/app/(protected)/aanbod/ is het meest doordachte van de vier: empty-state.tsx benoemt wélke actieve filters tot nul rijen leidden en biedt per filter een link om hem te wissen, en waking-up-notice.tsx verschijnt pas na drie seconden zodat hij niet knippert bij een snelle laadbeurt (Neon suspendeert na vijf minuten stilte, en de eerste query daarna duurt merkbaar). supplier-onboarding-vercel/src/app/error.tsx, not-found.tsx en global-error.tsx zijn drie keer met de hand geschreven volledige-pagina-schermen met logo en een groot statusnummer. voorraadbeheer/src/app/(dashboard)/products/page.tsx (en zeven-plus andere paginas) inlinen de hele triade letterlijk: een rode foutbox, platte laadtekst, een leeg-blok met dashed border en een lucide-icoon.

Dit item combineert drie van de vier: groerportal2's icoon-in-cirkel-vorm, floriday's hints-vondst en zijn vertraagde melding, en voorraadbeheer's dashed-border-kader. Het vierde patroon — de volledige-pagina-schermen van supplier-onboarding-vercel — zit er bewust niet in; zie Bekende beperkingen.

Waarvoor

Vier losse componenten voor een blok ín een pagina: aan het laden, leeg, of mislukt. Pak LoadingState voor een generieke spinner met optioneel onderschrift, EmptyState voor een nul-resultaten-blok (met of zonder filter-hints), ErrorState voor een mislukte fetch met optionele retry-knop, en WakingUpNotice los binnen een loading.tsx/Suspense-fallback voor een serverloze database die koud kan opstarten.

Gebruik het niet voor een paginaspecifieke skeleton (die moet de vorm van de content kennen — zie Bewuste keuzes) en niet voor het volledige 404/500-scherm met logo en statusnummer — dat is een ander soort ding, zie Bekende beperkingen.

Wat je app moet leveren

Niets structureels — vier presentatiecomponenten, alle tekst via props. Geen backend-contract, geen hooks.

import { LoadingState, EmptyState, ErrorState, WakingUpNotice } from '@/components/page-states'

<LoadingState label="Aanbod laden..." />

<EmptyState
  icon={Package}
  title="Geen aanbodregels gevonden"
  description={`Periode ${formatRange(filters.range)}, locatie ${filters.locations.join(', ')}.`}
  hints={[
    // href: een echt anker (prefetch, delen, middelklik) — voor URL-gedreven filters zoals hier.
    { label: 'Wis alle filters', href: buildHref({ ...filters, locations: [], search: '' }, view) },
    // geen href: een knop — voor client-side gefilterde state die niet in de URL zit.
    { label: 'Wis zoekterm "roos"', onClear: () => setSearch('') },
  ]}
  action={<Button onClick={createNew}>Nieuw product</Button>}
/>

<ErrorState
  title="Het aanbod kon niet worden geladen"
  description="Er ging iets mis bij het ophalen van de gegevens."
  retryLabel={<><RefreshCw className="mr-2 h-4 w-4" />Opnieuw proberen</>}
  onRetry={() => router.refresh()}
/>

// binnen loading.tsx / een Suspense-fallback
<WakingUpNotice>De database wordt waarschijnlijk wakker na een stille periode.</WakingUpNotice>
function LoadingState(props: { label?: string }): JSX.Element

type EmptyStateHint =
  | { label: string; href: string; onClear?: () => void }   // anker; onClear vuurt als zijkanaal op onClick
  | { label: string; href?: undefined; onClear: () => void } // knop; onClear is de enige actie

function EmptyState(props: {
  icon?: React.ComponentType<{ className?: string }>
  title: string
  description?: string
  action?: React.ReactNode
  hints?: EmptyStateHint[]
}): JSX.Element

function ErrorState(props: {
  title?: string             // standaard "Something went wrong"
  description?: string
  onRetry?: () => void       // weglaten = geen retry-knop
  retryLabel?: React.ReactNode  // standaard "Retry"; ReactNode zodat een icoon + tekst kan
}): JSX.Element

function WakingUpNotice(props: {
  afterMs?: number        // standaard 3000
  children?: React.ReactNode  // weglaten = standaardtekst in het Engels
}): JSX.Element
Component Verplicht Opmerking
LoadingState — label weglaten toont alleen de spinner
EmptyState title icon is een componentverwijzing (icon={Package}), geen vooraf gerenderd element — zie Bewuste keuzes. Elke hints-entry heeft óf href (rendert als anker) óf onClear zonder href (rendert als knop) — nooit geen van beide, dat sluit de discriminated union uit
ErrorState — onRetry weglaten laat de knop weg, niet alleen uitschakelen. retryLabel mag een icoon bevatten (ReactNode)
WakingUpNotice — moet binnen een loading.tsx/Suspense-fallback staan; de timer start bij mount

Bestanden

Bestand Landt in Soort
page-states.tsx components/page-states.tsx beheerd

registryDependencies: button (voor de retry-knop, de hint-knoppen zónder href, en de buttonVariants-styling van de hint-ankers mét href; geen asChild, gewoon een directe <Button onClick> resp. een <Link className={cn(buttonVariants({...}))}> — zie AGENTS.md regel 5, hetzelfde patroon, geen nieuw idioom). Geen cssVars.

Dit item importeert next/link, en is daarmee Next-specifiek — een bewuste keuze, geen bijvangst. Alle vijf de bronapps draaien Next 16, en @col/auth-pages-starter importeert next/link al (login-page.tsx, forgot-password-page.tsx, reset-password-page.tsx), dus dit item introduceert geen nieuwe afhankelijkheid in de registry, alleen een nieuwe voor dít item zelf. Een consument buiten Next.js zou href-hints niet kunnen installeren zonder de import te vervangen door zijn eigen routerlink.

Gebruikt door

App Sinds versie Opmerkingen
— Nog niet vanuit dit item geïnstalleerd.

growerportal2 (7 content-componenten + CSS-recepten), floriday api ((protected)/aanbod/) en voorraadbeheer (products, orders en meer) hebben elk hun eigen versie — zie Herkomst. supplier-onboarding-vercel's drie volledige-pagina-schermen blijven buiten dit item, zie Bekende beperkingen. Voeg hier een regel toe zodra een app is overgezet.

Bewuste keuzes

Bekende beperkingen