# TP-DESIGN-SYSTEM — näyttely (@karkkainen/ui)

Jokainen kuvio ja komponentti: kuvaus, tuontirivi ja esimerkki, jonka voi liittää sellaisenaan.
Käyttöönotto: /kayttoonotto · yhteinen kehitys: repon AGENTS.md.
Palvelimen agentit: http://127.0.0.1:3100/nayttely.md (ilman välityspalvelinta ja allowlistiä).
Kuvakkeet (esim. `Boxes`, `Clock`) tulevat paketista `lucide-react`.

## Kuviot (7)

### Kuori (`AppFrame`)

Sivupalkki (palvelun vaihtaja, navigaatio, oma tili), vierivä sisältökortti ja yläpalkki pienennyksineen, murupolkuineen ja ympäristömerkkeineen (päätös 0004).

```tsx
import { ServiceSwitcher, AccountMenu } from "@karkkainen/ui";
```

Esimerkki ei käytä osaa suoraan; katso sivu.

Sivu: /kuviot/kuori

### Kehykset (`Frame`)

Otsikollinen kehys, jonka sisällä paneelit erillisinä kortteina; tilastokehys yhdelle luvulle (päätös 0003).

```tsx
import { Frame, FramePanel, FrameBody, StatFrame } from "@karkkainen/ui";

function KehysNayte() {
  return (
    <div className="space-y-4">
      <div className="grid gap-4 sm:grid-cols-3">
        <StatFrame icon={Boxes} label="Tuotteita" value="61 957" hint="luettelossa" />
        <StatFrame icon={ImageIcon} label="Kuvallisia" value="52 735" hint="85 %" />
        <StatFrame icon={Clock} label="Päivitetty" value="klo 13.36" hint="22.10." />
      </div>
      <Frame
        title="Frame: otsikko, kuvaus, merkit ja toiminnot"
        description="Paneelit kehyksen sisällä erillisinä kortteina."
        badges={
          <>
            <Badge variant="secondary">3 syötettä</Badge>
            <Badge variant="success">Ajossa</Badge>
          </>
        }
        actions={
          <Button variant="outline" size="sm">
            Avaa
          </Button>
        }
        footer={<span>Päivitetty 22.10. klo 13.36</span>}
      >
        <div className="grid gap-1.5 @2xl:grid-cols-3">
          <FramePanel title="Syötteet">
            <p className="text-sm text-muted-foreground">Tuoteluettelo, saldot ja hinnat.</p>
          </FramePanel>
          <FramePanel title="Asetukset">
            <p className="text-sm text-muted-foreground">Tunniste Item No., EAN EAN/UPC.</p>
          </FramePanel>
          <FramePanel title="Eteneminen">
            <p className="text-sm text-muted-foreground">Perustamatta 61 957.</p>
          </FramePanel>
        </div>
      </Frame>
      <Frame title="FrameBody: yksi paneeli reunasta reunaan" className="p-0">
        <FrameBody>
          <p className="text-sm text-muted-foreground">Lista tai kaavio, jolla on oma täyte.</p>
        </FrameBody>
      </Frame>
    </div>
  );
}
```

Sivu: /kuviot/kehykset

### Taulukkokehys (`TableFrame`)

Taulukko kehyksessä: tahmea työkalurivi, sarakkeet datana, lajittelu, leveydensäätö ja sivutus.

```tsx
import { TableFrame, TableFramePagination, DataTable } from "@karkkainen/ui";

function TaulukkoNayte() {
  const [sivu, setSivu] = useState(1);
  const [koko, setKoko] = useState(50);
  return (
    <TableFrame
      toolbar={
        <div className="flex w-full items-center justify-between gap-2 px-1">
          <span className="text-sm font-medium">TableFrame</span>
          <Button variant="outline" size="sm">Vie</Button>
        </div>
      }
      footer={
        <TableFramePagination
          pagination={{ page: sivu, pageSize: koko, totalItems: 61957, totalPages: Math.ceil(61957 / koko) }}
          onPageChange={setSivu}
          pageSize={koko}
          onPageSizeChange={setKoko}
        />
      }
    >
      <DataTable columns={SARAKKEET} rows={RIVIT} rowKey={(r) => r.tunnus} />
    </TableFrame>
  );
}
```

Sivu: /kuviot/taulukkokehys

### Työjono (`WorkQueue`)

