From a8fdc9ebccb28d9d2d443d1c93e0d326770e7b96 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 14:01:24 +0000 Subject: [PATCH] =?UTF-8?q?ALVA:=20webbplats,=20portal=20och=20kunskapsk?= =?UTF-8?q?=C3=A4llor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Webbplatsen och plattformen är ett system. Samma komponentbibliotek, samma rutnät, samma ord — besökaren lämnar aldrig marknadswebben när den loggar in, eftersom den inte gör det. När apparna kommer återanvänder de samma bibliotek. Det som saknas på startsidan är lika avsiktligt som det som finns: ingen hjältebild, ingen gradient, ingen animation, inga kundlogotyper, ingen uppmaning att boka demo. En sida som försöker övertyga läser som marknadsföring; en sida som redovisar läser som dokumentation. Den senare är vad en teknisk chef fattar beslut utifrån. Kunskapskällorna är byggda som Operational Knowledge Infrastructure: leverantörsoberoende gränssnitt där organisationen ansluter det den har licens till. Det gör att produkten aldrig behöver marknadsföras med "vi stödjer X". En betainstallation fungerar på enbart egna dokument. Livscykeln har fem tillstånd med avsikt. Configured och Connected är inte samma sak — en nyckel kan vara ifylld och ändå fel — och Validated skiljer sig från Connected: anslutningen svarar, men har den data för de fordon organisationen faktiskt arbetar med? Det är den frågan som avgör om källan är till nytta. Portalens nyckeltal visas som NOT APPLICABLE i stället för som tomma grafer. Att visa noll vore att lova något som inte finns; att dölja raderna vore att dölja riktningen. Ett test läser den faktiska källkoden och låser disciplinen: inga gradienter, ingen animation, inga emojier utanför ✓ ○ □ →, ingen färg utanför paletten, inga marknadsföringsord. Ett designsystem som bara finns i ett dokument blir urvattnat på tredje sidan någon lägger till. Testet avslöjade att min egen kod använde py-3 — 12 px — och alltså bröt 8 px-rutnätet på sju ställen. Jag rättade koden, inte testet. Ett rutnät med undantag är inget rutnät. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt --- felsokning/app/src/App.tsx | 13 + felsokning/app/src/alva/komponenter.tsx | 16 +- .../src/felsokning/__tests__/alva-yta.test.ts | 100 ++++++ felsokning/app/src/pages/alva/Ansokan.tsx | 126 ++++++++ .../app/src/pages/alva/Kunskapskallor.tsx | 197 ++++++++++++ felsokning/app/src/pages/alva/LoggaIn.tsx | 80 +++++ felsokning/app/src/pages/alva/Portal.tsx | 115 +++++++ felsokning/app/src/pages/alva/Ram.tsx | 77 +++++ felsokning/app/src/pages/alva/Start.tsx | 293 ++++++++++++++++++ 9 files changed, 1009 insertions(+), 8 deletions(-) create mode 100644 felsokning/app/src/felsokning/__tests__/alva-yta.test.ts create mode 100644 felsokning/app/src/pages/alva/Ansokan.tsx create mode 100644 felsokning/app/src/pages/alva/Kunskapskallor.tsx create mode 100644 felsokning/app/src/pages/alva/LoggaIn.tsx create mode 100644 felsokning/app/src/pages/alva/Portal.tsx create mode 100644 felsokning/app/src/pages/alva/Ram.tsx create mode 100644 felsokning/app/src/pages/alva/Start.tsx diff --git a/felsokning/app/src/App.tsx b/felsokning/app/src/App.tsx index cc32107..1250573 100644 --- a/felsokning/app/src/App.tsx +++ b/felsokning/app/src/App.tsx @@ -21,6 +21,11 @@ import DelatArende from "./pages/felsokning/DelatArende"; import PublikDelning from "./pages/felsokning/PublikDelning"; import Oversikt from "./pages/felsokning/Oversikt"; import Installningar from "./pages/felsokning/Installningar"; +import AlvaStart from "./pages/alva/Start"; +import AlvaAnsokan from "./pages/alva/Ansokan"; +import AlvaLoggaIn from "./pages/alva/LoggaIn"; +import AlvaPortal from "./pages/alva/Portal"; +import AlvaKunskapskallor from "./pages/alva/Kunskapskallor"; import { PaymentTestModeBanner } from "@/components/PaymentTestModeBanner"; const queryClient = new QueryClient(); @@ -51,6 +56,14 @@ function AppContent() { } /> } /> } /> + {/* ALVA — publik webbplats och kundportal. Samma system, samma + komponentbibliotek: besökaren lämnar aldrig marknadswebben + när den loggar in. */} + } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/felsokning/app/src/alva/komponenter.tsx b/felsokning/app/src/alva/komponenter.tsx index 968b270..d07edf1 100644 --- a/felsokning/app/src/alva/komponenter.tsx +++ b/felsokning/app/src/alva/komponenter.tsx @@ -123,7 +123,7 @@ export function Falt({ etikett, children }: { etikett: string; children: ReactNo return (
{etikett} -
+
{children}
@@ -135,14 +135,14 @@ export function Block({ children, rubrik, beteckning }: { children: ReactNode; r
{(rubrik || beteckning) && (
{rubrik && {rubrik}} {beteckning && {beteckning}}
)} -
{children}
+
{children}
); } @@ -164,7 +164,7 @@ export function Fasrad({ aktiv, klara = [] }: { aktiv: Fas; klara?: Fas[] }) {
  • {f.bokstav}
  • -
    {f.namn}
    -
    +
    {f.namn}
    +
    {f.syfte}
    @@ -302,7 +302,7 @@ export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNo {k} @@ -314,7 +314,7 @@ export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNo {rader.map((rad, i) => ( {rad.map((cell, j) => ( - + {cell} ))} diff --git a/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts b/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts new file mode 100644 index 0000000..118689f --- /dev/null +++ b/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts @@ -0,0 +1,100 @@ +// @vitest-environment node +// Disciplinen är det karakteristiska, inte färgerna. +// +// Ett designsystem som bara finns i ett dokument blir urvattnat på tredje +// sidan någon lägger till. Testet läser den faktiska källkoden för +// webbplatsen och portalen och låser de regler som annars glider: +// inga gradienter, ingen animation, inga emojier, inga marknadsföringsord, +// och ingen färg utanför paletten. +// +// Det testar utseende genom att läsa kod, vilket är trubbigt. Men det +// fångar exakt den sortens tillägg som faktiskt sker — en `animate-pulse` +// här, en `bg-gradient-to-r` där — och som ingen enskilt tycker är fel. +import { readdirSync, readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { FORBJUDNA_ORD } from "@/alva/sprak"; + +const KATALOGER = ["src/alva", "src/pages/alva"]; + +const filer = KATALOGER.flatMap((katalog) => + readdirSync(katalog) + .filter((f) => f.endsWith(".ts") || f.endsWith(".tsx")) + .map((f) => [`${katalog}/${f}`, readFileSync(`${katalog}/${f}`, "utf8")] as const), +); + +/** Kommentarer får diskutera det som är förbjudet i gränssnittet. */ +const utanKommentarer = (kod: string) => + kod.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); + +describe("ALVA-ytan följer designsystemet", () => { + it("hittar filerna att granska", () => { + expect(filer.length).toBeGreaterThanOrEqual(7); + }); + + it.each(filer)("%s använder ingen gradient, glaseffekt eller skugga", (_namn, kod) => { + const k = utanKommentarer(kod); + expect(k).not.toMatch(/gradient/i); + expect(k).not.toMatch(/backdrop-blur|backdrop-filter/i); + expect(k).not.toMatch(/\bshadow-(sm|md|lg|xl|2xl)\b/); + }); + + it.each(filer)("%s innehåller ingen animation", (_namn, kod) => { + const k = utanKommentarer(kod); + // Rörelse som inte bär information är brus i ett utrymme där + // teknikern redan har för mycket. + expect(k).not.toMatch(/\banimate-|\btransition-|@keyframes/); + }); + + it.each(filer)("%s använder inga emojier", (_namn, kod) => { + const k = utanKommentarer(kod); + // Fyra tecken är tillåtna: ✓ ○ □ → + const emojier = k.match(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu) ?? []; + expect(emojier.filter((t) => !["✓", "○", "□", "→"].includes(t))).toEqual([]); + }); + + it.each(filer)("%s använder bara färger ur paletten", (_namn, kod) => { + const k = utanKommentarer(kod); + const palett = ["#1B1E22", "#4D5662", "#D7DCE2", "#F6F7F8", "#005CA9", "#FFFFFF"]; + const funna = (k.match(/#[0-9A-Fa-f]{3,8}\b/g) ?? []).map((f) => f.toUpperCase()); + expect(funna.filter((f) => !palett.includes(f))).toEqual([]); + }); + + it.each(filer.filter(([n]) => !n.endsWith("sprak.ts")))( + "%s innehåller inga marknadsföringsord", + (_namn, kod) => { + // sprak.ts undantas: den definierar listan och måste innehålla den. + const text = utanKommentarer(kod).toLowerCase(); + for (const ord of FORBJUDNA_ORD) expect(text, ord).not.toContain(ord); + }, + ); + + it("hela ytan undviker AI-språk — produkten beskrivs som en metod", () => { + for (const [namn, kod] of filer) { + const text = utanKommentarer(kod); + expect(text, namn).not.toMatch(/\bAI[- ]?(assistant|assistent|powered|driven)\b/i); + expect(text, namn).not.toMatch(/\bchatt?bot\b/i); + } + }); + + it("rubriker är versaler — sektionsetiketter, inte meningar", () => { + const komponenter = readFileSync("src/alva/komponenter.tsx", "utf8"); + expect(komponenter).toMatch(/uppercase/); + // Både Etikett och Rubrik måste sätta versaler. + const rubrik = komponenter.slice(komponenter.indexOf("export function Rubrik")); + expect(rubrik.slice(0, 600)).toMatch(/uppercase/); + }); + + it("avståndsmått följer 8 px-rutnätet", () => { + // Rutnätet gäller layout, inte typografi: teckenstorlek och + // radavstånd sätts av läsbarhet, inte av rytmen i sidan. Det som + // mäts här är Tailwinds avståndsklasser, som är 4 px-steg — ett + // jämnt steg är alltså en 8 px-multipel. + const udda: string[] = []; + for (const [namn, kod] of filer) { + for (const m of utanKommentarer(kod).matchAll(/\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)\b/g)) { + if (Number(m[2]) % 2 !== 0) udda.push(`${namn}: ${m[0]}`); + } + } + expect(udda).toEqual([]); + }); +}); diff --git a/felsokning/app/src/pages/alva/Ansokan.tsx b/felsokning/app/src/pages/alva/Ansokan.tsx new file mode 100644 index 0000000..135cdfa --- /dev/null +++ b/felsokning/app/src/pages/alva/Ansokan.tsx @@ -0,0 +1,126 @@ +// Kontoansökan. +// +// Ingen självbetjäning, ingen kortbetalning, ingen provperiod. Ansökan +// granskas manuellt och det står på sidan — inte som en ursäkt utan som +// en uppgift. Ett system som avgör vad som får kallas verifierat i en +// garantitvist kan inte dela ut konton automatiskt. + +import { type FormEvent, useState } from "react"; +import { Block, Etikett, FARG, Knapp, Rubrik, Statusmärke } from "@/alva/komponenter"; +import { Ram } from "./Ram"; + +interface Falt { + namn: string; + etikett: string; + typ?: string; + kravs?: boolean; + hjalp?: string; +} + +const FALT: Falt[] = [ + { namn: "foretag", etikett: "Company", kravs: true }, + { namn: "orgnummer", etikett: "Organization number", kravs: true }, + { namn: "kontakt", etikett: "Contact person", kravs: true }, + { namn: "epost", etikett: "Email", typ: "email", kravs: true }, + { namn: "telefon", etikett: "Phone", typ: "tel", kravs: true }, + { namn: "tekniker", etikett: "Number of technicians", typ: "number", kravs: true }, + { namn: "bransch", etikett: "Industry", kravs: true }, + { namn: "land", etikett: "Country", kravs: true }, + { namn: "anvandare", etikett: "Expected number of users", typ: "number" }, +]; + +export default function Ansokan() { + const [skickad, setSkickad] = useState(false); + const [referens, setReferens] = useState(""); + + const skicka = (e: FormEvent) => { + e.preventDefault(); + // Referensen bildas lokalt så sökanden har något att hänvisa till + // omedelbart. Den slutliga beteckningen sätts vid granskning. + const data = new FormData(e.currentTarget); + const bas = String(data.get("orgnummer") ?? "").replace(/\D/g, "").slice(-4) || "0000"; + setReferens(`ALVA-ORG-${bas}`); + setSkickad(true); + }; + + if (skickad) { + return ( + +
    + Application +
    + Submitted +
    + +
    +
    +
    Status
    +
    + +
    +
    +
    +
    Reference
    +
    {referens}
    +
    +
    +

    + Applications are reviewed manually. An invoice is issued on approval. The organization is activated + once payment is registered. +

    +
    +
    +
    + ); + } + + return ( + +
    + Registration +
    + Request account +
    +

    + Applications are reviewed manually. No payment is taken at this stage. +

    + +
    + + {FALT.map((f) => ( +
    + + +
    + ))} +
    + +