Hoppa till innehållet

Devportalen /portal/dev

Startsideportalens ekosystem för människa och agent

Devportalen

Designsystem

Systemet är skrivet som data och renderas här, som JSON och som text. Formen är vald för att en modell ska kunna följa det utan att gissa: varje post har ett stabilt id, uttömmande varianter, villkor för när den ska användas – och det fel som är lätt att göra istället.

Källan är apps/portal/src/app/dev/ui/system/. Ändras en primitiv i packages/ui eller en token i packages/tokens ska posten här ändras i samma commit.

Så läser du systemet

Fyra egenskaper gör systemet läsbart för en modell. De är samtidigt vad som gör det användbart för en människa som har bråttom.

Stabila id

Varje post har ett id som ui.card eller monster.aiyta. Id:t ändras aldrig; en ny form får en ny post. Hänvisa till id, inte till rubriker.

Uttömmande varianter

Alla tillåtna värden står utskrivna. Finns inte värdet i listan finns det inte i systemet – hitta inte på ett nytt.

Villkor, inte smak

"Använd när" och "Använd inte" är formulerade så att valet mellan två poster kan göras på texten allena.

Fel bredvid rätt

Felindexet listar de misstag som faktiskt uppstår, i formen fel → rätt → varför. Det går att kontrollera mot en diff.

Hämta systemet som text

curl -s http://localhost:3000/dev/ui/llms.txt   # hela systemet, plattext
curl -s http://localhost:3000/dev/ui/spec.json  # samma data, maskinläsbart

Bärande regler

Ordningen är prioritetsordning: krockar två regler vinner den som står först.

  • R1Grönt är status – klart, godkänt, positivt – och betyder aldrig AI. AI-ytor tonas med --color-ai-* i blått.

    Grönt betyder klart och godkänt överallt annars. Delar AI tonen med 'klart' blir båda signalerna otydliga – och en grön AI-yta läses som godkänd (ADR-026).

  • R2Allt AI-genererat märks synligt med <AiChip> – texten '✦ AI-genererat' eller en mer precis notis.

    Märkningen är ett löfte till användaren, inte en dekoration. Den får inte kunna glömmas bort.

  • R3Blått betyder identitet: den här personen, den här saken. Avvikande färg betyder status.

    En avatar i rött skulle läsas som att personen är försenad. Identitet och status får därför aldrig dela färgskala.

  • R4Färg kommer alltid ur tokens: inga hex-värden i className. Mått tas ur Tailwinds skala, och där den inte räcker skrivs godtyckliga värden i rem – aldrig i px.

    Ett hårdkodat hex-värde överlever inte nästa temaändring och syns inte i den här specifikationen. Rotens font-size är 62,5 %, så ett px-mått bryter skalningen när användaren ändrar textstorlek – medan rem följer med. Båda halvorna fångas av eslint-regeln startsidan/inga-hardkodade-designvarden.

  • R5Statusfärg bärs av en kantlinje, ett chip eller ett enskilt värde – aldrig av en hel yta. Undantag: dialogstödets områdeskort, där hela kortet fylls i statusens ljusa ton (green-bg, amber-bg, red-bg) utan ram, och statusen alltid står i text.

    Fylls hela kortet i rött tar en enskild avvikelse över hela arbetsytans intryck. Dialogstödets kort är undantaget för att vyn visas i ett möte: där är lägena själva samtalsämnet och ska synas på avstånd. Undantaget gäller inte startsidan eller andra vyer.

  • R6Moduler skriver Tailwind-utilities. CSS i shell.css ägs av skalet och rörs inte av moduler.

    Skalets krom är porterad 1:1 från prototypen. Ändras den från en modul går måtten isär.

  • R7Server components som default. "use client" bara på det som faktiskt behöver interaktion.

    Modulerna hämtar sin egen data på servern (ADR-006). Ett onödigt klientlager tvingar upp datat i props.

  • R8Varje kort får en tillgänglig rubrik: <Card headingId="…" title="…">.

    headingId blir sektionens aria-labelledby. Utan den är kortet en namnlös region för skärmläsaren.

  • R9Svenska i all UI-text, i props-värden som visas, och i commit-ämnen.

    Prototypen visas för kommunens medarbetare. Blandat språk läcker igenom i demolägen.

  • R10TypeScript strict, inga any. Varianter uttrycks som unionstyper, inte som fri sträng.

    Unionen är det som gör varianterna uppräkningsbara – både för kompilatorn och för den här specifikationen.

  • R11Ett kort i main eller side fungerar i båda spalterna. Anpassa efter kortets egen bredd med containerfrågor (@3xl = 48rem), aldrig efter fönstret (md:, lg:).

    Användaren flyttar korten mellan spalterna (ADR-018). Ett kort som bara fungerar brett går sönder i högerspalten, och en fönsterbrytpunkt vet inte vilken spalt kortet står i.

  • R12Allt UI uppfyller WCAG 2.2 nivå AA: allt nås och används med tangentbordet, fokus syns och döljs inte av något klistrat, klickytor är minst 24 px, inget rullar i sidled vid 320 px utom tabeller och kod, och läget sägs med aria (aria-current, aria-expanded), inte bara med färg.

    Kommunens webb omfattas av lagen om tillgänglighet till digital offentlig service. En yta som bara fungerar med mus eller på bred skärm stänger ute medarbetare. Huvudmenyn (ADR-033) är förebilden.

Tokens

Färgen är inte poängen – betydelsen är. Välj token efter vad ytan betyder, aldrig efter hur den ser ut.

Neutraler