Asiat ryhmiteltyinä vasemmalla, valitun sisältö oikealla; poikkeuskortti kertoo mikä on vialla ja kuka korjaa.

```tsx
import { WorkQueue, IssueCard } from "@karkkainen/ui";

function TyojonoNayte() {
  const [valittu, setValittu] = useState("hinta");
  const kohdat: Record<string, { otsikko: string; teksti: string }> = {
    hinta: { otsikko: "Ostohinta ei vastaa sovittua", teksti: "Syötteen hinta 26,13 € poikkeaa sovitusta 25,80 €:sta. Ostaja päättää, kumpi on voimassa." },
    ean: { otsikko: "EAN puuttuu 14 tuotteelta", teksti: "Toimittaja täydentää seuraavaan syötteeseen; meiltä ei toimenpiteitä." },
    kuva: { otsikko: "Kuvat haettu TP-MEDIAan", teksti: "52 735 kuvaa, säännöt ratkaisivat." },
  };
  return (
    <div className="grid gap-4 md:grid-cols-[18rem_1fr]">
      <WorkQueue
        label="Muutosjono"
        value={valittu}
        onValueChange={setValittu}
        groups={[
          { label: "Ostaja", items: [{ id: "hinta", title: "Ostohinta ei vastaa sovittua", description: "syöte 26,13 € · sovittu 25,80 €", tone: "pending" }] },
          { label: "Toimittaja", hint: "ei toimenpiteitä meiltä", items: [{ id: "ean", title: "EAN puuttuu", description: "14 tuotetta", tone: "info" }] },
          { label: "Säännöt ratkaisivat", items: [{ id: "kuva", title: "Kuvat haettu", description: "52 735", tone: "none" }] },
        ]}
      />
      <div className="space-y-3">
        <IssueCard
          title={kohdat[valittu].otsikko}
          owner={valittu === "hinta" ? "Ostaja" : valittu === "ean" ? "Toimittaja" : "Säännöt"}
          ownerTone={valittu === "hinta" ? "self" : "other"}
          status={valittu === "kuva" ? "Ratkaistu" : "Odottaa"}
          statusTone={valittu === "kuva" ? "success" : "warning"}
          description={kohdat[valittu].teksti}
        />
      </div>
    </div>
  );
}
```

Sivu: /kuviot/tyojono

### Tyhjä tila (`EmptyState`)

Mitä tässä olisi, miksi sitä ei ole ja mitä tehdä seuraavaksi.

```tsx
import { EmptyState } from "@karkkainen/ui";

<EmptyState
  icon={Inbox}
  title="Ei ajoja"
  description="Ajo syntyy, kun luettelosta valitaan tuotteita perustettavaksi."
  action={
    <Button size="sm" variant="outline">
      Näytä luettelo
    </Button>
  }
/>
```

Sivu: /kuviot/tyhja-tila

### Tilamerkit (`StatusBadge`)

Tila merkkinä yksittäiselle riville tai pisteenä toistuvassa taulukossa; aina sanoin, ei pelkkä väri.

```tsx
import { StatusBadge, StatusDot } from "@karkkainen/ui";

<div className="grid gap-4 sm:grid-cols-2">
  <div className="flex flex-wrap items-center gap-2">
    <StatusBadge tone="success">Valmis</StatusBadge>
    <StatusBadge tone="warning" pulse>
      Ajossa
    </StatusBadge>
    <StatusBadge tone="error">Epäonnistui</StatusBadge>
    <StatusBadge tone="info">Odottaa</StatusBadge>
    <StatusBadge tone="neutral">Keskeytetty</StatusBadge>
  </div>
  <div className="space-y-2">
    <StatusDot tone="success" detail="22.10. klo 13.36">
      Valmis
    </StatusDot>
    <StatusDot tone="warning" pulse detail="12 / 61 957">
      Ajossa
    </StatusDot>
    <StatusDot tone="error" detail="Tunniste ei erotellut rivejä">
      Epäonnistui
    </StatusDot>
  </div>
</div>
```

Sivu: /kuviot/tilamerkit

### Versiovertailu (`VersionDiff`)

Kahden version ero avain–arvo-riveinä, muutos päällekkäin −/+ ja muutosloki aikajanana.

