# Designsystem – Startsidan, Sundsvalls kommun Läs detta innan du skriver UI-kod i repot. Systemet beskriver det som är byggt: tokens i packages/tokens, primitiver i packages/ui, ytor i apps/portal/src/shell. All UI-text skrivs på svenska. Formen är avsiktlig: stabila id, uttömmande varianter, villkor istället för smak, och fel bredvid rätt. Finns ett värde inte i en lista här finns det inte i systemet – lägg inte till ett eget. In English: this document describes a Swedish municipal design system. UI text is intentionally in Swedish, and so are the token names – the Swedish is the source of truth, not a translation artifact. Källa: apps/portal/src/app/dev/ui/system/ (samma data som /dev/ui och /dev/ui/spec.json) innehallsversion: c2fnl4i9e4nh (hash av innehållet, samma värde som ETag) ## Bärande regler (prioritetsordning – krock löses uppifrån) R1. Grönt är status – klart, godkänt, positivt – och betyder aldrig AI. AI-ytor tonas med --color-ai-* i blått. Därför: 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). R2. Allt AI-genererat märks synligt med – texten '✦ AI-genererat' eller en mer precis notis. Därför: Märkningen är ett löfte till användaren, inte en dekoration. Den får inte kunna glömmas bort. R3. Blått betyder identitet: den här personen, den här saken. Avvikande färg betyder status. Därför: 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. R4. Fä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. Därför: 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. R5. Statusfä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. Därför: 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. R6. Moduler skriver Tailwind-utilities. CSS i shell.css ägs av skalet och rörs inte av moduler. Därför: Skalets krom är porterad 1:1 från prototypen. Ändras den från en modul går måtten isär. R7. Server components som default. "use client" bara på det som faktiskt behöver interaktion. Därför: Modulerna hämtar sin egen data på servern (ADR-006). Ett onödigt klientlager tvingar upp datat i props. R8. Varje kort får en tillgänglig rubrik: . Därför: headingId blir sektionens aria-labelledby. Utan den är kortet en namnlös region för skärmläsaren. R9. Svenska i all UI-text, i props-värden som visas, och i commit-ämnen. Därför: Prototypen visas för kommunens medarbetare. Blandat språk läcker igenom i demolägen. R10. TypeScript strict, inga any. Varianter uttrycks som unionstyper, inte som fri sträng. Därför: Unionen är det som gör varianterna uppräkningsbara – både för kompilatorn och för den här specifikationen. R11. Ett 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:). Därför: 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. R12. Allt 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. Därför: 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 Format: tailwind-namn | css-variabel | värde | betydelse ### Färg – Neutraler Regel: Bär all yta och all text som inte har en betydelse att signalera. - ink | --color-ink | #1f1f25 | Brödtext och rubriker. - ink-soft | --color-ink-soft | #4a4a55 | Metatext, andrahandsinformation. - surface | --color-surface | #fff | Kortets botten. - page | --color-page | #edf1f5 | Sidans botten, och rader inuti kort. - page-deep | --color-page-deep | #e4eaf0 | Ett steg mörkare botten, för helbreddsband som ska läsas som en annan del av sidan än den ovanför. - border | --color-border | #dde3e9 | Kortets ram, tunna avdelare. - border-strong | --color-border-strong | #c3ccd4 | Ram på interaktiva ytor, t.ex. ghost-knapp. - muted-bg | --color-muted-bg | #eef0f2 | Dämpad chipbotten, listavdelare. - muted-tx | --color-muted-tx | #5a6470 | Text på dämpad botten. - ink-faint | --color-ink-faint | #9aa3ad | Platshållartext: ett fält som väntar på innehåll. Läses som frånvaro, inte som dämpad information. ### Färg – Identitet – blå Regel: 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 | #005595 | Primärknapp, avatar, aktiv markering. - blue-h | --color-blue-h | #004070 | Hovertillstånd för primärknapp. - blue-bg | --color-blue-bg | #eaf1f8 | Ljus botten: hover på rader, info-chip. - blue-acc | --color-blue-acc | #cfe0ec | Accentram, t.ex. kort vid hover. - blue-tx | --color-blue-tx | #004c85 | Länktext och text på blue-bg. - blue-lift | --color-blue-lift | #1472b8 | Identitetsblå mot mörk platta, t.ex. avatar i fokuslägets topprad. Samma betydelse som blue, lyft för att synas. - sidebar | --color-sidebar | #003c69 | Endast sidopanelens botten. ### Färg – Status Regel: 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 | #12874e | Klart, godkänt. Grönt är bara status – aldrig AI (R1). - green-bg | --color-green-bg | #e8f4ec | Botten i chip med tone="ok". - green-acc | --color-green-acc | #c9e4d7 | Accentram för klart-tillstånd. - green-tx | --color-green-tx | #0c6a3c | Text på green-bg. - amber | --color-amber | #b06a1f | Väntar på någon annan. - amber-bg | --color-amber-bg | #fbeede | Botten i chip med tone="warn". - amber-tx | --color-amber-tx | #8a5117 | Text på amber-bg. - red | --color-red | #b3261e | Tidsgräns passerad, avvikelse. - red-bg | --color-red-bg | #fceceb | Botten i chip med tone="avv". ### Färg – AI – blå toner Regel: 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 | #1f64b8 | AI-knapp, AI-märke, AI-fältets fokusram. - ai-deep | --color-ai-deep | #164c8f | Hovertillstånd på AI-knapp. - ai-bg | --color-ai-bg | #eef4fc | Botten på AI-tonade kort och chips. - ai-acc | --color-ai-acc | #c9dcf3 | Ram på AI-tonade ytor. - ai-tx | --color-ai-tx | #164c8f | Text på ai-bg. ### Färg – Diagram Regel: 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 | #2a78d6 | Första serien. - graf-2 | --color-graf-2 | #eb6834 | Andra serien. - graf-3 | --color-graf-3 | #4a3aa7 | Tredje serien. - graf-4 | --color-graf-4 | #e87ba4 | Fjärde serien. Under 3:1 mot vitt – kräver synliga etiketter eller tabell. ### Typografi Rotens font-size är 62,5 %: 1rem = 10px, och Tailwinds skala behåller sin px-semantik (p-4 = 16px). Använd aldrig godtyckliga px-värden. - text-eyebrow | --text-eyebrow | 1.1rem (11px) | Versal etikett över ett nyckeltal. - text-xs | --text-xs | 1.2rem (12px) | Metatext, chips, sidfot. - text-sm | --text-sm | 1.4rem (14px) | Radrubriker, knappar. - text-base | --text-base | 1.6rem (16px) | Brödtext. - text-h2 | --text-h2 | 1.7rem (17px) | Kortrubrik. Sätts av Card, inte av modulen. - text-lg | --text-lg | 1.8rem (18px) | AI-fältets inmatning, ingress. - text-kpi | --text-kpi | 3rem (30px) | Nyckeltalets 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, mjuk | Allt som ligger på arbetsytan. - shadow-pop | --shadow-pop | djup | Menyer 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 i basreglerna. - animate-msg-in | --animate-msg-in | 0.25s ease | Nytt meddelande i ett samtalsflöde. - animate-blink | --animate-blink | 1.2s infinite | Assistenten skriver. - animate-flash | --animate-flash | 1s ease | Fält som AI just fyllt i formulärspegeln. ## Primitiver ### ui.card — Card Import: import { Card } from "@startsidan/ui"; Syfte: 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. Props: - title: ReactNode — Rubrik i kortets huvud. Utelämnas för kort utan huvud. - count: ReactNode — Antal bakom rubriken, t.ex. 4 eller "2/4". Renderas med punkt före. - action: ReactNode — Länk längst till höger i huvudet, normalt . - headingId: string — Id på rubriken. Blir sektionens aria-labelledby. Obligatorisk i praktiken. - tone: "default" | "ai" | tillåtna: default | ai | standard: default — ai ger AI-tonad botten och ram (blå toner). Endast för AI-ytor. - bodyClassName: string — För innehåll som bär egen luft, t.ex. en tabell som ska gå kant i kant. - className: string — Skriver över kortets egna utilities via tailwind-merge. Kod: ```tsx Visa alla →} > ``` Regler: - Rubriken sätts av Card. Skriv aldrig en egen