Bär all yta och all text som inte har en betydelse att signalera.

  • ink--color-ink · #1f1f25Brödtext och rubriker.
  • ink-soft--color-ink-soft · #4a4a55Metatext, andrahandsinformation.
  • surface--color-surface · #fffKortets botten.
  • page--color-page · #edf1f5Sidans botten, och rader inuti kort.
  • page-deep--color-page-deep · #e4eaf0Ett steg mörkare botten, för helbreddsband som ska läsas som en annan del av sidan än den ovanför.
  • border--color-border · #dde3e9Kortets ram, tunna avdelare.
  • border-strong--color-border-strong · #c3ccd4Ram på interaktiva ytor, t.ex. ghost-knapp.
  • muted-bg--color-muted-bg · #eef0f2Dämpad chipbotten, listavdelare.
  • muted-tx--color-muted-tx · #5a6470Text på dämpad botten.
  • ink-faint--color-ink-faint · #9aa3adPlatshållartext: ett fält som väntar på innehåll. Läses som frånvaro, inte som dämpad information.

Identitet – blå

Blått betyder 'den här personen eller saken'. Aldrig status. En avatar är alltid blå, för en person har ingen status.

  • blue--color-blue · #005595Primärknapp, avatar, aktiv markering.
  • blue-h--color-blue-h · #004070Hovertillstånd för primärknapp.
  • blue-bg--color-blue-bg · #eaf1f8Ljus botten: hover på rader, info-chip.
  • blue-acc--color-blue-acc · #cfe0ecAccentram, t.ex. kort vid hover.
  • blue-tx--color-blue-tx · #004c85Länktext och text på blue-bg.
  • blue-lift--color-blue-lift · #1472b8Identitetsblå mot mörk platta, t.ex. avatar i fokuslägets topprad. Samma betydelse som blue, lyft för att synas.
  • sidebar--color-sidebar · #003c69Endast sidopanelens botten.

Status

Grönt = klart, gult = väntar, rött = tidsgräns eller avvikelse. Statusfärg bärs av en kantlinje, ett chip eller ett värde – aldrig av en hel yta.

  • green--color-green · #12874eKlart, godkänt. Grönt är bara status – aldrig AI (R1).
  • green-bg--color-green-bg · #e8f4ecBotten i chip med tone="ok".
  • green-acc--color-green-acc · #c9e4d7Accentram för klart-tillstånd.
  • green-tx--color-green-tx · #0c6a3cText på green-bg.
  • amber--color-amber · #b06a1fVäntar på någon annan.
  • amber-bg--color-amber-bg · #fbeedeBotten i chip med tone="warn".
  • amber-tx--color-amber-tx · #8a5117Text på amber-bg.
  • red--color-red · #b3261eTidsgräns passerad, avvikelse.
  • red-bg--color-red-bg · #fcecebBotten i chip med tone="avv".

AI – blå toner

Reserverad

Tonen för ytor där AI gör något: AI-knapp, AI-fält, AI-kort och AiChip. Blå toner, ljusare än identitetsblått (ADR-026). Färgen är inte signalen – ✦-märkningen är det (R2). Använd aldrig --color-ai-* för status, och aldrig grönt för AI.

  • ai--color-ai · #1f64b8AI-knapp, AI-märke, AI-fältets fokusram.
  • ai-deep--color-ai-deep · #164c8fHovertillstånd på AI-knapp.
  • ai-bg--color-ai-bg · #eef4fcBotten på AI-tonade kort och chips.
  • ai-acc--color-ai-acc · #c9dcf3Ram på AI-tonade ytor.
  • ai-tx--color-ai-tx · #164c8fText på ai-bg.

Diagram

Serierna i ett diagram får färg i den här ordningen och högst fyra – fler serier slås ihop till Övrigt eller delas i flera diagram. Färgen följer serien, aldrig dess rang. Inget grönt: grönt är status. Text i diagram är alltid ink eller ink-soft, aldrig seriens färg, och diagrammet har alltid en tabellvy.

  • graf-1--color-graf-1 · #2a78d6Första serien.
  • graf-2--color-graf-2 · #eb6834Andra serien.
  • graf-3--color-graf-3 · #4a3aa7Tredje serien.
  • graf-4--color-graf-4 · #e87ba4Fjärde serien. Under 3:1 mot vitt – kräver synliga etiketter eller tabell.

Typografi

Rotens font-size är 62,5 %, så 1 rem = 10 px och Tailwinds skala behåller sin px-semantik: p-4 är 16 px. Skriv aldrig text-[14px] när text-sm finns.

  • text-eyebrow--text-eyebrow · 1.1rem (11px)SundsvallVersal etikett över ett nyckeltal.
  • text-xs--text-xs · 1.2rem (12px)SundsvallMetatext, chips, sidfot.
  • text-sm--text-sm · 1.4rem (14px)SundsvallRadrubriker, knappar.
  • text-base--text-base · 1.6rem (16px)SundsvallBrödtext.
  • text-h2--text-h2 · 1.7rem (17px)SundsvallKortrubrik. Sätts av Card, inte av modulen.
  • text-lg--text-lg · 1.8rem (18px)SundsvallAI-fältets inmatning, ingress.
  • text-kpi--text-kpi · 3rem (30px)SundsvallNyckeltalets värde.

Form och djup

  • rounded-xs--radius-xs · 0.6rem (6px)Chip, rad inuti kort.
  • rounded-sm--radius-sm · 0.8rem (8px)Knapp.
  • rounded-md--radius-md · 1rem (10px)shadcns `--radius`; sällan direkt.
  • rounded-lg--radius-lg · 1.6rem (16px)Kort och nyckeltalsruta.
  • shadow-card--shadow-card · två lager, mjukAllt som ligger på arbetsytan.
  • shadow-pop--shadow-pop · djupMenyer som svävar över ytan.
  • max-w-wrap--container-wrap · 128rem (1280px)Innehållsbredd. Sätts av `.wrap`.

Rörelse