```tsx
import { VersionDiff, ChangePair } from "@karkkainen/ui";

const rivit = diffEntries(
  [
    { key: "Nimi", value: "Battery for MT9557" },
    { key: "EAN", value: "5711045123456" },
    { key: "Väri", value: "musta" },
  ],
  [
    { key: "Nimi", value: "Akku MT9557, 3,87 V" },
    { key: "EAN", value: "5711045123456" },
    { key: "Paino", value: "0,12 kg" },
  ],
);
return (
  <div className="space-y-4">
    <ChangePair label="Ostohinta" before="29,86 €" after="31,20 €" />
    <VersionDiff entries={rivit} beforeLabel="Versio 3" afterLabel="Versio 4" />
  </div>
);
```

Sivu: /kuviot/versiovertailu

## Komponentit (35)

### Edistymispalkki (`Progress`)

Kuinka pitkällä ajo tai lataus on.

```tsx
import { Progress } from "@karkkainen/ui";

<Progress value={62} className="max-w-sm" />
```

Sivu: /komponentit/progress

### Esittelykortti (`SpotlightFrame`)

Ikoni pinottujen korttien päällä, otsikko, selitys ja toiminto: pisteruudukolla kehyksessä tai korkeana korttina ruudukon sarakkeessa.

```tsx
import { SpotlightFrame, SpotlightCard } from "@karkkainen/ui";

function EsittelykorttiNayte() {
  return (
    <div className="space-y-6">
      <SpotlightFrame
        title="Kortin otsikko"
        icon={History}
        heading="Terveisiä kortista!"
        description="Tämä on kortti TP-PERUSTUS agentille"
      />
      {/* Ilman otsikkoriviä, painikkeen kanssa (omistaja 23.10.2026). */}
      <SpotlightFrame
        icon={Mail}
        heading="Tämä on otsikko"
        description="Tämä on kappale otsikon alla"
        action={
          <Button variant="outline" size="sm">
            Painike
          </Button>
        }
      />
      {/* Korkea kortti kolmanneksen levyisenä (omistaja 23.10.2026). */}
      <div className="grid gap-6 md:grid-cols-3">
        <SpotlightCard
          title="Haluatko antaa meille otsikon?"
          subtitle="Klikkaa tästä ja valitse kappale"
          icon={UserX}
          heading="Unohditko otsikkosi?"
          description="Palauta se tämän kappaleen alta."
          action={<Button>Palauta otsikko</Button>}
        />
      </div>
    </div>
  );
}
```

Sivu: /komponentit/spotlight

### Hakukenttä (`SearchCombobox`)

Ehdottaa kirjoittaessa; kuori ilman datakerrosta.

```tsx
import { SearchCombobox } from "@karkkainen/ui";

function HakuNayte() {
  const [avattu, setAvattu] = useState<string | null>(null);
  return (
    <div className="max-w-sm space-y-2">
      <SearchCombobox
        label="Hae toimittajaa"
        placeholder="Hae nimellä tai numerolla"
        search={async (q) => TOIMITTAJAT.filter((t) => `${t.nimi} ${t.id}`.toLowerCase().includes(q.toLowerCase()))}
        hitKey={(t) => t.id}
        renderHit={(t) => (
          <span className="flex justify-between gap-2">
            <span>{t.nimi}</span>
            <span className="tabular-nums text-muted-foreground">{t.id}</span>
          </span>
        )}
        onOpen={(t) => setAvattu(t.nimi)}
      />
      {avattu && <p className="text-sm text-muted-foreground">Avattu: {avattu}</p>}
    </div>
  );
}
```

Sivu: /komponentit/search-combobox

### Ilmoituskeskus (`NotificationCenter`)

Kello, lukumerkki ja ilmoitusten lista ponnahdusikkunassa.

```tsx
import { NotificationCenter } from "@karkkainen/ui";

<NotificationCenter
  notifications={[
    { id: "1", title: "EET Retail: 61 957 ehdokasta vastaanotettu", when: "13.36", unread: true },
    { id: "2", title: "Ajo valmis: 12 tuotetta katselmoitavana", description: "Asir 303592", when: "12.10", unread: true },
    { id: "3", title: "Päivitys asennettu", when: "eilen" },
  ]}
/>
```

Sivu: /komponentit/notification-center

### Ilmoituskupla (`Toast`)

Itsestään sulkeutuva ilmoitus näytön alalaidassa.

