Käyttöönotto

Paketti kulkee palveluun vendoroituna kopiona. Alla paketin README:n luvut sellaisinaan, sekä kaksi kohtaa, jotka ovat menneet väärin useammassa palvelussa.

Kaksi tunnettua ansaa

Fontti: --font-sans, ei --font-geist-sans

Teema lukee muuttujaa --font-sans. Vercelin geist/font/sans asettaa --font-geist-sans, jolloin fontti latautuu mutta mikään ei käytä sitä ja sivu näyttää järjestelmäfonttia. Lataa Geist next/font/localilla muuttujaan --font-sans. Mitattu TP-PERUSTUKSESSA 19.10. ja tällä sivustolla 22.10.2026.

const fontSans = localFont({
  src: "../node_modules/geist/dist/fonts/geist-sans/Geist-Variable.woff2",
  weight: "100 900",
  variable: "--font-sans",
});

Sävy: data-vari juureen

Sävyteemat ovat styles/savyt.css:ssä. Sävy valitaan juuren attribuutilla, teema .dark-luokalla. Ilman attribuuttia on paketin oletus, joka on sama kuin Neutraali.

@import "tailwindcss";
@import "@karkkainen/ui/styles/theme.css";
@import "@karkkainen/ui/styles/savyt.css";

<html data-vari="punainen">

Asennus