All rörelse slås av under prefers-reduced-motion – det är redan gjort i basreglerna och ska inte kringgås.

  • animate-msg-in--animate-msg-in · 0.25s easeNytt meddelande i ett samtalsflöde.
  • animate-blink--animate-blink · 1.2s infiniteAssistenten skriver.
  • animate-flash--animate-flash · 1s easeFält som AI just fyllt i formulärspegeln.

Primitiver

Byggstenarna i packages/ui. En modul ska klara sig på dessa – behöver du något annat är det ett tecken på att systemet saknar en primitiv, inte på att modulen ska hitta på en egen.

Card

ui.card

Vitt kort med mjuk skugga – grundformen för allt innehåll på arbetsytan.

Använd när Varje gång en modul lägger något i sloten main eller side.

Använd inte För nyckeltal (använd nyckeltalsrutan) och inuti ett annat kort – kort staplas inte i kort.

Import

import { Card } from "@startsidan/ui";

Exempel

Mina ärenden

4

Kortets kropp. Innehållet är modulens.

Kod

<Card
  headingId="arenden-h"
  title="Mina ärenden"
  count={4}
  action={<MoreLink href="/flode/arenden">Visa alla →</MoreLink>}
>
  <ul className="grid list-none">{/* ListRow-rader */}</ul>
</Card>

Props

PropTypStandardBetydelse
titleReactNode–Rubrik i kortets huvud. Utelämnas för kort utan huvud.
countReactNode–Antal bakom rubriken, t.ex. 4 eller "2/4". Renderas med punkt före.
actionReactNode–Länk längst till höger i huvudet, normalt <MoreLink>.
headingIdstring–Id på rubriken. Blir sektionens aria-labelledby. Obligatorisk i praktiken.
tone"default" | "ai"defaultaidefaultai ger AI-tonad botten och ram (blå toner). Endast för AI-ytor.
bodyClassNamestring–För innehåll som bär egen luft, t.ex. en tabell som ska gå kant i kant.
classNamestring–Skriver över kortets egna utilities via tailwind-merge.

Regler

  • Rubriken sätts av Card. Skriv aldrig en egen <h2> inuti kortet.
  • tone="ai" kräver att innehållet faktiskt är AI-genererat (R1, R2).
  • Kortet är en <section>; headingId gör den namngiven (R8).
  • Huvudet radbryter: blir det trångt hamnar action under rubriken, och antalet följer rubrikens sista ord (ADR-018).

Vanligt fel

  • Fel

    <Card className="rounded-xl">

    Rätt

    <Card>

    rounded-lg är V3:s kortform och sätts redan; rounded-xl är shadcns default som medvetet skrivs över.

Chip

ui.chip

Litet statusmärke med punkt före texten.

Använd när När en rad eller ett kort har ett tillstånd som får plats i ett ord.

Använd inte För att märka AI-innehåll – där gäller AiChip. Och aldrig som knapp.

Import

import { Chip, type ChipTone } from "@startsidan/ui";

Exempel

KlartVäntar på chefUnder handläggningUtkastFörsenad

Kod

<Chip tone="ok">Klart</Chip>
<Chip tone="warn">Väntar på chef</Chip>
<Chip tone="avv">Försenad</Chip>

Props

PropTypStandardBetydelse
toneChipToneokwarninfomutedavvmutedok = klart, warn = väntar, info = neutral upplysning, muted = utan laddning, avv = avvikelse eller passerad tidsgräns.
childrenReactNode–Etiketten. Ett till två ord på svenska.

Regler

  • Tonen ska följa av data, inte väljas för utseendet.
  • ok använder statusgrönt (green-bg/green-tx). AI-tonen är blå och betyder aldrig status (R1).

AiChip

ui.aichip

Märkning av AI-genererat innehåll.

Använd när På allt en modell har skrivit, sammanfattat, föreslagit eller fyllt i – utan undantag.

Använd inte På innehåll som kommer ur ett API eller en regel. Då är märket en osanning.

Import

import { AiChip } from "@startsidan/ui";

Exempel

✦ AI-genererat✦ Föreslagen prioritet

Kod

<AiChip />
<AiChip>✦ Föreslagen prioritet</AiChip>

Props

PropTypStandardBetydelse
childrenReactNode✦ AI-genereratErsätt standardtexten när en mer precis notis säger mer, t.ex. "✦ Sammanfattad av AI".

Regler

  • Får aldrig utelämnas på AI-ytor (R2).
  • Stjärnan ✦ är märkets tecken och byts inte ut.

Btn / BtnLink

ui.btn

Knappen i V3:s mått. BtnLink är samma form som ankare.

Använd när Btn för något som händer på sidan, BtnLink för något som leder vidare.

Använd inte Använd inte shadcns Button direkt – den saknar V3:s mått och varianten ai.

Import

import { Btn, BtnLink } from "@startsidan/ui";

Exempel

Felanmäl

Kod

<Btn>Godkänn</Btn>
<Btn variant="ghost" size="sm">Visa detaljer</Btn>
<Btn variant="ai">✦ Sammanfatta åt mig</Btn>
<BtnLink href="/flode/felanmalan">Felanmäl</BtnLink>

Props

PropTypStandardBetydelse
variantBtnVariantprimaryghostaiprimaryprimary = blå, huvudhandlingen. ghost = vit med bård, sidohandling. ai = AI-blå, endast för handlingar som utför något AI-genererat.
sizeBtnSizemdsmmdsm används i rader och korthuvuden.

Regler

  • Ett kort har högst en primary. Fler och blicken tappar riktning.
  • variant="ai" kräver att knappen startar något AI gör (R1).
  • Fokus ritas av basens outline: 2 px i ring-färgen, utanför knappen. Släck den aldrig med outline-none (R12).

StatusRow

ui.statusrow

Rad med färgad vänsterkant: titel, meta och handlingar till höger.

Använd när I "Att agera på" och överallt där raden i sig har ett tillstånd.

Använd inte I en vanlig innehållslista utan tillstånd – där gäller ListRow.

Import

import { StatusRow, type RowStatus } from "@startsidan/ui";

Exempel

  • Attestera tidrapportTidsgräns passerad · 2 dagarAttestera
  • Ledighetsansökan – Kim HedlundInkom 12 marsVäntarÖppna
  • Medarbetarsamtal med Alex LundBokat 3 april 10:00✦ Underlag klart
  • BrandutbildningGenomförd 2 marsKlart

Kod

<ul className="grid list-none gap-2">
  <StatusRow
    status="krit"
    title="Attestera tidrapport"
    meta="Tidsgräns passerad · 2 dagar"
    end={<BtnLink href="#" size="sm">Attestera</BtnLink>}
  />
</ul>

Props

PropTypStandardBetydelse
statusRowStatuskritwarninfookneutralneutralkrit = röd kant (tidsgräns), warn = gul, info = blå, ok = grön, neutral = grå.
titleReactNode–Vad raden gäller.
metaReactNode–När, av vem, varifrån.
endReactNode–Chips och knappar, i ordningen AiChip → Chip → knapp.

Regler

  • Renderas som <li> och måste ligga i en <ul>.
  • Kanten bär statusen; resten av raden förblir lugn (R5).
  • Blir raden för trång bryts handlingarna (end) till en egen rad under titeln – så fungerar den i båda spalterna (R11).

ListRow / Progress

ui.listrow

Rad i en lista inuti ett kort: tunn avdelare, ingen ram. Progress är förloppet under raden.

Använd när För innehållslistor: utvecklingsplaner, utbildningar, ärenden utan tabell.

Använd inte När raden har ett tillstånd som ska synas i kanten – då StatusRow.

Import

import { ListRow, Progress, Avatar } from "@startsidan/ui";

Exempel

  • Utvecklingsplan 2026Uppdaterad 12 mars
    Väntar
  • Ledarskap i förändringAvslutad 2 mars
    Klart

Kod

<ul className="grid list-none">
  <ListRow
    lead={<Avatar initials="AL" />}
    title="Utvecklingsplan 2026"
    meta="Uppdaterad 12 mars"
    end={<Chip tone="warn">Väntar</Chip>}
  >
    <Progress value={60} />
  </ListRow>
</ul>

Props

PropTypStandardBetydelse
leadReactNode–Något framför titeln, normalt <Avatar>.
titleReactNode–Radens rubrik.
metaReactNode–Understödjande text.
endReactNode–Chip eller värde längst till höger.
childrenReactNode–Extra innehåll under metaraden, normalt <Progress>.
value (Progress)number–0–100. Klipps automatiskt till intervallet.
done (Progress)booleanfalseGrön stapel när klar, annars blå.

Regler

  • Första raden får ingen toppluft och sista ingen avdelare – det sköts av primitiven.
  • Progress är Radix under ytan; bygg aldrig en egen div med role="progressbar".

Avatar

ui.avatar

Identitetsikon med initialer.

Använd när När en rad handlar om en bestämd person.

Använd inte Som statusmarkör. En person har ingen status (R3).

Import

import { Avatar } from "@startsidan/ui";

Exempel

Kod

<Avatar initials="KH" />
<Avatar initials="AL" size="sm" />

Props

PropTypStandardBetydelse
initialsstring–Ett till två versaler.
size"sm" | "md"smmdmdsm i täta listor.

Regler

  • Alltid blå. Ingen variantprop för färg finns, och ska inte tillkomma.

Table

ui.table

shadcns tabell, vidarelänkad för moduler som behöver rader med kolumner.

Använd när När data har fler än två jämförbara fält per rad, som i modulen arenden.

Använd inte För listor med en rubrik och en metarad – där räcker ListRow.

Import

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

Exempel

ÄrendeInkomStatus
Behörighet Heroma12 marsVäntar
Trasig projektor, rum 3.1410 marsÅtgärdat

Kod

<Card headingId="arenden-h" title="Mina ärenden" bodyClassName="px-0 pb-0">
  <Table>
    <TableHeader>
      <TableRow><TableHead>Ärende</TableHead><TableHead>Status</TableHead></TableRow>
    </TableHeader>
    <TableBody>
      <TableRow><TableCell>Behörighet Heroma</TableCell><TableCell><Chip tone="warn">Väntar</Chip></TableCell></TableRow>
    </TableBody>
  </Table>
</Card>

Regler

  • Tabellen bär egen luft: ge kortet bodyClassName så att raderna går kant i kant.
  • Kolumnspåret måste kunna krympa (min-width: 0), annars trycker tabellen ut sidan.

Mönster

Sammansättningar som återkommer. Mönstren är inte kod att importera utan form att följa; två moduler som löser samma sak ska se likadana ut.

Nyckeltalsruta

monster.nyckeltal

Ett mätvärde med versal etikett, stort tal och en kommentar. Hela rutan är en länk.

Använd när I sloten kpi, som spänner över hela arbetsytans bredd i fyra spår.

Använd inte För något som inte är ett tal. En status hör hemma i ett chip.

Kod

<a href={post.lank} className="block rounded-lg border border-border bg-surface px-6 py-5 text-ink no-underline shadow-card transition-[transform,border-color] duration-150 hover:-translate-y-0.5 hover:border-blue-acc">
  <span className="flex items-center justify-between gap-2 font-heading text-eyebrow font-bold tracking-[0.12em] text-ink-soft uppercase">
    <span className="hyphens-auto [overflow-wrap:anywhere]">Budgetutfall</span>
    <i aria-hidden="true" className="shrink-0 text-sm not-italic">→</i>
  </span>
  <span className="mt-2.5 mb-1 block font-heading text-kpi leading-[1.1] font-extrabold text-green-tx">
    +2,4<small className="ml-1 text-sm font-bold text-ink-soft">%</small>
  </span>
  <span className="block text-xs text-ink-soft">Mot budget hittills i år</span>
</a>

Regler

  • Värdet bär tonen (text-blue-tx, text-green-tx, text-amber), aldrig hela rutan (R5).
  • Etiketten avstavas hellre än kapas – långa etiketter förekommer.

AI-yta

monster.aiyta

Ett kort där innehållet är maskinskrivet: AI-tonad botten, ram och märke.

Använd när Sammanfattningar, förslag och allt annat en modell producerat och som visas som innehåll.

Använd inte För något som bara handlar om AI. Ett kort som beskriver assistenten är vanligt innehåll.

Exempel

Din vecka

✦ AI-genererat

Tre ärenden väntar på dig, varav ett har passerat sin tidsgräns.

Kod

<Card headingId="sammanfattning-h" tone="ai" title="Din vecka" action={<AiChip />}>
  <p className="text-sm text-ink-soft">
    Tre ärenden väntar på dig, varav ett har passerat sin tidsgräns.
  </p>
  <div className="mt-4 flex gap-2">
    <Btn variant="ai" size="sm">✦ Föreslå ordning</Btn>
    <Btn variant="ghost" size="sm">Visa källor</Btn>
  </div>
</Card>

Regler

  • tone="ai" och AiChip hör ihop: den ena utan den andra är ett fel (R1, R2).
  • En AI-yta ska kunna peka på sin källa – lämna plats för en källrad eller en länk.

Att agera på

monster.agera

Kortet som aggregerar andra modulers actions till en enda handlingslista.

Använd när Bidra med rader genom slots.actions i modulens manifest. Rendera aldrig ett eget sådant kort.

Kod

// packages/modules/<namn>/src/manifest.ts
slots: {
  actions: [
    {
      id: "ledighet-attest",
      roles: ["chef"],
      items: async (ctx) => [
        {
          id: "att-1",
          status: "warn",
          title: "Attestera ledighet – Kim H",
          meta: "Inkom 12 mars",
          chip: { tone: "warn", label: "Väntar" },
          action: { href: "/flode/ledighet", label: "Attestera" },
        },
      ],
    },
  ],
}

Regler

  • Modulen äger raden, kortet äger ordningen (ADR-007).
  • status på raden ska följa av data: passerad tidsgräns → krit, väntar på någon → warn.

Modulkort

monster.modulkort

Den vanligaste formen: en server component som hämtar sin data och renderar ett Card.

Använd när Som mall för varje nytt kort i sloten main eller side.

Kod

import { Card, Chip, ListRow, MoreLink } from "@startsidan/ui";
import type { UserContext } from "@startsidan/module-sdk";
import { hamtaNagot } from "../data";

export async function NagotKort({ ctx }: { ctx: UserContext }) {
  const { poster } = await hamtaNagot(ctx);

  return (
    <Card
      headingId="nagot-h"
      title={ctx.role === "chef" ? "Enhetens ärenden" : "Mina ärenden"}
      count={poster.length}
      action={<MoreLink href="#">Visa alla →</MoreLink>}
    >
      <ul className="grid list-none">
        {poster.map((p) => (
          <ListRow key={p.id} title={p.rubrik} meta={p.kommentar}
            end={<Chip tone={p.chip.ton}>{p.chip.etikett}</Chip>} />
        ))}
      </ul>
    </Card>
  );
}

Regler

  • Ingen "use client". Datat hämtas på servern (R7, ADR-006).
  • Rolltexten väljs i komponenten, inte i två olika manifestposter.
  • key sätts på datats id, aldrig på index.
  • Kortet fungerar i båda spalterna (R11). Användaren kan flytta det; anpassa med @3xl:, inte md:. Ge widgeten en label i manifestet – den läses upp på flyttknapparna.

Fokusyta

monster.fokus

En nyhet i startsidans främsta plats, direkt under AI-fältet, för något alla ska se – tills de stänger den (ADR-021).

Använd när En viktig nyhet eller en förändring som rör allas arbete. Bidra genom slots.fokus; skalet renderar och äger stängningen.

Använd inte För vanliga nyheter – de hör till nyhetskortet. Och aldrig för något som kräver en handling: det är en notis eller en rad i "Att agera på".

Kod

// packages/modules/<namn>/src/manifest.ts
slots: {
  fokus: async (ctx) => ({
    id: "fokus-ny-startsida-2026-10",   // nytt id = visas igen för alla
    kicker: "Fokusnyhet",
    title: "Ny startsida ska göra arbetsdagen enklare för medarbetare och chefer",
    lede: "Från och med i dag möts alla medarbetare av en ny startsida …",
    image: { src: "https://…/bild.png", alt: "" },      // dekorativ: tom alt
    links: [{ label: "Läs hela nyheten", href: "/verktyg/nyheter/ny-startsida", primary: true }],
    from: "Kommunikationsenheten",
    published: "2026-10-05",
  }),
}

Regler

  • Bara en fokusyta åt gången – den nyaste som inte är stängd.
  • Formad som en nyhet: bild, datum, rubrik, ingress, avsändare – och en primär länk till hela nyheten. Rubriken länkar dit också.
  • Ljusblå botten (blue-bg) och blå ram (blue-acc), så att den sticker ut bland de vita korten. Aldrig grönt (R1) eller statusfärg (R5).
  • Ligger direkt under AI-fältet. Ovanför hälsningen läses den som en introduktion, inte som en nyhet.
  • Stäng flyttar fokus till AI-fältet, så att tangentbordsanvändaren inte tappar bort sig.