```tsx
import { Toast } from "@karkkainen/ui";

function IlmoitusNayte() {
  const [auki, setAuki] = useState(false);
  return (
    <>
      <Button size="sm" variant="outline" onClick={() => setAuki(true)}>
        Tallenna asetukset
      </Button>
      {auki && <Toast message="Asetukset tallennettu" tone="success" onDismiss={() => setAuki(false)} />}
    </>
  );
}
```

Sivu: /komponentit/toast

### Kortti (`Card`)

Erillinen pinta otsikolle, sisällölle ja alatunnisteelle.

```tsx
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from "@karkkainen/ui";

<Card className="max-w-sm">
  <CardHeader>
    <CardTitle>EET Retail Aps</CardTitle>
    <CardDescription>Toimittaja 303202 · tuoteluettelo</CardDescription>
  </CardHeader>
  <CardContent className="text-sm">61 957 tuotetta, joista 85 % kuvallisia.</CardContent>
  <CardFooter>
    <Button size="sm" variant="outline">
      Avaa luettelo
    </Button>
  </CardFooter>
</Card>
```

Sivu: /komponentit/card

### Kytkin (`Switch`)

Asetus päälle tai pois, vaikuttaa heti.

```tsx
import { Switch } from "@karkkainen/ui";

function KytkinNayte() {
  const [paalla, setPaalla] = useState(true);
  return (
    <label className="flex items-center gap-2 text-sm">
      <Switch checked={paalla} onChange={(e) => setPaalla(e.target.checked)} /> Automaattinen toipuminen
    </label>
  );
}
```

Sivu: /komponentit/switch

### Liukupaneeli (`Sheet`)

Sisällön päälle liukuva paneeli, joka lukitsee taustan.

```tsx
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@karkkainen/ui";

<Sheet>
  <SheetTrigger asChild>
    <Button variant="outline" size="sm">
      Avaa liukupaneeli
    </Button>
  </SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Suodattimet</SheetTitle>
      <SheetDescription>Liukuu sisällön päälle ja lukitsee taustan.</SheetDescription>
    </SheetHeader>
    <SheetBody className="space-y-2 text-sm">
      <label className="flex items-center gap-2">
        <Checkbox defaultChecked /> Kuvalliset
      </label>
      <label className="flex items-center gap-2">
        <Checkbox /> Perustetut
      </label>
    </SheetBody>
    <SheetFooter>
      <Button size="sm">Käytä</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

Sivu: /komponentit/sheet

### Lomakeikkuna (`FormDialog`)

Valintaikkuna, joka omistaa lähetystilan ja virheen.

```tsx
import { FormDialog } from "@karkkainen/ui";

function LomakeikkunaNayte() {
  const [auki, setAuki] = useState(false);
  const [nimi, setNimi] = useState("");
  return (
    <>
      <Button size="sm" variant="outline" onClick={() => setAuki(true)}>
        Nimeä luettelo
      </Button>
      <FormDialog
        open={auki}
        onOpenChange={setAuki}
        title="Nimeä luettelo"
        description="Nimi näkyy toimittajakortilla ja vienneissä."
        submitLabel="Tallenna"
        submittingLabel="Tallennetaan…"
        onSubmit={async () => {
          await new Promise((r) => setTimeout(r, 600));
          if (!nimi.trim()) return "Nimi puuttuu.";
        }}
      >
        <div className="space-y-1.5">
          <Label htmlFor="esim-luettelo">Nimi</Label>
          <Input id="esim-luettelo" value={nimi} onChange={(e) => setNimi(e.target.value)} placeholder="EET Retail, tuoteluettelo" />
        </div>
      </FormDialog>
    </>
  );
}
```

Sivu: /komponentit/form-dialog

### Merkki (`Badge`)

Lyhyt nimiö: lukumäärä, luokka tai tila tekstinä.

```tsx
import { Badge } from "@karkkainen/ui";

<div className="flex flex-wrap items-center gap-2">
  {(["default", "secondary", "outline", "success", "warning", "info", "destructive"] as const).map((v) => (
    <Badge key={v} variant={v}>
      {v}
    </Badge>
  ))}
</div>
```

Sivu: /komponentit/badge

### Mittarikortti (`StatCard`)

Ikoni, selite ja yksi iso luku.

```tsx
import { StatCard, StatCardRow } from "@karkkainen/ui";

<StatCardRow>
  <StatCard icon={Boxes} label="Tuotteita" value="61 957" />
  <StatCard icon={ImageIcon} label="Kuvallisia" value="52 735" />
  <StatCard icon={Wallet} label="Kustannus" value="12,40 €" />
  <StatCard icon={Clock} label="Päivitetty" value="13.36" />
</StatCardRow>
```

Sivu: /komponentit/stat-card

### Murupolku (`Breadcrumb`)

Missä olet ja miten pääset takaisin.

```tsx
import { Breadcrumb, BreadcrumbLink, BreadcrumbPage } from "@karkkainen/ui";

<div className="space-y-3">
  <Breadcrumb>
    <BreadcrumbLink href="#">Toimittajat</BreadcrumbLink>
    <BreadcrumbLink href="#">EET Retail Aps</BreadcrumbLink>
    <BreadcrumbPage>Ajot</BreadcrumbPage>
  </Breadcrumb>
  {/* collapse: kapealla juuri › … › edellinen › nykyinen */}
  <Breadcrumb collapse>
    <BreadcrumbLink href="#">Yleisnäkymä</BreadcrumbLink>
    <BreadcrumbLink href="#">Toimittajat</BreadcrumbLink>
    <BreadcrumbLink href="#">EET Retail Aps</BreadcrumbLink>
    <BreadcrumbLink href="#">Ajot</BreadcrumbLink>
    <BreadcrumbPage>Ajo 23.10.</BreadcrumbPage>
  </Breadcrumb>
</div>
```

Sivu: /komponentit/breadcrumb

### Nimiö (`Label`)

Kentän nimi, kytketty kenttään.

```tsx
import { Label } from "@karkkainen/ui";

<div className="max-w-sm space-y-1.5">
  <Label htmlFor="esim-tunniste">Tunnistekenttä</Label>
  <Input id="esim-tunniste" defaultValue="Item No." />
</div>
```

Sivu: /komponentit/label

### Painike (`Button`)

Toiminto: ensisijainen, toissijainen, reunallinen, haamu, vaarallinen ja linkki.

```tsx
import { Button } from "@karkkainen/ui";

<div className="space-y-3">
  <div className="flex flex-wrap items-center gap-2">
    {(["default", "secondary", "outline", "ghost", "destructive", "link"] as const).map((v) => (
      <Button key={v} variant={v}>
        {v}
      </Button>
    ))}
  </div>
  <div className="flex flex-wrap items-center gap-2">
    <Button size="sm" variant="outline">
      <Download /> Vie <ChevronDown />
    </Button>
    <Button size="sm">Pieni</Button>
    <Button disabled>Estetty</Button>
  </div>
</div>
```

Sivu: /komponentit/button

### Paneelin osat (`PanelHeader`)

Sivupaneelien yhteinen otsikko, runko ja alatunniste.

```tsx
import { PanelHeader, PanelBody, PanelFooter } from "@karkkainen/ui";

<div className="flex h-64 max-w-md flex-col overflow-hidden rounded-xl border bg-card">
  <PanelHeader>
    <span className="font-semibold">Tuotteen tiedot</span>
  </PanelHeader>
  <PanelBody>
    <p className="text-sm text-muted-foreground">
      Otsikko ja toimintopalkki pysyvät paikallaan, ja vain tämä alue vierii. Sama rakenne on Liukupaneelissa ja Sivupaneelissa.
    </p>
    {Array.from({ length: 8 }, (_, i) => (
      <p key={i} className="mt-2 text-sm">
        Attribuutti {i + 1}
      </p>
    ))}
  </PanelBody>
  <PanelFooter>
    <Button size="sm">Tallenna</Button>
  </PanelFooter>
</div>
```

Sivu: /komponentit/panel

### Ponnahdusikkuna (`Popover`)

Elementtiin kiinnittyvä sisältö, joka ei lukitse sivua.

```tsx
import { Popover, PopoverTrigger, PopoverContent } from "@karkkainen/ui";

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline" size="sm">
      Suodattimet
    </Button>
  </PopoverTrigger>
  <PopoverContent className="w-64 space-y-2 text-sm">
    <label className="flex items-center gap-2">
      <Checkbox defaultChecked /> Kuvalliset
    </label>
    <label className="flex items-center gap-2">
      <Checkbox /> Perustetut
    </label>
  </PopoverContent>