inuti kortet. - tone="ai" kräver att innehållet faktiskt är AI-genererat (R1, R2). - Kortet är en
; 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: RÄTT: VARFÖR: rounded-lg är V3:s kortform och sätts redan; rounded-xl är shadcns default som medvetet skrivs över. ### ui.chip — Chip Import: import { Chip, type ChipTone } from "@startsidan/ui"; Syfte: 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. Props: - tone: ChipTone | tillåtna: ok | warn | info | muted | avv | standard: muted — ok = klart, warn = väntar, info = neutral upplysning, muted = utan laddning, avv = avvikelse eller passerad tidsgräns. - children: ReactNode — Etiketten. Ett till två ord på svenska. Kod: ```tsx Klart Väntar på chef Försenad ``` 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). ### ui.aichip — AiChip Import: import { AiChip } from "@startsidan/ui"; Syfte: 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. Props: - children: ReactNode | standard: ✦ AI-genererat — Ersätt standardtexten när en mer precis notis säger mer, t.ex. "✦ Sammanfattad av AI". Kod: ```tsx ✦ Föreslagen prioritet ``` Regler: - Får aldrig utelämnas på AI-ytor (R2). - Stjärnan ✦ är märkets tecken och byts inte ut. ### ui.btn — Btn / BtnLink Import: import { Btn, BtnLink } from "@startsidan/ui"; Syfte: 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. Props: - variant: BtnVariant | tillåtna: primary | ghost | ai | standard: primary — primary = blå, huvudhandlingen. ghost = vit med bård, sidohandling. ai = AI-blå, endast för handlingar som utför något AI-genererat. - size: BtnSize | tillåtna: md | sm | standard: md — sm används i rader och korthuvuden. Kod: ```tsx Godkänn Visa detaljer ✦ Sammanfatta åt mig Felanmäl ``` 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). ### ui.statusrow — StatusRow Import: import { StatusRow, type RowStatus } from "@startsidan/ui"; Syfte: 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. Props: - status: RowStatus | tillåtna: krit | warn | info | ok | neutral | standard: neutral — krit = röd kant (tidsgräns), warn = gul, info = blå, ok = grön, neutral = grå. - title: ReactNode — Vad raden gäller. - meta: ReactNode — När, av vem, varifrån. - end: ReactNode — Chips och knappar, i ordningen AiChip → Chip → knapp. Kod: ```tsx
    Attestera} />