Notis

monster.notis

Något som väntar på användaren, i klockan i sidopanelen. Leder alltid till funktionen där det görs (ADR-020).

Använd när När något har hänt eller väntar som användaren inte annars ser – ett samtal att godkänna, en tidsgräns, en ny nyhet. Bidra genom manifestets notifications.

Använd inte För sådant som redan står på startsidan i "Att agera på" och inte har en egen funktion att leda till. En notis utan href är brus.

Kod

// packages/modules/<namn>/src/manifest.ts
notifications: async (ctx) => [
  {
    id: "personal:n-godkann-us-108",     // stabilt – läst sparas på id
    kind: "Utvecklingssamtal",           // funktionen, visas som etikett
    title: "Utvecklingssamtal att godkänna",
    meta: "Hanna Sjöberg",
    time: "2026-10-03",
    href: "/verktyg/personal/m-108/utvecklingssamtal",
    status: "warn",                      // kantlinjen: krit, warn, info, ok
  },
]

Regler

  • href är obligatoriskt och pekar på funktionen, inte på en översikt.
  • Räkna fram notisen ur datat när det går, så att den försvinner när saken är gjord.
  • Statusfärgen bärs av kantlinjen, som i "Att agera på" (R5).

Sida: ingång och artikel

monster.sida

Redaktionellt innehåll under "Sidor" (ADR-003). Skalet äger layouten; modulen lämnar ett PageManifest och innehållet som block.

Använd när När innehållet är text och länkar som alla läser, inte uppgifter för en roll. layout: "hub" för en sektionsingång med snabbvägar, "article" för en innehållssida under den.

Använd inte För en roll eller en uppgift – det är en widget eller ett fokusflöde. Och aldrig med en egen renderare eller egen sidtyp (AM1).

Kod

// packages/modules/<namn>/src/manifest.ts
pages: [
  {
    slug: "it-support",
    section: "service-it-lokaler",       // id ur skalets sektionslista
    layout: "hub",
    title: "IT-support",
    lede: "Hjälp med dator, telefon, konton och behörigheter.",
    quickLinks: [{ label: "Felanmäl", text: "Skapa ett ärende", href: "/flode/felanmalan", primary: true }],
    ai: { suggestions: () => [{ label: "Jag har glömt mitt lösenord" }] },
  },
  {
    slug: "prioritering",
    section: "service-it-lokaler",
    parent: "it-support",                  // ger /sidor/service-it-lokaler/it-support/prioritering
    layout: "article",
    title: "Prioriteringsordning för IT-ärenden",
    kicker: "IT-support · Styrande information",
    article: { owner: { name: "IT-avdelningen" }, review: "Granskas årligen" },
    content: async () => ({ blocks: [{ type: "heading", level: 2, text: "Varför prioriterar vi?" }] }),
  },
]

Regler

  • Brödtexten är högst 72ch bred, i ett vitt kort på artikeln och som prose i kort på ingången.
  • Innehåll från en extern källa märks alltid med källa, datum och länk till originalet. Märkningen är inte AI-tonad – den är inte AI (R2).
  • En sparad version märks med ett gult chip: den väntar på källan (R5).
  • Innehållet är ContentBlock, aldrig HTML. react/no-danger är en lintgrind (AM12).
  • Länkar till en annan sida i startsidan går via next/link; externa öppnas i ny flik med ↗.
  • Vanliga frågor och nivåer (P1–P4) är blocket details: native <details>, ett valfritt chip för status, block inuti (ADR-022).
  • Levande innehåll på en ingång – driftstatus, ärenden – är modulens egna kort i PageManifest.slots, inte text. Redaktionellt är block, i content.cards.
  • En inramad sida (layout "embed") fyller arbetsytan under menyraden, utan egen rubrik eller kant – som om man var på sidan. Sidans h1 finns för skärmläsare. Blockerar källan ramen visas en förklaring med gult chip, aldrig en tom ram.
  • En länk med undersidor (PageLink.children) fälls ihop; första raden inuti är sidan själv. Använd det för webbkartor i stället för en egen lista.

Kommunens avsändare i toppraden

monster.varumarke

Visa att sidan är Sundsvalls kommuns, med samma grundstruktur som kommunikationsguidens sidmall: logotyp, en lodrät linje och produktens namn. Hela raden är en länk till produktens första sida.

Använd när I projektportalens topprad (projektyta/Topprad) på alla dess sidor – roten, status, funktionerna, tidslinjen, materialet, visionen och rösten – i devportalens topprad, och på varje ny fristående yta som visas utanför skalet.

Använd inte Inte i skalet (/start och modulvyerna): där är sidopanelen avsändaren. Och aldrig som rubrik inne på en sida.

Import

import { Varumarke } from "@startsidan/gemensamt/Varumarke"; // apps/projekt/src/varumarke

Kod

<header className="border-b border-border bg-surface">
  <div className="mx-auto flex max-w-[104rem] items-center justify-between gap-4 px-4 py-3 sm:px-8">
    <Varumarke produkt="Min startsida" />
    {/* ytans knappar till höger; menyn i en egen rad under, se monster.huvudmeny */}
  </div>
</header>

Props

PropTypStandardBetydelse
produktstring–Produktens namn efter linjen, t.ex. "Min startsida".
hrefstring"/"Produktens första sida.
etikettstring"till första sidan"Dold text efter det synliga namnet, så att länkens namn börjar med det som syns (WCAG 2.5.3).

Regler

  • Ordningen är alltid logotyp, lodrät linje, produktnamn – som i kommunikationsguidens topbar.
  • Logotypen är kommunens fil i public/brand och byts aldrig mot text, en annan färg eller en egen bild.
  • Måtten är mallens: logotypen 4rem hög, linjen 3.2rem i border-strong och namnet 1.9rem extrabold Raleway; under sm 3.2rem, 2.6rem och 1.5rem så att raden får plats på 375 px.
  • /brand ligger utanför åtkomstspärren, eftersom avsändaren syns på de öppna sidorna.

Sidans hero

monster.hero

Ge varje sida i projektets yta samma början: eyebrow, rubrik och ingress på ett ljust gråblått band direkt under toppraden. Den aktiva fliken i huvudmenyn har samma färg och blöder in i bandet, så att det syns var man är (ADR-033).

Använd när Överst på varje sida med <Topprad />, direkt efter toppraden eller brödsmulorna. Sidans sammanfattande delar – siffror, lägen, lagren – får ligga i den.

Använd inte Inte i skalet eller modulvyerna. Inte för en enskild händelse som medvetet bryter profilen (workshopens mörka hero). Och aldrig med vit botten – heron är alltid --color-page-deep.

Import

import { Hero } from "../projektyta/Hero"; // apps/projekt/src/projektyta

Kod

<Hero>
  <Eyebrow>Status</Eyebrow>
  <h1 className="max-w-[22ch] font-heading text-[3.6rem] leading-[1.08] font-extrabold text-ink sm:text-[4.2rem]">
    Vad har hänt i projektet?
  </h1>
  <p className="max-w-[66ch] text-lg text-ink-soft">…</p>
</Hero>
<div className="mx-auto grid max-w-[104rem] gap-10 px-4 pt-10 pb-16 sm:px-8">…</div>

Props

PropTypStandardBetydelse
classNamestring–Gäller rutnätet inuti: avstånd, spalter eller en annan bredd (devportalen 132rem).

Regler

  • Bandet är --color-page-deep med border-b border-border; innehållet under ligger på --color-page.
  • Heron står direkt under toppraden, eller under brödsmulorna som har samma band – annars blöder fliken in i fel yta.
  • Länkar i ingressen stryks under (WCAG 1.4.1).

Projektets huvudmeny

monster.huvudmeny

Visa alla delar av tjänsten i en klassisk meny som uppfyller WCAG 2.2 AA (ADR-033): efter header A: en rad dockad mot toppradens underkant under avsändaren, grå text utan bakgrund, sidan man är på som en flik i heroens färg som blöder in i heron, grupper med en enkel lista – och under md en Meny-knapp med samma lista, en post per rad.

Använd när På varje sida i projektportalen, genom <Topprad />. En ny sorts innehåll läggs som en sida i projektyta/meny.ts, i raden eller i en grupp. Den som är inloggad med ett konto ser sin identitetsikon i övre högra hörnet, med vägen till kontot och för admin till kontona (projektyta/Kontomeny, ADR-039).

Använd inte Inte för enskilda tillfällen – ett möte blir en händelse under Tidslinjen, en rapport en post under Status. Inte i skalet, där sidopanelen är navigeringen. Och aldrig som flikar som rullar i sidled.

Import

import { Topprad } from "../projektyta/Topprad"; // apps/projekt/src/projektyta

Kod

// projektyta/meny.ts – en ny sida i en grupp
{ href: "/material/ordlista", namn: "Ordlistan" },

// Sidan själv: toppraden från layouten, innehållet i main
<Topprad />
<main id="innehall" tabIndex={-1} className="focus:outline-none">…</main>

Regler

  • Disclosure-mönstret, inte role="menu": länkar i listor, grupper som knappar med aria-expanded och aria-controls.
  • Aktuell sida får aria-current="page" och ett streck – aldrig bara en annan färg.
  • Varje sida med toppraden har <main id="innehall">, så att hopplänken har ett mål.
  • Det som klistrar under toppraden använder top-(--topprad-hojd), aldrig en fast höjd. Toppraden klistrar bara från lg.
  • Sidor bakom åtkomstspärren får last: true, så att menyn säger det före klicket.
  • Ingen fet stil i raden: font-sans text-sm text-ink-soft, ljus botten vid hovring. Fliken man står på har heroens färg och blöder in i <Hero> (monster.hero). Raden bryts hellre än rullar.

Ytor

Det skalet äger. En modul rör dem inte direkt – den anmäler sig via sitt manifest och får en plats.

Arbetsytan

yta.arbetsyta

Rutnätet skalet lägger ut åt modulerna: en helbreddsrad för nyckeltal och två spalter.

Använd när Ägs av skalet. En modul väljer bara slot i sitt manifest.

Kod

/* apps/portal/src/shell/shell.css – skalets, inte modulernas */
.kpis  { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2rem; }
.board { grid-template-columns: minmax(0, 1fr) 36rem; gap: 2rem; }
.col   { grid-template-columns: minmax(0, 1fr); gap: 2rem; min-width: 0; }

/* I modulens manifest: */
slots: { kpi: [...], main: [...], side: [...] }

Regler

  • kpi = fyra spår över hela bredden. main = vänsterspalten. side = högerspalten (36 rem).
  • Under 1180 px blir båda rutnäten enspaltiga. Bygg kortet så att det tål det.
  • Spåren har min-width: 0 – bryts det trycker en bred tabell ut hela sidan (R6).

AI-fältet

yta.aifalt

Skalets AI-yta överst på startsidan: hälsning, fråga och rollens förslag.

Använd när Ägs av skalet. En modul bidrar med förslagschips via ai.suggestions(ctx) i sitt manifest.

Kod

// packages/modules/<namn>/src/manifest.ts
ai: {
  suggestions: (ctx) =>
    ctx.role === "chef"
      ? [{ id: "godkann", label: "Godkänn veckans ledighetsansökningar" }]
      : [{ id: "felanmal", label: "Felanmäl något på kontoret" }],
}

Regler

  • Fältet är runt (border-radius 999px) och AI-kantat, med ✦ – formen och märket betyder AI (R2).
  • Förslagen ska vara handlingar användaren faktiskt kan utföra just nu.

Fokusflöde

yta.fokus

Helskärm utan sidopanel: smal topprad med Avbryt, ett ärende från början till slut.

Använd när För flöden som kräver odelad uppmärksamhet, som felanmälan. Deklareras med routes i manifestet.

Använd inte För något som ryms i ett kort. Fokusläget tar hela skärmen och kostar därefter.

Kod

// packages/modules/<namn>/src/manifest.ts
routes: [
  {
    path: "/flode/felanmalan",
    title: "Felanmälan",
    mode: "focus",
    Component: FelanmalanFlode,
  },
]

Regler

  • mode: "focus" ger FlowBar med Avbryt och inget annat krom (ADR-004).
  • Samtalet är inmatningen, formuläret är kvittot: visa i realtid vad assistenten uppfattat.
  • Ärendet skapas aldrig utan uttrycklig bekräftelse – och servern kontrollerar själv att allt obligatoriskt finns.

Modulvy över hela ytan

yta.full

En arbetsplats man stannar i: sidopanelen kvar, vyn tar hela arbetsytan i skärmens höjd. Som en chattjänst – lista längs kanten, innehållet i full höjd.

Använd när När vyn själv är verktyget och behöver bredden – Min AI, och nästa steg ett dokument bredvid samtalet. Deklareras med routes och mode: "full" (ADR-025).

Använd inte För en vy som läses uppifrån och ned, som en personalakt – den är mode: "standard". För ett ärende från början till slut med Avbryt – det är fokusflödet.

Kod

// packages/modules/<namn>/src/manifest.ts
routes: [
  { path: "/verktyg/min-ai", title: "Min AI", mode: "full", Component: MinAiVy },
],
slots: {
  // Ingången: en knapp bredvid hälsningen. ai: true ger AI-tonen och ✦.
  genvag: [{ label: "Min AI", href: "/verktyg/min-ai", order: 1, ai: true }],
}

Regler

  • Vyn äger höjden och scrollningen: fyll ytan med flex och låt bara listor och innehåll scrolla (min-h-0, overflow-y-auto).
  • Läsbar text hålls i en spalt (högst cirka 78rem) – inte över hela bredden. Ytan bredvid är för en panel, inte för längre rader.
  • Kolumnen längs kanten blir en låda på smal skärm, öppnad med en knapp med aria-expanded och stängd med Escape.
  • Ingen rubrik eller ingress ovanför – vyn är ytan. Det vyn behöver säga hör hemma i dess egna fält.

Felindex

De misstag som faktiskt uppstår när något byggs snabbt i den här kodbasen. Läs det som en granskningslista.

  • Fel

    className="bg-ai-bg" på ett kort som visar "Klart"

    Rätt

    <Chip tone="ok">Klart</Chip> på ett vanligt kort

    AI-tonen är för AI, grönt är för status – en AI-tonad yta säger inte klart (R1).

  • Fel

    className="text-[#005595]"

    Rätt

    className="text-blue"

    Hårdkodad färg går förbi tokens och syns inte i temat (R4). Fångas av yarn lint.

  • Fel

    className="text-[14px]" – eller vilket px-mått som helst i hakparentes

    Rätt

    className="text-sm", eller className="border-l-[0.3rem]" när skalan inte räcker

    Rotens font-size är 62,5 %, så px bryter skalningen när användaren ändrar textstorlek. Rem följer med (R4). Fångas av yarn lint.

  • Fel

    <Button> importerad från shadcn-mappen

    Rätt

    <Btn> ur @startsidan/ui

    Btn bär V3:s mått och varianten ai. shadcns Button är grunden under, inte gränssnittet utåt.

  • Fel

    <Card> utan headingId

    Rätt

    <Card headingId="arenden-h" title="Mina ärenden">

    Kortet renderas som <section> och behöver aria-labelledby (R8).

  • Fel

    AI-genererad sammanfattning renderad som vanlig text

    Rätt

    Samma text med <AiChip> intill, eller i ett kort med tone="ai"

    Omärkt AI-innehåll bryter mot löftet till användaren (R2).

  • Fel

    <Avatar> i grön eller röd variant

    Rätt

    <Avatar initials="AL" /> – alltid blå, status läggs bredvid

    Identitet och status delar inte färgskala (R3).

  • Fel

    Ny regel skriven i apps/portal/src/shell/shell.css från en modul

    Rätt

    Tailwind-utilities i modulens egen komponent

    shell.css ägs av skalet (R6).

  • Fel

    tone: string i en modulprop

    Rätt

    tone: ChipTone – unionen ur @startsidan/ui

    Fri sträng gör varianterna omöjliga att räkna upp och kontrollera (R10).

  • Fel

    Egen div med role="progressbar"

    Rätt

    <Progress value={60} /> ur @startsidan/ui

    Progress är Radix under ytan och sätter aria-valuenow/min/max korrekt.

Checklista

Gå igenom den mot diffen innan du committar.

  • Inga hex-värden och inga px i hakparentes – yarn lint fäller båda, men se dem i diffen först.
  • Grönt bara för status, AI-ytor med --color-ai-*, och all AI-genererad text bär AiChip.
  • Varje Card har headingId och title.
  • Varianter är unionstyper, inte strängar. Inga any.
  • Komponenten är en server component om den inte behöver interaktion.
  • Inga nya regler i shell.css från en modul.
  • All UI-text är på svenska.
  • yarn lint && yarn test är gröna.
Designfacit är prototyperna i docs/. Systemet här beskriver det som faktiskt är byggt – när de skiljer sig är det systemet som ska rättas.