</Popover>
```

Sivu: /komponentit/popover

### Profiilikuva (`Avatar`)

Nimikirjaimet tai kuva käyttäjälle ja palvelulle.

```tsx
import { Avatar } from "@karkkainen/ui";

<div className="flex items-center gap-3">
  <Avatar name="TP Perustus" size="sm" />
  <Avatar name="TP Vastaanotto" />
  <Avatar name="Eet Retail" size="lg" />
</div>
```

Sivu: /komponentit/avatar

### Pudotusvalikko (`Select`)

Yksi arvo listasta.

```tsx
import { Select } from "@karkkainen/ui";

<Select
  className="max-w-sm"
  defaultValue="punainen"
  options={[
    { value: "neutraali", label: "Neutraali" },
    { value: "petroli", label: "Petroli" },
    { value: "kanerva", label: "Kanerva" },
    { value: "punainen", label: "Punainen" },
  ]}
/>
```

Sivu: /komponentit/select

### Pylväsrivi (`MiniBarChart`)

Pieni aikasarja: päiväkustannukset, päivittäiset ajot.

```tsx
import { MiniBarChart } from "@karkkainen/ui";

<MiniBarChart
  label="Ajot päivittäin"
  data={["16.10.", "17.10.", "18.10.", "19.10.", "20.10.", "21.10.", "22.10."].map((label, i) => ({ label, value: [4, 7, 2, 0, 9, 12, 6][i] }))}
/>
```

Sivu: /komponentit/mini-bar-chart

### Reittivälilehdet (`TabNav`)

Välilehtien näköiset linkit eri osoitteisiin.

```tsx
import { TabNav, TabNavLink } from "@karkkainen/ui";

<TabNav label="Toimittajan näkymät" currentPath="/toimittajat/303202/ehdokkaat">
  <TabNavLink href="/toimittajat/303202/ehdokkaat" count={61957}>
    Luettelo
  </TabNavLink>
  <TabNavLink href="/toimittajat/303202/ajot">Ajot</TabNavLink>
  <TabNavLink href="/toimittajat/303202/asetukset">Asetukset</TabNavLink>
</TabNav>
```

Sivu: /komponentit/tab-nav

### Segmenttivalitsin (`Segmented`)

Kahdesta neljään toisensa poissulkevaa näkymää, esimerkiksi Taulukko | Kortit.

```tsx
import { Segmented } from "@karkkainen/ui";

function SegmentNayte() {
  const [nakyma, setNakyma] = useState<"taulukko" | "kortit">("taulukko");
  const [rajaus, setRajaus] = useState<"omat" | "kaikki">("omat");
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Segmented
        label="Näkymä"
        value={nakyma}
        onValueChange={setNakyma}
        options={[
          { value: "taulukko", label: "Taulukko", icon: Table2 },
          { value: "kortit", label: "Kortit", icon: LayoutGrid },
        ]}
      />
      {/* variant="surface": painikkeiden valkoinen tyyli työkaluriville */}
      <Segmented
        variant="surface"
        label="Rajaus"
        value={rajaus}
        onValueChange={setRajaus}
        options={[
          { value: "omat", label: "Omat" },
          { value: "kaikki", label: "Kaikki" },
        ]}
      />
    </div>
  );
}
```

Sivu: /komponentit/segmented

### Sisältökortti (`PanelCard`)

Valkoinen kortti, otsikko ja apurivi vasemmassa yläkulmassa, alla vapaa sisältö kuten taulukko reunasta reunaan.

```tsx
import { PanelCard } from "@karkkainen/ui";

function SisaltokorttiNayte() {
  return (
    <div className="grid gap-6 md:grid-cols-3">
      <SpotlightCard
        title="Haluatko antaa meille otsikon?"
        subtitle="Klikkaa tästä ja valitse kappale"
        icon={UserX}
        heading="Unohditko otsikkosi?"
        description="Palauta se tämän kappaleen alta."
        action={<Button>Palauta otsikko</Button>}
      />
      {/* Viereen kaksi kolmannesta: sama kortti taulukolla (omistaja 23.10.2026). */}
      <PanelCard
        className="md:col-span-2"
        title="Toimittajien tilanne"
        subtitle="Esimerkkidata: luettelot ja luokittelu toimittajittain"
        actions={<Button variant="outline" size="sm">Avaa</Button>}
      >
        {/* Rivit reunasta reunaan: kortin täyte (16 px) kumotaan sivuilta ja alhaalta, ja
            ensimmäinen ja viimeinen sarake saavat saman täytteen. */}
        <div className="-mx-4 -mb-4 mt-4 flex-1">
        <Table className="[&_tr>*:first-child]:ps-4 [&_tr>*:last-child]:pe-4 [&_tbody_tr:last-child]:border-b-0">
          <TableHeader>
            <TableRow>
              <TableHead>Toimittaja</TableHead>
              <TableHead className="text-right">Tuotteita</TableHead>
              <TableHead className="text-right">Luokiteltu</TableHead>
              <TableHead>Päivitetty</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {[
              ["heo GmbH", "61 957", "72 %", "22.10. klo 13.36"],
              ["Asir Group", "4 168", "41 %", "21.10. klo 09.12"],
              ["Stihl", "322", "15 %", "20.10. klo 16.40"],
              ["Rehvid Pluss OÜ", "1 900", "88 %", "19.10. klo 11.05"],
              ["Matrolight", "261", "100 %", "18.10. klo 08.30"],
              ["Reimo", "2 410", "64 %", "17.10. klo 14.22"],
              ["Vannetukku", "8 905", "57 %", "16.10. klo 10.48"],
              ["Insmat", "1 132", "93 %", "15.10. klo 07.55"],
            ].map(([nimi, n, osuus, aika]) => (
              <TableRow key={nimi}>
                <TableCell className="font-medium">{nimi}</TableCell>
                <TableCell className="text-right tabular-nums">{n}</TableCell>
                <TableCell className="text-right tabular-nums">{osuus}</TableCell>
                <TableCell className="text-muted-foreground">{aika}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
        </div>
      </PanelCard>
    </div>
  );
}
```

Sivu: /komponentit/panel-card

### Sivun otsikko (`PageHeader`)

Otsikko, yläotsikko ja kuvaus sivun alkuun.

```tsx
import { PageHeader } from "@karkkainen/ui";

<PageHeader eyebrow="Toimittaja 303202" title="EET Retail Aps" subtitle="Tuoteluettelo, ajot ja asetukset." />
```

Sivu: /komponentit/page-header

### Sivupaneeli (`SidePanel`)

Sisältöä kaventava paneeli; molemmat puolet jäävät käytettäviksi.

```tsx
import { SidePanel, SidePanelHeader, SidePanelBody, SidePanelTitle } from "@karkkainen/ui";

function SivupaneeliNayte() {
  const [auki, setAuki] = useState(true);
  return (
    <div className="flex h-56 overflow-hidden rounded-xl border bg-card">
      <div className="min-w-0 flex-1 space-y-2 overflow-y-auto p-4 text-sm">
        <p className="text-muted-foreground">Sisältö kapenee paneelin viereltä, ja molemmat puolet pysyvät käytettävinä.</p>
        <Button size="sm" variant="outline" onClick={() => setAuki((a) => !a)}>
          {auki ? "Sulje paneeli" : "Avaa paneeli"}
        </Button>
      </div>
      <SidePanel open={auki} onClose={() => setAuki(false)} size="sm">
        <SidePanelHeader>
          <SidePanelTitle>Aplus A703 265/65R17</SidePanelTitle>
        </SidePanelHeader>
        <SidePanelBody className="text-sm text-muted-foreground">Tunnus W128900000 · 29,86 €</SidePanelBody>
      </SidePanel>
    </div>
  );
}
```

Sivu: /komponentit/side-panel

### Sivutus (`Pagination`)

Rivimäärä vasemmalla, sivunvaihto oikealla.

```tsx
import { Pagination } from "@karkkainen/ui";

function SivutusNayte() {
  const [sivu, setSivu] = useState(1);
  return <Pagination pagination={{ page: sivu, pageSize: 50, totalItems: 61957, totalPages: 1240 }} onPageChange={setSivu} />;
}
```

Sivu: /komponentit/pagination

### Syötekenttä (`Input`)

Yksirivinen tekstikenttä.

```tsx
import { Input } from "@karkkainen/ui";

<Input placeholder="Hae nimellä tai tunnisteella" className="max-w-sm" />
```

Sivu: /komponentit/input

### Taulukko (`Table`)

Taulukon perusosat omaa rakennetta varten.

```tsx
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from "@karkkainen/ui";

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Nimi</TableHead>
      <TableHead>Tunnus</TableHead>
      <TableHead className="text-right">Hinta</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {[
      ["Aplus A703 265/65R17", "W128900000", "29,86 €"],
      ["Cradle (black) for NQuire 10", "W128900411", "51,76 €"],
    ].map(([n, t, h]) => (
      <TableRow key={t}>
        <TableCell>{n}</TableCell>
        <TableCell className="tabular-nums">{t}</TableCell>
        <TableCell className="text-right tabular-nums">{h}</TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>
```

Sivu: /komponentit/table

### Tekstialue (`Textarea`)

Monirivinen tekstikenttä.

```tsx
import { Textarea } from "@karkkainen/ui";

<Textarea placeholder="Kuvaus" className="max-w-sm" />
```

Sivu: /komponentit/textarea

### Tiedoston pudotusalue (`UploadDropzone`)

Tiedosto vetämällä tai valitsemalla.

```tsx
import { UploadDropzone } from "@karkkainen/ui";

<UploadDropzone title="Pudota pohjatiedosto tähän" description="XLSX tai CSV, enintään 25 Mt." accept=".xlsx,.csv" />
```

Sivu: /komponentit/upload-dropzone

### Työkaluvihje (`Tooltip`)

Lyhyt selite osoitettaessa; ei sisällä toimintoja.

```tsx
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@karkkainen/ui";

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <Button variant="ghost" size="sm">
        Osoita minua
      </Button>
    </TooltipTrigger>
    <TooltipContent>Toimittajan alkuperäinen tiedosto</TooltipContent>
  </Tooltip>
</TooltipProvider>
```

Sivu: /komponentit/tooltip

### Valikko (`DropdownMenu`)

Toimintojen lista painikkeen takana, ryhmiteltynä otsikoin.

```tsx
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from "@karkkainen/ui";

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline" size="sm">
      <Download /> Vie <ChevronDown />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Tuotteen perustajalle</DropdownMenuLabel>
    <DropdownMenuItem>Tuotetiedot (XLSX)</DropdownMenuItem>
    <DropdownMenuItem>Attribuutit (XML)</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuLabel>Hinnoittelijalle</DropdownMenuLabel>
    <DropdownMenuItem>Hintaehdotukset (XLSX)</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

Sivu: /komponentit/dropdown-menu

### Valintaikkuna (`Dialog`)

Modaali vahvistukselle tai lyhyelle tehtävälle.

```tsx
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from "@karkkainen/ui";

<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline" size="sm">
      Perusta valitut
    </Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Perustetaanko 12 tuotetta?</DialogTitle>
      <DialogDescription>Valitut tuotteet luokitellaan ja viedään katselmointiin.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <Button>Perusta</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

Sivu: /komponentit/dialog

### Valintaruutu (`Checkbox`)

Yksi kyllä/ei-valinta tai rivin valinta taulukossa.

```tsx
import { Checkbox } from "@karkkainen/ui";

<div className="space-y-2">
  <label className="flex items-center gap-2 text-sm">
    <Checkbox defaultChecked /> Näytä vain kuvalliset
  </label>
  <label className="flex items-center gap-2 text-sm">
    <Checkbox /> Näytä perustetut
  </label>
</div>
```

Sivu: /komponentit/checkbox

### Versioilmoitus (`VersionNotice`)

"Palvelu on päivittynyt", kun välilehti ajaa vanhaa versiota.

```tsx
import { VersionNoticeCard } from "@karkkainen/ui";
```

Esimerkki ei käytä osaa suoraan; katso sivu.

Sivu: /komponentit/version-notice

### Välilehdet (`Tabs`)

Saman sivun näkymät välilehtinä.

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@karkkainen/ui";

<Tabs defaultValue="a">
  <TabsList>
    <TabsTrigger value="a">Luettelo</TabsTrigger>
    <TabsTrigger value="b">Ajot</TabsTrigger>
  </TabsList>
  <TabsContent value="a" className="pt-3 text-sm text-muted-foreground">
    Välilehden sisältö.
  </TabsContent>
  <TabsContent value="b" className="pt-3 text-sm text-muted-foreground">
    Toinen välilehti.
  </TabsContent>
</Tabs>
```

Sivu: /komponentit/tabs