Paketilla on oma reponsa: [Karkkainen-AI/TP-DESIGN-SYSTEM](https://karkkainen.ghe.com/Karkkainen-AI/TP-DESIGN-SYSTEM) (luotu 2026-08-11). Paikallinen työpuu on /home/karkkai/tp-design-system.

Vendorointi on jakelutapa — ks. [AGENTS.md](AGENTS.md) §7 päätöksen perustelut. Huomaa että perustelu muuttui osittain kun oma repo saatiin: git-riippuvuus on nyt teknisesti mahdollinen, mutta AIMO:n .npmrc estää sen heillä (allow-git=none), joten vendorointi on yhä ainoa tapa joka toimii kaikille kolmelle.

Kopion tuottaminen

npm run vendor -- /polku/kuluttajaan/vendor/karkkainen-ui

Kohdepolku on vendor-hakemisto itse, ei kuluttajan repojuuri.

> Kontissa ajettaessa tarvitaan safe.directory, koska git kieltäytyy > lukemasta repoa jonka omistaja on eri kuin prosessin käyttäjä. Ilman sitä > ajo epäonnistuu kohdassa joka lukee commit-shan, ja virhe näyttää > likaisuustarkistukselta vaikka työpuussa ei ole vikaa: > > ``bash > docker run --rm -v /home/karkkai/tp-design-system:/ds -v /polku/kuluttajaan:/kohde \ > -w /ds node:22-alpine sh -c "apk add --no-cache git && \ > git config --global --add safe.directory /ds && \ > node scripts/vendor.mjs /kohde/vendor/karkkainen-ui" >

Työkalu kirjoittaa paketin kohdehakemistoon ja leimaa sen package.json:iin:

"vendored": {
  "from": "tp-design-system",
  "version": "0.1.0",
  "commit": "2ab3db5e014b…",
  "committedAt": "2026-08-11T06:04:38+02:00",
  "contentHash": "67bce268e27d…"
}

commit kertoo alkuperän, contentHash sisällön. Tarkistus nojaa jälkimmäiseen — muuten jokainen dokumentaatio- tai demo-commit merkitsisi kaikki kopiot vanhentuneiksi turhaan.

Työkalu kieltäytyy jos työpuu on likainen (muuten sha valehtelisi kopion sisällöstä), ja poistaa scripts-kentän kopiosta, koska se osoittaisi tiedostoihin joita ei vendoroida.

Kopion tarkistaminen

npm run vendor:check -- /polku/kuluttajaan/vendor/karkkainen-ui

Kertoo onko kopio ajan tasalla ja kuinka monta committia jäljessä se on. > ⚠️ Missä tämän voi ajaa: tarkistus lukee DS-lähderepon git-tilan ja > laskee sen sisällöstä tiivisteen, jotta se voi verrata. Se toimii siis vain > ympäristössä jossa sekä tämä repo että kuluttajan kopio ovat läsnä — > ylläpitäjän koneella tai cross-repo-CI:ssä. Sitä ei voi ajaa kuluttajan > käynnistysskriptissä eikä kontissa, jossa DS-repoa ei ole.

PaluuarvoMerkitys
0ajan tasalla
1vanhentunut — paketin sisältö on muuttunut lähteessä
2ei leimaa — vendoroitu ennen tätä työkalua, alkuperää ei voi päätellä
3kopiota on muokattu paikallisesti — sisältö ei vastaa sen omaa leimaa

Paluuarvo 3 on syytä ottaa vakavasti: vendor/ on generoitua sisältöä ja käsin tehdyt muutokset katoavat seuraavassa päivityksessä. Vie ne DS-repoon.

> Tunnettu kompromissi: README.md ja CHANGELOG.md kopioidaan mukaan > mutta jätetään tiivisteestä pois (muuten jokainen changelog-merkintä > merkitsisi kaikki kopiot vanhentuneiksi). Seuraus: jos joku muokkaa > vendoroidun kopion README:tä tai CHANGELOG:ia, tarkistus ei huomaa sitä. > Muokkaustunnistus kattaa vain buildiin vaikuttavan sisällön. Se on > hyväksyttävää koska nuo tiedostot eivät vaikuta ajoon — mutta se on > kompromissi, ei ominaisuus.

Kuluttajan oma eheystarkistus

Kopion mukana matkustaa verify.mjs, joka toimii ilman DS-repoa — työpuussa, CI:ssä tai käynnistysskriptissä:

node vendor/karkkainen-ui/verify.mjs

Se laskee kopion oman sisällön tiivisteen ja vertaa sitä kopion omaan leimaan, eli havaitsee paikallisen muokkauksen. Ei riippuvuuksia, ei gitiä, ei verkkoa.

> ⚠️ Next.js `output: "standalone"` ei kopioi `verify.mjs`:ää imageen. > Standalone-tuloste sisältää vain tiedostot joita sovellus oikeasti > importtaa. verify.mjs ei ole importattu mistään, joten se karsiutuu — > kontissa on tällöin vain package.json. Todettu 2026-08-11: AIMO:n ja > TP-Vastaanoton konteissa tiedostoa ei ollut, TP-IAM:in oli (heidän > imagensa ei ole standalone). > > Versioleima toimii silti kaikkialla, koska package.json matkustaa > mukana — eli kysymykseen "mikä versio meillä on ajossa" saa vastauksen > kontista: > > ``bash > docker exec <kontti> cat /app/vendor/karkkainen-ui/package.json > > > Jos haluat muokkaustunnistuksen myös ajoympäristöön, kopioi tiedosto > eksplisiittisesti runtime-vaiheessa: > > dockerfile > COPY --from=build /app/vendor/karkkainen-ui/verify.mjs ./vendor/karkkainen-ui/verify.mjs > > > Useimmiten sitä ei tarvita ajossa: muokkaustunnistus kuuluu CI:hin ja > työpuuhun, jossa verify.mjs` on aina saatavilla.

PaluuarvoMerkitys
0kopio vastaa leimaansa
2leima puuttuu tai on tuntematonta tiivistemuotoa
3kopiota on muokattu

> Se ei kerro onko kopio jäljessä lähteestä — se vaatii pääsyn DS-repoon. > Staleness on ylläpitäjän vendor:check, muokkaus on kuluttajan > verify.mjs. Tarkistus ei myöskään ole turvamekanismi vaan vahingon > havaitsin: verify.mjs itse ei kuulu tiivisteeseen, joten tarkoituksellinen > peukalointi voi kiertää sen.

Kuluttajan pää

"dependencies": {
  "@karkkainen/ui": "file:./vendor/karkkainen-ui"
}

> ⚠️ Vendoroinnin tunnettu heikkous on että kopio irtoaa lähteestään > hiljaisesti. Leima ja vendor:check ovat olemassa juuri siksi — käytä > niitä, älä luota siihen että muistat.

Käyttöönotto palvelussa

1. Asenna peer-riippuvuudet

npm install react react-dom tailwindcss @tailwindcss/postcss tw-animate-css \
  @radix-ui/react-dialog @radix-ui/react-dropdown-menu \
  @radix-ui/react-label @radix-ui/react-popover @radix-ui/react-select \
  @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-tabs \
  @radix-ui/react-tooltip \
  class-variance-authority clsx lucide-react next-themes tailwind-merge

Tailwind 4 ja `tailwind-merge` 3. Paketin luokat ovat v4:n nimiä (shadow-xs, outline-hidden), eikä tailwind-merge 2 tunne niitä — cn() jättäisi ristiriitaiset luokat yhdistämättä hiljaa.

2. PostCSS

// postcss.config.mjs
export default { plugins: { "@tailwindcss/postcss": {} } };

Ei `tailwind.config.ts`:ää eikä `content`-listaa. Teema on CSS:ssä (kohta 3), ja se kertoo Tailwindille paketin lähdekoodin sijainnin itse (@source "../src"). v3:n yleisin käyttöönottovirhe — paketin polku puuttui contentista ja komponentit renderöityivät tyylittöminä, tai pahemmin: animate-pulse ja motion-reduce:animate-none karsiutuivat hiljaa — ei ole enää kuluttajan muistettava.

Varmista silti käännetystä CSS:stä eikä oletuksesta:

grep -o 'animate-pulse{[^}]*}' .next/static/css/*.css

3. CSS

/* app/globals.css */
@import "tailwindcss";
@import "@karkkainen/ui/styles/theme.css";   /* tuo myös tokens.css:n */

@layer base {
  body {
    @apply bg-background text-foreground;
  }
}

theme.css sisältää värit, säteet, varjot, fontit, .dark-variantin, animaatiot (tw-animate-css), palvelutiilien väriluokat ja perustyylit: reunan oletusvärin (myös <table>) ja kirjasimen renderöinnin.

Tailwind 4:n omat oletukset ovat voimassa sellaisinaan, esimerkiksi painikkeiden cursor: default. Teema ei jäljittele v3:n ulkoasua.

> v3:sta siirtyvälle: poista tailwind.config.ts, autoprefixer ja > tailwindcss-animate, ja korvaa @tailwind-direktiivit yllä olevalla. > Oman koodisi luokat: npx @tailwindcss/upgrade hoitaa uudelleennimeämiset > (shadow-sm → shadow-xs, shadow → shadow-sm, outline-none → > outline-hidden, ring → ring-3). `rounded-sm`/`rounded-md`/`rounded-lg` > eivät muutu, koska teema määrittelee ne --radiusista kuten preset. > > Vanha * { @apply border-border } saa jäädä — arvo on sama.

Jos palvelusi käyttää React Flow'ta, tuo lisäksi valinnainen teemoitus:

@import "@karkkainen/ui/styles/theme.css";
@import "@karkkainen/ui/styles/react-flow.css";

Nämä säännöt asuivat 0.2.0:aan asti tokens.css:ssä, jolloin jokainen palvelu sai kolmannen osapuolen kirjaston teemoituksen käyttipä sitä tai ei. Tunnettu kuluttaja: TP-Vastaanoton /topologia.

4. Next.js: transpilePackages

Jos @karkkainen/ui shipataan TS-lähdekoodina (workspace-tilanne), Next.js pitää saada transpileeraamaan se:

// next.config.ts
const nextConfig = {
  transpilePackages: ["@karkkainen/ui"],
};
export default nextConfig;

Vite-pohjaisissa projekteissa transpilaatio toimii oletuksena.

5. Dockerfile: kopio on oltava paikallaan ennen npm ci:tä

`file:`-riippuvuus vaatii hakemiston olemassa asennushetkellä. Jos npm ci ajetaan ennen kuin vendor/ on kopioitu kuvaan, se kaatuu — ja virhe nimeää tämän paketin, eli näyttää paketin vialta vaikka on Dockerfilen järjestys.

WORKDIR /app
COPY package.json package-lock.json ./
COPY vendor ./vendor          # ← ennen npm ci:tä, ei sen jälkeen
RUN npm ci

Ja `vendor/` on oltava buildin kontekstin sisällä. Kontekstin ulkopuolista polkua ei voi kopioida, joten kopio kuuluu sen package.jsonin viereen joka ilmoittaa riippuvuuden — monorepossa sovelluksen hakemistoon eikä repon juureen.

> Tämä puuttui README:stä 0.37.1:een asti, ja kuudes kuluttaja kaatui > siihen vaikka olin kertonut saman asian heille tuntia aiemmin — mutta > perusteluna kopion *sijainnille*, en käyttöönoton askeleena. Sama > vikaluokka kuin sortRowsin varoitus väärässä tiedostossa: oikea > teksti väärässä kategoriassa ei tavoita lukijaa ennen kuin päätös on jo > tehty.

6. Theme provider

// app/layout.tsx
import { ThemeProvider } from "@karkkainen/ui";
import "./globals.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fi" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

> ⚠️ `defaultTheme`ia ei ole tässä esimerkissä, ja se on tarkoituksellista. > next-themesin oma oletus on enableSystemin kanssa "system", eli > käyttöjärjestelmän teema — ja se on parempi oletus kuin sen > ohittaminen. Rivin puuttuminen on itsedokumentoiva; defaultTheme="system" > näyttäisi valinnalta jonka joku voi myöhemmin "korjata" takaisin. > > Rivi `defaultTheme="light"` oli tässä esimerkissä 8.10.2026 asti. > TP-IAM löysi puutteen. Mitattu: rivi oli viidessä palvelussa, mutta > kolme niistä on vanhempi kuin tämä repo — se tuli TP-Vastaanoton > alkuperäisestä prototyypistä ja README peri sen extractissa. Esimerkki > ei siis ollut kaikkien lähde, mutta se olisi ollut jokaisen seuraavan. > > Jos vaihdat oletuksen jälkikäteen, mittaa ensin tummien tokenien > kattavuus. Palvelu jolla .dark on puutteellinen näyttäisi rikkinäisen > käyttöliittymän käyttäjille jotka eivät ole sitä pyytäneet, ja se on > pahempi kuin epäyhtenäinen oletus. Pelkällä tokens.css:llä kattavuus on > täysi: 28 tokenia :rootissa, 25 .darkissa, ja ne kolme eroa > (--brand, --brand-foreground, --radius) kuuluu pitää samoina. > Mittaa oma repo, koska omat lisätokenit näkyvät vain siellä.

7. Kehykset ja kuori (vaihe D)

Frame-perhe, TableFrame ja AppFrame (0.54.0–0.55.0, päätökset 0003–0005). TP-PERUSTUS siirtyi niihin 22.10.2026 ilman näkyvää muutosta (pikselivertailu 55 näkymää, 0 eroa). Alla ne kohdat, jotka siinä vaativat työtä. Lue ne ennen kuin aloitat.

Versio ja tyylit. Vähintään 0.55.0. theme.css tuo frame.css:n itse; savyt.css vain, jos palvelu käyttää paketin sävyteemoja (<html data-vari="…">).

Tokenit ovat HSL-kolmikoita. --frame, --frame-border, --sidebar, --nav-active ja --nav-active-foreground luetaan muodossa hsl(var(--x)). Jos palvelu on määritellyt jonkin niistä itse kokonaisena värinä (#fafafa, oklch(…)), bg-sidebar ja muut apuluokat ovat virheellisiä eikä selain kerro siitä. Määrittele omat sävysi kolmikkoina: --frame: 192 37.5% 97.4%.

Kerrokset: kaksi sääntöä eri kaskadissa.

  • Kehys itse ([data-frame]: kulmat, reunan väri, varjo) on @layer components -kerroksessa: palvelun apuluokka voittaa sen. Älä siis anna kehykselle shadow-xs-luokkaa tai omaa reunaväriä: ne korvaavat kehyksen varjon ja sisäreunan viivan.
  • Kehyksen taulukko ([data-table-frame-body]: solujen täyte, tasaus, otsakkeen korkeus) on kerrosten ulkopuolella, koska paketin oma TableCell (p-3 align-middle) muuten voittaisi sen. Solun oma px-*-luokka ei siis vaikuta kehyksessä. Yksittäinen toimintosolu keskelle: <div data-cell-center> solun sisällä. Rivien tasaus: TableFrame align="top" tai "middle".
  • Solut eivät rivity (white-space: nowrap), jotta lyhyet arvot pysyvät rivillään. Pitkä vapaa teksti (tuotenimi, kuvaus) merkitään <TableCell data-wrap>: solu rivittyy ja on vähintään 10rem leveä. Ilman merkintää pitkät nimet työntävät muut sarakkeet vaakavierityksen taakse.
  • Rivin tausta tulee kehykseltä. Kerroksettomat säännöt maalaavat solujen taustan ja nollaavat rivin hoverin, joten rivin oma bg-*- tai hover:bg-*-luokka ei näy kehyksessä, eikä siitä tule virhettä. Valittu rivi merkitään data-state="selected" (<TableRow data-state="selected">), ja hover tulee kehykseltä. Jos palvelu korosti valitun rivin luokalla (className="bg-accent"), korostus katoaa siirrossa hiljaa (TP-TTK:n vertaisarvio).

Kaksi kehysmuotoa. Frame ilman muuta = sisennetyt paneelit (FramePanel, useita rinnakkain). Yksi paneeli reunasta reunaan: <Frame className="p-0"> + FrameBody. Mittari: StatFrame. Otsikon rivinkorkeus on leading-tight; jos palvelun oma otsikko oli tavallisella rivillä, anna otsikko <span className="leading-5">-kääreessä, muuten nauha lyhenee 2,5 px.

Kuori: `AppFrame`. Kehykset eivät vaadi kuoren vaihtoa: AppShellissä pysyvä palvelu voi ottaa ne käyttöön sellaisenaan, koska AppShell asettaa --shell-topin itse (3rem, insetissä 3.625rem; PR #7). TableFramen tahmea työkalurivi toimii siis molemmissa kuorissa (TP-MEDIAn vertaisarvio).

Palvelussa ennenAppFramessa
sivupalkin ylälaita (palvelun vaihtaja)sidebarHeader
käyttäjätili sivupalkin alalaidassasidebarFooter; kääre lisää p-2, vähennä oma täyte saman verran
murupolku yläpalkissaheaderStart
haku, kapealla käyttäjätiliheaderEnd
päivitys- tai tilanauha ylimpänäbanner
sivujen oma täytecontentClassName="px-0 pb-0 md:px-0", jos sivut tekevät täytteen itse

AppShellista siirtyvälle, propsi kerrallaan:

AppShellAppFrame
product, services, nav, navFooter, currentPath, LinkComponentsamat
productHreflinkki sidebarHeaderiin (esim. <Link href="/">palvelun nimi</Link>)
user (esim. UserMenu + onLogout)leveällä sidebarFooter, kapealla sama kompaktina headerEndissä, koska sivupalkki on kapealla piilossa
search, actionsheaderEnd
contentClassNamecontentClassName (oletus: keskitetty sisältöalue 1146 px, kääre 1194 px − 2 × 24 px täyte, päätös 0006; max-w-none toimii kuten ennen)
sidebarCollapsibleaina pienennettävä
sidebarStorageKeysidebarStorageKey (sama avain säilyttää käyttäjän valinnan)
variant="inset"AppFrame on aina inset-muotoinen
hideSidebar, defaultSidebarCollapsedei vastinetta; jos tarvitset, ilmoita PR:llä
  • Tahmeat elementit kiinnittyvät kohtaan var(--shell-top), jonka AppFrame asettaa yläpalkin korkeudeksi. Korvaa omat vastaavat muuttujat sillä, ja myös Tailwind-literaalit, jotka olettavat yläpalkin korkeuden: esim. lg:top-16 lg:max-h-[calc(100vh-5rem)] → lg:top-[calc(var(--shell-top)+1rem)] lg:max-h-[calc(100dvh-var(--shell-top)-2rem)] (TP-TTK:n vertaisarvio).
  • Sivupalkin tunniste on tp-app-frame-sidebar. Omat CSS-valitsimet (#oma-sivupalkki nav …) on vaihdettava.
  • AppFramen esilatausskripti asettaa vain pienennetyn tilan (data-sidebar). Jos palvelun oma esilataus asetti muutakin (esim. sävyn), siirrä se juurikuoreen.
  • AppFrame piirtää navigaation itse. Jos palvelu kuunteli navigaation klikkauksia kääreellä (optimistinen korostus), kuuntele niitä asiakirjatasolla sieppausvaiheessa.
  • Vierityssäiliö on sisältökortti, ei ikkuna. window.scrollTo(...) ei tee AppFramessa mitään. Vieritä kortti (lähin vierittävä esivanhempi) tai element.scrollIntoView(). TP-TTK:n mobiilinäkymä, joka vieritti valitun rivin tiedot näkyviin, lakkasi toimimasta hiljaa (TP-TTK:n siirto 22.10.2026).
  • Palvelun nimi pienennettynä: sidebarHeaderissa nimi piiloon luokalla lg:group-data-[sidebar=collapsed]/shell:hidden, jolloin pienennetyssä palkissa näkyy vain merkki.

Todennus. Pikselivertailu nykyistä tuotantoa vasten koeinstansseilla: kaikki sivut, molemmat teemat, kapea näyttö ja pienennetty sivupalkki. Ennen kuin luotat nollaeroon, varmista että sivuilla on dataa: kantavirhe tai tyhjä näkymä antaa myös nollan. TP-PERUSTUKSESSA kaksi vertailua näytti nollaa tyhjällä kannalla, ja vain porttiehto (kantavirhe sivulla kaataa ajon) paljasti sen. Työkalu vertailuun tulee tähän repoon (scripts/kuvavertailu).

  • Datan portti. Staging lukee tuotannon dataa vain luku -oikeudella (dev/staging-malli 22.10.2026), joten se on luonteva todellisen datan vertailukohde. Kiinteä otos (esim. valmiit, tuloksen sisältävät tehtävät) pitää vertailun toistettavana. Kantavirheportti tarvitaan silti: tyhjä näkymä syntyy myös, kun lähde on alhaalla.
  • CSS:n portti. Kerroksettomat säännöt ovat se kohta, jossa Lightning CSS voi pudottaa säännön varoituksella ja next build päättyy silti koodiin 0. Lisää buildiin porttiehto, joka kaatuu riviin "warning while optimizing generated CSS" (TP-TTK:llä Dockerfilessä 21.10.2026 alkaen, todennettu mutaatiolla).

8. Uusi ulkoasu askel askeleelta

Kaikki palvelut tekevät uuden ulkoasun samassa järjestyksessä. Malli on TP-PERUSTUS (22.10.2026), ja jokaisella osalla on esimerkki koodeineen TP-DESIGN-SYSTEM-sivustolla (Kuviot ja Komponentit). Tee askel kerrallaan ja todenna jokainen kuvavertailulla (scripts/kuvavertailu) ennen seuraavaa: silloin tiedät, mikä muutos aiheutti eron.

Järjestys on suositus, ei riippuvuus: askel 3 (kehykset) toimii myös AppShellissä, koska se asettaa --shell-topin (kohta 7). Palvelu, joka on jo aloittanut kehyksistä, jatkaa siitä (TP-TTK teki askeleen 3 ennen askelta 1).

Tila: ✅ paketissa (kuoren osat 0.56.0)

Askel 0. Pohja

  1. ✅ Paketti vähintään 0.55.1 ja Tailwind 4. Tailwind 3 -palvelut tekevät ensin siirron (sivuston Siirtyminen-sivu ja kohdat 2–3 yllä).
  2. ✅ Fontti Geist next/font/localilla muuttujaan --font-sans ja Geist Mono muuttujaan --font-mono (paketin oletus; koodi-, EAN- ja tunnussarakkeet samalla fontilla kaikissa palveluissa). Lukitse geist-paketin tarkka versio ("geist": "1.8.1", ei ^1.8.1), jotta päivää vanha julkaisu ei tule mukaan huomaamatta (TP-TTK 22.10.2026: ^ ratkesi 1.8.2:ksi). Ei geist/font/sans (se asettaa muuttujan --font-geist-sans, jota teema ei lue). Fontin vaihto on omistajan hyväksyttävä palvelukohtaisesti.
  3. ✅ Tumma teema: ThemeProvider, .dark (kohta 6).

Askel 1. Kuori: sivupalkki, yläpalkki ja sisältökortti (`AppFrame`)

Tee ylhäältä alas:

  1. ✅ Sivupalkin ylälaita: palvelun vaihtaja ServiceSwitcher sidebarHeaderiin. Palvelun nimi on painike, ja sen valikossa ovat muut TP-palvelut monogrammeineen, nykyinen merkittynä. Pienennettynä näkyy monogrammi. Sama services-lista kuin AppFramen servicesilla (kapealla yläpalkin AppLauncher).
  2. ✅ Navigaatio: nav (NavGroup[]: otsikko ja kohdat kuvakkeineen, laskuri badge) ja navFooter alalaitaan (esim. Dokumentaatio, Versiohistoria). Ryhmän otsikko ja kohtien värit tulevat paketista.
  3. ✅ Sivupalkin alalaita: oma tili -painike AccountMenu sidebarFooteriin, ja sama compact-muodossa headerEndiin kapealle näytölle. Avatar, nimi ja sähköposti. Valikossa tilin linkit (links), teema (vaalea, tumma, järjestelmä), sävy (shades, jos palvelu tarjoaa) ja uloskirjautuminen (onLogout). Oma tili on tämä TP-PERUSTUKSEN mallin mukainen AccountMenu, ei vanha UserMenu: koko rivi (avatar, nimi, sähköposti, ⇅) on painike, ja teemassa on myös Järjestelmä (omistaja 22.10.2026; TP-TTK:n UserMenu aukesi vain avatarista).
  4. ✅ Yläpalkki: pienennyspainike (AppFrame), murupolku headerStartiin, haku ja kapealla kompakti oma tili headerEndiin. Haku on HeaderSearch (SearchCombobox yläpalkin ulkoasulla, Ctrl+K / ⌘K, kapealla pyöreä hakupainike ja paneeli). Se vie kohteeseen; listan rajaus on erikseen TableFramen työkalurivillä.
  5. ✅ Nauha ylimpänä: banner (esim. päivitysilmoitus).
  6. ✅ Ympäristömerkki: AppFrame environment={process.env.YMPARISTO} → "Koeversio" (staging) tai "Kehitysversio" (dev) murupolun perään. Tuotannossa arvo puuttuu, eikä merkkiä näy. Kuoreen eikä sivuille, koska stagingissa data on samaa kuin tuotannossa eikä niitä muuten erota.
  7. ✅ Vieritys ja tahmeat: --shell-top, vierityssäiliönä kortti (kohta 7).

Askeleen 1 todennus kirjautuneena. Ilman käyttäjää oma tili -painike ja kompakti tili jäävät tyhjiksi, eikä kuva paljasta sitä (TP-TTK:n vertaisarvio). Aja vertailu kirjautuneena: devissä jaetulla testitilillä, tai koeinstanssissa tekaistulla käyttäjällä (TP-PERUSTUKSEN avoimessa tilassa "Operaattori"). Lisää asetuksiin vaadittu, joka tarkistaa käyttäjän nimen sivupalkista.

Askel 2. Sivun alku

  1. ✅ PageHeader: yläotsikko (eyebrow), otsikko, kuvaus ja toiminnot.
  2. ✅ Mittarit rivinä: StatFrame (kuvake, selite, luku, lukuohje).

Askel 3. Sisältö kehyksiin

  1. ✅ Otsikolliset lohkot: Frame ja FramePanel (sisennetyt paneelit) tai Frame className="p-0" ja FrameBody (yksi paneeli reunasta reunaan).
  2. ✅ Taulukot: TableFrame. Työkaluriville haku, suodattimet ja näkymän kytkin Segmented variant="surface", alatunnisteeseen TableFramePagination. Sarakkeet DataTablella, valittu rivi data-state="selected" ja aria-selected="true" (data-state ohjaa vain ulkoasua, ruudunlukija lukee aria-selectedin), toimintosolu data-cell-center.
  3. ✅ Tyhjät tilat: EmptyState.

Askel 4. Tilat ja vuorovaikutus

  1. ✅ Tila taulukon riveillä StatusDot, yksittäinen tila StatusBadge. Aina sanoin, ei pelkkänä värinä.
  2. ✅ Valintaikkunat Dialog ja FormDialog. Rivin tiedot vierelle SidePanel, päälle Sheet. Ilmoitukset Toast.

Askel 5. Viimeistely

  1. ✅ Sävyteemat (0.58.0, kaikille palveluille): savyt.css, ThemeProvider shades ja AccountMenu shades. Valinta kulkee jaetussa tp-savy-evästeessä, joten käyttäjän sävy on sama kaikissa palveluissa (ks. AccountMenu alla).
  2. ✅ Versioilmoitus VersionNotice product="…" kuoren rinnalle. Palvelu tarjoaa reitin /api/versio, joka palauttaa { sha, builtAt } kuvan rakennuksesta.
  3. ✅ Tarkista tumma teema ja kapea näyttö jokaisella sivulla.

Julkaisutahti: askeleet 0–1 yhtenä julkaisuna, 2–3 toisena ja 4–5 kolmantena. Jokaisesta kuvavertailu ja ilmoitus TP-DEVOPSille kuten muistakin vaihdoista. Jos jotain puuttuu paketista, tee se PR:llä pakettiin äläkä palvelun omaksi kopioksi. Juuri omista kopioista vaihe B joutui luopumaan.