``` Regler: - Renderas som
  • och måste ligga i en
      . - 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). ### ui.listrow — ListRow / Progress Import: import { ListRow, Progress, Avatar } from "@startsidan/ui"; Syfte: 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. Props: - lead: ReactNode — Något framför titeln, normalt . - title: ReactNode — Radens rubrik. - meta: ReactNode — Understödjande text. - end: ReactNode — Chip eller värde längst till höger. - children: ReactNode — Extra innehåll under metaraden, normalt . - value (Progress): number — 0–100. Klipps automatiskt till intervallet. - done (Progress): boolean | standard: false — Grön stapel när klar, annars blå. Kod: ```tsx
        } title="Utvecklingsplan 2026" meta="Uppdaterad 12 mars" end={Väntar} >
      ``` 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". ### ui.avatar — Avatar Import: import { Avatar } from "@startsidan/ui"; Syfte: 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). Props: - initials: string — Ett till två versaler. - size: "sm" | "md" | tillåtna: sm | md | standard: md — sm i täta listor. Kod: ```tsx ``` Regler: - Alltid blå. Ingen variantprop för färg finns, och ska inte tillkomma. ### ui.morelink — MoreLink Import: import { MoreLink } from "@startsidan/ui"; Syfte: Länken längst till höger i ett korthuvud: "Visa alla →". Använd när: När kortet visar ett urval och det finns en fullständig vy. Props: - href: string — Målet. - children: ReactNode — Text med avslutande →. Kod: ```tsx Kalender →}>… ``` Regler: - Pilen → hör till texten, inte till en ikon. ### ui.table — Table Import: import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@startsidan/ui"; Syfte: 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. Kod: ```tsx ÄrendeStatus Behörighet HeromaVäntar
      ``` 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 ### monster.nyckeltal — Nyckeltalsruta Syfte: 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: ```tsx Budgetutfall +2,4% Mot budget hittills i år ``` 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. ### monster.aiyta — AI-yta Syfte: 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. Kod: ```tsx }>

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

      ✦ Föreslå ordning Visa källor
      ``` 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. ### monster.agera — Att agera på Syfte: 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: ```tsx // packages/modules//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. ### monster.modulkort — Modulkort Syfte: 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: ```tsx 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 ( Visa alla →} >
        {poster.map((p) => ( {p.chip.etikett}} /> ))}
      ); } ``` 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. ### monster.fokus — Fokusyta Syfte: 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: ```tsx // packages/modules//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. ### monster.notis — Notis Syfte: 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: ```tsx // packages/modules//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). ### monster.sida — Sida: ingång och artikel Syfte: 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: ```tsx // packages/modules//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
      , 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. ### monster.varumarke — Kommunens avsändare i toppraden Import: import { Varumarke } from "@startsidan/gemensamt/Varumarke"; // apps/projekt/src/varumarke Syfte: 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. Props: - produkt: string — Produktens namn efter linjen, t.ex. "Min startsida". - href: string | standard: "/" — Produktens första sida. - etikett: string | standard: "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). Kod: ```tsx
      {/* ytans knappar till höger; menyn i en egen rad under, se monster.huvudmeny */}
      ``` 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. ### monster.hero — Sidans hero Import: import { Hero } from "../projektyta/Hero"; // apps/projekt/src/projektyta Syfte: 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 , 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. Props: - className: string — Gäller rutnätet inuti: avstånd, spalter eller en annan bredd (devportalen 132rem). Kod: ```tsx Status

      Vad har hänt i projektet?

      …

      …
      ``` 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). ### monster.huvudmeny — Projektets huvudmeny Import: import { Topprad } from "../projektyta/Topprad"; // apps/projekt/src/projektyta Syfte: 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 . 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. Kod: ```tsx // 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
      …
      ``` 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
      , 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 (monster.hero). Raden bryts hellre än rullar. ## Ytor ### yta.arbetsyta — Arbetsytan Syfte: 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: ```tsx /* 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). ### yta.aifalt — AI-fältet Syfte: 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: ```tsx // packages/modules//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. ### yta.fokus — Fokusflöde Syfte: 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: ```tsx // packages/modules//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. ### yta.full — Modulvy över hela ytan Syfte: 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: ```tsx // packages/modules//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 - FEL: className="bg-ai-bg" på ett kort som visar "Klart" RÄTT: Klart på ett vanligt kort VARFÖR: 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" VARFÖR: 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 VARFÖR: 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: