ALVA: webbplats, portal och kunskapskällor

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-05 14:01:24 +00:00
parent 610bb9eca6
commit a8fdc9ebcc
9 changed files with 1009 additions and 8 deletions
+13
View File
@@ -21,6 +21,11 @@ import DelatArende from "./pages/felsokning/DelatArende";
import PublikDelning from "./pages/felsokning/PublikDelning"; import PublikDelning from "./pages/felsokning/PublikDelning";
import Oversikt from "./pages/felsokning/Oversikt"; import Oversikt from "./pages/felsokning/Oversikt";
import Installningar from "./pages/felsokning/Installningar"; 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"; import { PaymentTestModeBanner } from "@/components/PaymentTestModeBanner";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -51,6 +56,14 @@ function AppContent() {
<Route path="/admin" element={<Admin />} /> <Route path="/admin" element={<Admin />} />
<Route path="/order-confirmation" element={<OrderConfirmation />} /> <Route path="/order-confirmation" element={<OrderConfirmation />} />
<Route path="/account/change-subscription" element={<ChangeSubscription />} /> <Route path="/account/change-subscription" element={<ChangeSubscription />} />
{/* ALVA — publik webbplats och kundportal. Samma system, samma
komponentbibliotek: besökaren lämnar aldrig marknadswebben
när den loggar in. */}
<Route path="/alva" element={<AlvaStart />} />
<Route path="/alva/ansokan" element={<AlvaAnsokan />} />
<Route path="/alva/logga-in" element={<AlvaLoggaIn />} />
<Route path="/alva/portal" element={<AlvaPortal />} />
<Route path="/alva/portal/kunskapskallor" element={<AlvaKunskapskallor />} />
<Route path="/felsokning" element={<Arendelista />} /> <Route path="/felsokning" element={<Arendelista />} />
<Route path="/felsokning/nytt" element={<NyttArende />} /> <Route path="/felsokning/nytt" element={<NyttArende />} />
<Route path="/felsokning/arende/:id" element={<ArendeSida />} /> <Route path="/felsokning/arende/:id" element={<ArendeSida />} />
+8 -8
View File
@@ -123,7 +123,7 @@ export function Falt({ etikett, children }: { etikett: string; children: ReactNo
return ( return (
<div className="border-t py-2" style={{ borderColor: FARG.lightSteel }}> <div className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
<Etikett>{etikett}</Etikett> <Etikett>{etikett}</Etikett>
<div className="mt-1 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}> <div className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}>
{children} {children}
</div> </div>
</div> </div>
@@ -135,14 +135,14 @@ export function Block({ children, rubrik, beteckning }: { children: ReactNode; r
<section className="mb-6 border bg-white" style={{ borderColor: FARG.lightSteel }}> <section className="mb-6 border bg-white" style={{ borderColor: FARG.lightSteel }}>
{(rubrik || beteckning) && ( {(rubrik || beteckning) && (
<header <header
className="flex items-baseline justify-between border-b px-4 py-3" className="flex items-baseline justify-between border-b px-4 py-2"
style={{ borderColor: FARG.lightSteel, background: FARG.background }} style={{ borderColor: FARG.lightSteel, background: FARG.background }}
> >
{rubrik && <Rubrik niva={3}>{rubrik}</Rubrik>} {rubrik && <Rubrik niva={3}>{rubrik}</Rubrik>}
{beteckning && <span className="font-mono text-[11px]" style={{ color: FARG.steel }}>{beteckning}</span>} {beteckning && <span className="font-mono text-[11px]" style={{ color: FARG.steel }}>{beteckning}</span>}
</header> </header>
)} )}
<div className="px-4 py-3">{children}</div> <div className="px-4 py-2">{children}</div>
</section> </section>
); );
} }
@@ -164,7 +164,7 @@ export function Fasrad({ aktiv, klara = [] }: { aktiv: Fas; klara?: Fas[] }) {
<li <li
key={f.id} key={f.id}
aria-current={ärAktiv ? "step" : undefined} aria-current={ärAktiv ? "step" : undefined}
className="flex-1 border-r px-4 py-3 last:border-r-0" className="flex-1 border-r px-4 py-2 last:border-r-0"
style={{ style={{
borderColor: FARG.lightSteel, borderColor: FARG.lightSteel,
background: ärAktiv ? FARG.blue : FARG.white, background: ärAktiv ? FARG.blue : FARG.white,
@@ -175,8 +175,8 @@ export function Fasrad({ aktiv, klara = [] }: { aktiv: Fas; klara?: Fas[] }) {
<span className="text-[18px] font-semibold leading-none">{f.bokstav}</span> <span className="text-[18px] font-semibold leading-none">{f.bokstav}</span>
<Symbol ikon={ärKlar ? "klar" : ärAktiv ? "pagaende" : "vantar"} ton={ärAktiv ? FARG.white : undefined} /> <Symbol ikon={ärKlar ? "klar" : ärAktiv ? "pagaende" : "vantar"} ton={ärAktiv ? FARG.white : undefined} />
</div> </div>
<div className="mt-1 text-[11px] font-semibold uppercase tracking-[0.1em]">{f.namn}</div> <div className="mt-2 text-[11px] font-semibold uppercase tracking-[0.1em]">{f.namn}</div>
<div className="mt-1 text-[11px] leading-[16px]" style={{ opacity: ärAktiv ? 0.9 : 0.75 }}> <div className="mt-2 text-[11px] leading-[16px]" style={{ opacity: ärAktiv ? 0.9 : 0.75 }}>
{f.syfte} {f.syfte}
</div> </div>
</li> </li>
@@ -302,7 +302,7 @@ export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNo
<th <th
key={k} key={k}
scope="col" scope="col"
className="border-b px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.1em]" className="border-b px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.1em]"
style={{ borderColor: FARG.lightSteel, color: FARG.steel }} style={{ borderColor: FARG.lightSteel, color: FARG.steel }}
> >
{k} {k}
@@ -314,7 +314,7 @@ export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNo
{rader.map((rad, i) => ( {rader.map((rad, i) => (
<tr key={i}> <tr key={i}>
{rad.map((cell, j) => ( {rad.map((cell, j) => (
<td key={j} className="border-b px-3 py-2 align-top" style={{ borderColor: FARG.lightSteel, color: FARG.graphite }}> <td key={j} className="border-b px-4 py-2 align-top" style={{ borderColor: FARG.lightSteel, color: FARG.graphite }}>
{cell} {cell}
</td> </td>
))} ))}
@@ -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([]);
});
});
+126
View File
@@ -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<HTMLFormElement>) => {
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 (
<Ram>
<div className="mx-auto max-w-[680px] px-6 py-16">
<Etikett>Application</Etikett>
<div className="mt-2 mb-8">
<Rubrik niva={1}>Submitted</Rubrik>
</div>
<Block rubrik="Registration" beteckning={referens}>
<dl className="text-[14px] leading-[24px]">
<div className="flex justify-between border-b py-2" style={{ borderColor: FARG.lightSteel }}>
<dt style={{ color: FARG.steel }}>Status</dt>
<dd>
<Statusmärke status="pending" />
</dd>
</div>
<div className="flex justify-between py-2">
<dt style={{ color: FARG.steel }}>Reference</dt>
<dd className="font-mono">{referens}</dd>
</div>
</dl>
<p className="mt-4 border-t pt-4 text-[13px] leading-[20px]" style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
Applications are reviewed manually. An invoice is issued on approval. The organization is activated
once payment is registered.
</p>
</Block>
</div>
</Ram>
);
}
return (
<Ram>
<div className="mx-auto max-w-[680px] px-6 py-16">
<Etikett>Registration</Etikett>
<div className="mt-2 mb-2">
<Rubrik niva={1}>Request account</Rubrik>
</div>
<p className="mb-8 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
Applications are reviewed manually. No payment is taken at this stage.
</p>
<form onSubmit={skicka} noValidate={false}>
<Block>
{FALT.map((f) => (
<div key={f.namn} className="border-t py-2 first:border-t-0" style={{ borderColor: FARG.lightSteel }}>
<label htmlFor={f.namn} className="block text-[11px] font-semibold uppercase tracking-[0.12em]" style={{ color: FARG.steel }}>
{f.etikett}
{f.kravs && <span aria-hidden="true"> ·</span>}
{f.kravs && <span className="sr-only"> (required)</span>}
</label>
<input
id={f.namn}
name={f.namn}
type={f.typ ?? "text"}
required={f.kravs}
className="mt-2 w-full border px-4 py-2 text-[14px]"
style={{ borderColor: FARG.lightSteel, background: FARG.white, color: FARG.graphite }}
/>
</div>
))}
<div className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
<label htmlFor="noteringar" className="block text-[11px] font-semibold uppercase tracking-[0.12em]" style={{ color: FARG.steel }}>
Notes
</label>
<textarea
id="noteringar"
name="noteringar"
rows={4}
className="mt-2 w-full border px-4 py-2 text-[14px]"
style={{ borderColor: FARG.lightSteel, background: FARG.white, color: FARG.graphite }}
/>
</div>
</Block>
<Knapp type="submit">Submit application</Knapp>
</form>
</div>
</Ram>
);
}
@@ -0,0 +1,197 @@
// Kunskapskällor — Operational Knowledge Infrastructure.
//
// Arkitekturprincipen: ALVA antar aldrig en viss leverantör. Varje källa
// implementerar samma gränssnitt, och organisationen ansluter det den har
// licens till. Det gör att produkten aldrig behöver marknadsföras med
// "vi stödjer X" — den använder organisationens auktoriserade källor.
//
// Praktiskt betyder det att en betainstallation fungerar med enbart egna
// PDF:er, och att externa leverantörer aktiveras senare genom separata
// anslutningar utan att plattformens kärna ändras.
//
// Livscykeln är 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.
import { Block, Etikett, FARG, Knapp, Rubrik, Statusmärke, Symbol, Tabell } from "@/alva/komponenter";
import type { Status } from "@/alva/sprak";
import { Ram } from "./Ram";
export type Livscykel = "installed" | "configured" | "connected" | "validated" | "operational";
export const LIVSCYKEL: { id: Livscykel; namn: string; innebord: string }[] = [
{ id: "installed", namn: "Installed", innebord: "Connector present. No credentials." },
{ id: "configured", namn: "Configured", innebord: "Credentials stored. Not yet contacted." },
{ id: "connected", namn: "Connected", innebord: "Provider responds to authentication." },
{ id: "validated", namn: "Validated", innebord: "Coverage confirmed for the organization's fleet." },
{ id: "operational", namn: "Operational", innebord: "In use during diagnostics." },
];
export interface Kunskapskalla {
beteckning: string;
namn: string;
livscykel: Livscykel;
autentisering: "API key" | "OAuth" | "File import" | "Network share" | "Internal";
formagor: string[];
senastSynk: string;
prioritet: number;
}
// Beta: interna dokument räcker för att systemet ska fungera. De externa
// raderna visar gränssnittet, inte en färdig integration — och statusen
// säger det rakt ut i stället för att antyda något annat.
const KALLOR: Kunskapskalla[] = [
{
beteckning: "ALVA-SRC-001",
namn: "Internal company procedures",
livscykel: "operational",
autentisering: "Internal",
formagor: ["Procedures", "Inspection protocols", "Warranty policy"],
senastSynk: "Continuous",
prioritet: 1,
},
{
beteckning: "ALVA-SRC-002",
namn: "Document library",
livscykel: "operational",
autentisering: "File import",
formagor: ["Service manuals", "Technical bulletins", "PDF documentation"],
senastSynk: "On upload",
prioritet: 2,
},
{
beteckning: "ALVA-SRC-010",
namn: "OEM documentation",
livscykel: "installed",
autentisering: "API key",
formagor: ["Repair procedures", "Technical bulletins", "Service information"],
senastSynk: "—",
prioritet: 3,
},
{
beteckning: "ALVA-SRC-011",
namn: "Wiring diagram provider",
livscykel: "installed",
autentisering: "API key",
formagor: ["Wiring diagrams", "Component locations"],
senastSynk: "—",
prioritet: 4,
},
{
beteckning: "ALVA-SRC-012",
namn: "Vehicle data provider",
livscykel: "installed",
autentisering: "API key",
formagor: ["Vehicle identification", "Equipment level", "Service campaigns"],
senastSynk: "—",
prioritet: 5,
},
{
beteckning: "ALVA-SRC-013",
namn: "Repair time catalogue",
livscykel: "installed",
autentisering: "API key",
formagor: ["Operation times", "Parts catalogue"],
senastSynk: "—",
prioritet: 6,
},
];
const STATUSAV: Record<Livscykel, Status> = {
installed: "pending",
configured: "in_progress",
connected: "in_progress",
validated: "passed",
operational: "complete",
};
export default function Kunskapskallor() {
return (
<Ram portal>
<div className="mx-auto max-w-[1040px] px-6 py-12">
<Etikett>Operational Knowledge Infrastructure</Etikett>
<div className="mt-2 mb-2">
<Rubrik niva={1}>Knowledge sources</Rubrik>
</div>
<p className="mb-8 max-w-[680px] text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
ALVA is provider-agnostic. Each source implements the same interface; the organization connects those
it holds licenses for. No provider is assumed by the platform.
</p>
<Block rubrik="Configured sources" beteckning="ALVA-DOC-014">
<Tabell
kolumner={["Reference", "Source", "Status", "Authentication", "Capabilities", "Last sync", "Priority"]}
rader={KALLOR.map((k) => [
<span key="r" className="font-mono text-[11px]">
{k.beteckning}
</span>,
k.namn,
<Statusmärke key="s" status={STATUSAV[k.livscykel]} />,
k.autentisering,
<span key="f" className="text-[12px]" style={{ color: FARG.steel }}>
{k.formagor.join(" · ")}
</span>,
k.senastSynk,
String(k.prioritet),
])}
/>
<div className="mt-6 flex gap-4">
<Knapp>Add provider</Knapp>
<Knapp variant="sekundar">Test connection</Knapp>
</div>
<p className="mt-4 text-[12px] leading-[18px]" style={{ color: FARG.steel }}>
Configuration requires the organization administrator role.
</p>
</Block>
<Block rubrik="Connector lifecycle" beteckning="ALVA-SPEC-011">
<ol>
{LIVSCYKEL.map((l, i) => (
<li key={l.id} className="flex gap-4 border-t py-2 first:border-t-0" style={{ borderColor: FARG.lightSteel }}>
<span className="font-mono text-[12px] leading-[20px]" style={{ color: FARG.steel }}>
{String(i + 1).padStart(2, "0")}
</span>
<div>
<div className="text-[13px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.graphite }}>
{l.namn}
</div>
<div className="mt-2 text-[13px]" style={{ color: FARG.steel }}>
{l.innebord}
</div>
</div>
</li>
))}
</ol>
</Block>
<Block rubrik="Resolution order during diagnostics" beteckning="ALVA-RULE-120">
<p className="mb-4 text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
Sources are consulted in order. The technician does not search across systems; relevant information
is presented within the procedure with full source attribution.
</p>
<ol className="text-[14px] leading-[26px]" style={{ color: FARG.graphite }}>
{[
"Internal company procedures",
"OEM documentation",
"Technical bulletins",
"Warranty information",
"Service manuals",
"Historical verified diagnostics",
"Organization best practices",
].map((rad, i) => (
<li key={rad} className="flex gap-4">
<span className="font-mono text-[12px]" style={{ color: FARG.steel }}>
{i + 1}
</span>
<Symbol ikon="nasta" />
{rad}
</li>
))}
</ol>
</Block>
</div>
</Ram>
);
}
+80
View File
@@ -0,0 +1,80 @@
// Inloggning.
//
// Användarnamn, lösenord, organisation. Ingen social inloggning, ingen
// "kom ihåg mig", ingen illustration. Ett industrisystem frågar vem du
// är och i vilken organisation — inte om du vill fortsätta med Google.
import { type FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Block, Etikett, FARG, Knapp, Rubrik } from "@/alva/komponenter";
import { Ram } from "./Ram";
const FALT = [
{ namn: "organisation", etikett: "Organization", typ: "text" },
{ namn: "anvandarnamn", etikett: "Username", typ: "text" },
{ namn: "losenord", etikett: "Password", typ: "password" },
];
export default function LoggaIn() {
const navigera = useNavigate();
const [fel, setFel] = useState("");
const skicka = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
if (!data.get("organisation") || !data.get("anvandarnamn")) {
// Statusspråket gäller även fel: ett konstaterande, inte en ursäkt.
setFel("Authentication incomplete. Organization and username required.");
return;
}
navigera("/alva/portal");
};
return (
<Ram>
<div className="mx-auto max-w-[440px] px-6 py-24">
<Etikett>Access</Etikett>
<div className="mt-2 mb-8">
<Rubrik niva={1}>Login</Rubrik>
</div>
<form onSubmit={skicka}>
<Block>
{FALT.map((f) => (
<div key={f.namn} className="border-t py-2 first:border-t-0" style={{ borderColor: FARG.lightSteel }}>
<label
htmlFor={f.namn}
className="block text-[11px] font-semibold uppercase tracking-[0.12em]"
style={{ color: FARG.steel }}
>
{f.etikett}
</label>
<input
id={f.namn}
name={f.namn}
type={f.typ}
autoComplete={f.typ === "password" ? "current-password" : "username"}
className="mt-2 w-full border px-4 py-2 text-[14px]"
style={{ borderColor: FARG.lightSteel, background: FARG.white, color: FARG.graphite }}
/>
</div>
))}
</Block>
{fel && (
<p role="alert" className="mb-4 border px-4 py-2 text-[13px]" style={{ borderColor: FARG.graphite, color: FARG.graphite }}>
{fel}
</p>
)}
<div className="flex items-center justify-between">
<Knapp type="submit">Sign in</Knapp>
<a href="/alva/ansokan" className="text-[12px] uppercase tracking-[0.1em]" style={{ color: FARG.steel }}>
Forgot password
</a>
</div>
</form>
</div>
</Ram>
);
}
+115
View File
@@ -0,0 +1,115 @@
// Kundportalen.
//
// Ingen välkomsthälsning, ingen aktivitetsström, ingen graf som rör sig.
// Portalen redovisar organisationens tillstånd i namngivna fält, på
// samma sätt som en instrumentpanel gör. Den som öppnar den vill veta
// tre saker: är licensen giltig, fungerar systemet, och vad har hänt.
import { Link } from "react-router-dom";
import { PLATTFORMSVERSION, arendebeteckning } from "@/alva/system";
import { Block, Etikett, FARG, Falt, Knapp, Rubrik, Statusmärke, Tabell } from "@/alva/komponenter";
import { MEDDELANDE } from "@/alva/sprak";
import { Ram } from "./Ram";
export default function Portal() {
return (
<Ram portal>
<div className="mx-auto max-w-[1040px] px-6 py-12">
<div className="mb-8 flex flex-wrap items-baseline justify-between gap-4">
<div>
<Etikett>Organization</Etikett>
<div className="mt-2">
<Rubrik niva={1}>Dashboard</Rubrik>
</div>
</div>
<span className="font-mono text-[11px]" style={{ color: FARG.steel }}>
ALVA-ORG-0142
</span>
</div>
<div className="grid gap-6 md:grid-cols-2">
<Block rubrik="Status" beteckning="ALVA-REP-0001">
<Falt etikett="Organization">
<Statusmärke status="complete" />
</Falt>
<Falt etikett="License">Platform + 12 user licenses</Falt>
<Falt etikett="Active users">12 of 12</Falt>
<Falt etikett="Platform version">{PLATTFORMSVERSION}</Falt>
<Falt etikett="Quarterly report">
<Statusmärke status="pending" />
</Falt>
</Block>
<Block rubrik="System notices" beteckning="ALVA-TB-0007">
<Falt etikett="Knowledge sources">{MEDDELANDE.kalla_ansluten}</Falt>
<Falt etikett="Rule package">Signature verified.</Falt>
<Falt etikett="Restore verification">Last performed on build. Passed.</Falt>
<Falt etikett="Retention">Cases scheduled per case type.</Falt>
</Block>
</div>
<Block rubrik="Recent diagnostics" beteckning="ALVA-DOC-002">
<Tabell
kolumner={["Case", "Object", "Phase", "Status", "Closed"]}
rader={[
[
<span key="a" className="font-mono text-[11px]">
{arendebeteckning(91821)}
</span>,
"Volvo XC60 D4",
"Action",
<Statusmärke key="s" status="complete" />,
"2026-08-04",
],
[
<span key="b" className="font-mono text-[11px]">
{arendebeteckning(91820)}
</span>,
"Volkswagen Passat 2.0 TDI",
"Verification",
<Statusmärke key="s" status="in_progress" />,
"—",
],
[
<span key="c" className="font-mono text-[11px]">
{arendebeteckning(91819)}
</span>,
"Scania R450",
"Localization",
<Statusmärke key="s" status="blocked" />,
"—",
],
]}
/>
<div className="mt-6">
<Link to="/felsokning">
<Knapp>Open diagnostics</Knapp>
</Link>
</div>
</Block>
{/* Platshållare. Att visa tomma nyckeltal vore att lova något
som inte finns; att inte visa dem vore att dölja riktningen.
Fälten redovisas med status NOT APPLICABLE tills det finns
underlag för dem. */}
<Block rubrik="Performance analytics" beteckning="ALVA-REP-0100">
<p className="mb-4 text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
Available from Phase 3. Metrics are derived from verified outcomes only; a case closed without
confirmed root cause contributes nothing.
</p>
<Tabell
kolumner={["Metric", "Basis", "Status"]}
rader={[
["Diagnostic accuracy", "Confirmed vs. hypothesized causes", <Statusmärke key="1" status="not_applicable" />],
["Completion time", "Case duration by phase", <Statusmärke key="2" status="not_applicable" />],
["Verification ratio", "Cases with confirmed root cause", <Statusmärke key="3" status="not_applicable" />],
["Common fault categories", "Root cause distribution", <Statusmärke key="4" status="not_applicable" />],
["Knowledge development", "Documents and coverage", <Statusmärke key="5" status="not_applicable" />],
["Procedure adoption", "Exemption rate per step", <Statusmärke key="6" status="not_applicable" />],
]}
/>
</Block>
</div>
</Ram>
);
}
+77
View File
@@ -0,0 +1,77 @@
// Sidramen. Samma på webbplatsen och i portalen — det är därför
// inloggningen inte känns som ett byte av system.
//
// Navigationen är en rad text, inte en meny med ikoner. Den som söker
// något i ett industrisystem vet vad det heter.
import type { ReactNode } from "react";
import { Link, useLocation } from "react-router-dom";
import { ALVA, PLATTFORMSVERSION } from "@/alva/system";
import { FARG } from "@/alva/komponenter";
const PUBLIKT = [
{ till: "/alva", text: "Overview" },
{ till: "/alva/ansokan", text: "Request account" },
{ till: "/alva/logga-in", text: "Login" },
];
const PORTAL = [
{ till: "/alva/portal", text: "Dashboard" },
{ till: "/alva/portal/kunskapskallor", text: "Knowledge sources" },
{ till: "/alva/portal/dokument", text: "Document library" },
{ till: "/felsokning", text: "Diagnostics" },
];
export function Ram({ children, portal = false }: { children: ReactNode; portal?: boolean }) {
const plats = useLocation();
const lankar = portal ? PORTAL : PUBLIKT;
return (
<div className="min-h-screen" style={{ background: FARG.background, color: FARG.graphite }}>
<header className="border-b" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
<div className="mx-auto flex max-w-[1040px] flex-wrap items-center justify-between gap-4 px-6 py-4">
<Link to={portal ? "/alva/portal" : "/alva"} className="flex items-baseline gap-4">
<span className="text-[20px] font-semibold tracking-[0.04em]" style={{ color: FARG.graphite }}>
{ALVA.namn}
</span>
<span className="hidden text-[10px] uppercase tracking-[0.16em] sm:inline" style={{ color: FARG.steel }}>
{ALVA.utlast}
</span>
</Link>
<nav aria-label={portal ? "Portal" : "Site"}>
<ul className="flex gap-6">
{lankar.map((l) => {
const aktiv = plats.pathname === l.till;
return (
<li key={l.till}>
<Link
to={l.till}
aria-current={aktiv ? "page" : undefined}
className="text-[12px] font-semibold uppercase tracking-[0.1em]"
style={{ color: aktiv ? FARG.blue : FARG.steel }}
>
{l.text}
</Link>
</li>
);
})}
</ul>
</nav>
</div>
</header>
<main>{children}</main>
<footer className="border-t" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
<div
className="mx-auto flex max-w-[1040px] flex-wrap justify-between gap-4 px-6 py-6 font-mono text-[11px]"
style={{ color: FARG.steel }}
>
<span>{PLATTFORMSVERSION}</span>
<span>{ALVA.position}</span>
</div>
</footer>
</div>
);
}
+293
View File
@@ -0,0 +1,293 @@
// ALVA — publik webbplats.
//
// Webbplatsen och plattformen är samma system. Besökaren ska aldrig
// uppleva att den lämnar marknadswebben när den loggar in, eftersom den
// inte gör det: samma komponentbibliotek, samma rutnät, samma ord.
//
// Det som saknas här är lika viktigt som det som finns. Ingen hjälte-
// bild, 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.
import { Link } from "react-router-dom";
import { ALVA, FASER, PLATTFORMSVERSION } from "@/alva/system";
import { Block, Etikett, FARG, Knapp, Rubrik, Symbol, Tabell } from "@/alva/komponenter";
import { Ram } from "./Ram";
function Sektion({ etikett, rubrik, children }: { etikett: string; rubrik?: string; children: React.ReactNode }) {
return (
<section className="border-t py-16" style={{ borderColor: FARG.lightSteel }}>
<div className="mx-auto max-w-[1040px] px-6">
<Etikett>{etikett}</Etikett>
{rubrik && (
<div className="mt-2 mb-8">
<Rubrik niva={2}>{rubrik}</Rubrik>
</div>
)}
{children}
</div>
</section>
);
}
export default function Start() {
return (
<Ram>
{/* ---- Hero. En mening. Ingen slogan. ---- */}
<section className="border-b" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
<div className="mx-auto max-w-[1040px] px-6 py-24">
<h1 className="text-[64px] font-semibold leading-none tracking-[0.02em]" style={{ color: FARG.graphite }}>
{ALVA.namn}
</h1>
<p className="mt-4 text-[15px] uppercase tracking-[0.18em]" style={{ color: FARG.steel }}>
{ALVA.utlast}
</p>
<p className="mt-2 text-[13px] uppercase tracking-[0.14em]" style={{ color: FARG.steel }}>
{ALVA.position}
</p>
<p className="mt-10 max-w-[620px] text-[18px] leading-[28px]" style={{ color: FARG.graphite }}>
{ALVA.definition}
</p>
<div className="mt-10 flex gap-4">
<Link to="/alva/ansokan">
<Knapp>Request account</Knapp>
</Link>
<Link to="/alva/logga-in">
<Knapp variant="sekundar">Customer login</Knapp>
</Link>
</div>
</div>
</section>
{/* ---- Metoden ---- */}
<Sektion etikett="Methodology" rubrik="The ALVA model">
<p className="mb-8 max-w-[680px] text-[15px] leading-[24px]" style={{ color: FARG.steel }}>
ALVA is a standardized method for systematic analysis, localization, verification and action. Every
decision is traceable, every conclusion verifiable, every action reproducible.
</p>
<div className="grid gap-px md:grid-cols-4" style={{ background: FARG.lightSteel }}>
{FASER.map((f, i) => (
<div key={f.id} className="bg-white p-6">
<div className="flex items-baseline justify-between">
<span className="text-[40px] font-semibold leading-none" style={{ color: FARG.blue }}>
{f.bokstav}
</span>
<span className="font-mono text-[11px]" style={{ color: FARG.lightSteel }}>
{String(i + 1).padStart(2, "0")}
</span>
</div>
<div className="mt-4 text-[13px] font-semibold uppercase tracking-[0.1em]" style={{ color: FARG.graphite }}>
{f.namn}
</div>
<p className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}>
{f.syfte}
</p>
<p className="mt-4 border-t pt-4 text-[12px] leading-[18px]" style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
{f.avgransning}
</p>
</div>
))}
</div>
</Sektion>
{/* ---- Arbetsgång. Numrerad, inte illustrerad. ---- */}
<Sektion etikett="Operation" rubrik="How it works">
<ol className="max-w-[680px]">
{[
["Create organization", "Registration is submitted and reviewed."],
["Users receive accounts", "Roles are assigned by the organization administrator."],
["Guided diagnostic procedures", "Each case follows a defined procedure."],
["Verification", "Root cause is confirmed before corrective action."],
["Documentation", "The record is generated from the case log."],
["Continuous improvement", "Verified outcomes refine subsequent procedures."],
].map(([rubrik, text], i) => (
<li key={rubrik} className="flex gap-6 border-t py-6" style={{ borderColor: FARG.lightSteel }}>
<span className="font-mono text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
{String(i + 1).padStart(2, "0")}
</span>
<div>
<div className="text-[15px] font-semibold" style={{ color: FARG.graphite }}>
{rubrik}
</div>
<div className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.steel }}>
{text}
</div>
</div>
</li>
))}
</ol>
</Sektion>
{/* ---- Organisationens eget lärande ---- */}
<Sektion etikett="Capability" rubrik="Enterprise learning">
<div className="grid gap-8 md:grid-cols-2">
<div>
<p className="text-[15px] leading-[24px]" style={{ color: FARG.graphite }}>
The platform continuously refines diagnostic procedures using verified operational experience from
your own organization.
</p>
<p className="mt-4 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
Every completed diagnostic contributes to subsequent guidance. Only verified outcomes are used
a case closed without confirmed root cause contributes nothing, by design.
</p>
<p className="mt-4 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
The knowledge base belongs to the organization. It is derived from your cases, your procedures and
your documentation, and it is not aggregated across customers.
</p>
</div>
<Block rubrik="Derived from" beteckning="ALVA-SPEC-004">
<ul className="text-[14px] leading-[24px]" style={{ color: FARG.graphite }}>
{[
"Verified root causes",
"Confirmed corrective actions",
"Recurring fault categories",
"Procedure completion records",
"Organization documentation",
].map((rad) => (
<li key={rad} className="flex gap-4">
<Symbol ikon="klar" />
{rad}
</li>
))}
</ul>
</Block>
</div>
</Sektion>
{/* ---- Kvartalsrapport ---- */}
<Sektion etikett="Reporting" rubrik="Quarterly improvement report">
<p className="mb-8 max-w-[680px] text-[15px] leading-[24px]" style={{ color: FARG.steel }}>
Each quarter the organization receives an automatically generated operational report. The format is
that of an engineering audit: findings, evidence, recommendation.
</p>
<Tabell
kolumner={["Section", "Content", "Basis"]}
rader={[
["New validated diagnostic patterns", "Patterns confirmed by verified outcomes", "Case log"],
["Diagnostic precision", "Ratio of confirmed to hypothesized causes", "Verification records"],
["Recurring fault categories", "Distribution across the fleet", "Root cause analyses"],
["Recommended procedural improvements", "Steps with high exemption rates", "Procedure records"],
["Knowledge growth", "Documents, sources and coverage", "Knowledge infrastructure"],
["Organizational maturity", "Completion, verification and documentation ratios", "Aggregate"],
]}
/>
</Sektion>
{/* ---- Roadmap. Faser, inte löften med datum. ---- */}
<Sektion etikett="Development" rubrik="Roadmap">
<div className="grid gap-px md:grid-cols-4" style={{ background: FARG.lightSteel }}>
{[
["Phase 1", "Guided diagnostics", "Current"],
["Phase 2", "Organization learning", "In development"],
["Phase 3", "Performance analytics", "Specified"],
["Phase 4", "Operational intelligence", "Planned"],
].map(([fas, namn, status], i) => (
<div key={fas} className="bg-white p-6">
<Etikett ton={i === 0 ? "blue" : "steel"}>{fas}</Etikett>
<div className="mt-4 text-[15px] font-semibold" style={{ color: FARG.graphite }}>
{namn}
</div>
<div className="mt-2 text-[12px] uppercase tracking-[0.1em]" style={{ color: FARG.steel }}>
{status}
</div>
</div>
))}
</div>
</Sektion>
{/* ---- Pris. Ingen onlinebetalning. ---- */}
<Sektion etikett="Commercial" rubrik="Licensing">
<div className="grid gap-8 md:grid-cols-2">
<div>
<Tabell
kolumner={["Component", "Basis"]}
rader={[
["Platform license", "Per organization, annually"],
["User licenses", "Per active user, monthly"],
["Enterprise modules", "Optional, per module"],
]}
/>
<p className="mt-6 text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
No online payment. No subscription sign-up. Invoicing follows review of the application.
</p>
</div>
<Block rubrik="Activation sequence" beteckning="ALVA-PROC-0001">
<ol className="text-[14px] leading-[26px]" style={{ color: FARG.graphite }}>
{[
"Organization submits registration.",
"Application is reviewed.",
"Invoice is issued.",
"Payment is registered.",
"Organization is activated.",
"Additional users are billed monthly.",
].map((rad, i) => (
<li key={rad} className="flex gap-4">
<span className="font-mono text-[12px]" style={{ color: FARG.steel }}>
{String(i + 1).padStart(2, "0")}
</span>
{rad}
</li>
))}
</ol>
</Block>
</div>
</Sektion>
{/* ---- Kunskapskällor ---- */}
<Sektion etikett="Infrastructure" rubrik="Operational Knowledge Infrastructure">
<div className="grid gap-8 md:grid-cols-2">
<div>
<p className="text-[15px] leading-[24px]" style={{ color: FARG.graphite }}>
ALVA uses the organization&rsquo;s own authorized knowledge sources. The architecture is
provider-agnostic: every source implements the same interface, and no provider is assumed.
</p>
<p className="mt-4 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
A beta installation operates on internal documentation alone. External providers are enabled later
through separate connectors, without change to the platform core.
</p>
</div>
<Block rubrik="Source resolution order" beteckning="ALVA-SPEC-011">
<ol className="text-[14px] leading-[24px]" style={{ color: FARG.graphite }}>
{[
"Internal company procedures",
"OEM documentation",
"Technical bulletins",
"Warranty information",
"Service manuals",
"Historical verified diagnostics",
"Organization best practices",
].map((rad, i) => (
<li key={rad} className="flex gap-4">
<span className="font-mono text-[12px]" style={{ color: FARG.steel }}>
{i + 1}
</span>
{rad}
</li>
))}
</ol>
</Block>
</div>
</Sektion>
<div className="border-t py-16" style={{ borderColor: FARG.lightSteel }}>
<div className="mx-auto flex max-w-[1040px] flex-wrap items-baseline justify-between gap-4 px-6">
<div className="font-mono text-[11px]" style={{ color: FARG.steel }}>
{PLATTFORMSVERSION}
</div>
<div className="flex gap-4">
<Link to="/alva/ansokan">
<Knapp>Request account</Knapp>
</Link>
<Link to="/alva/logga-in">
<Knapp variant="sekundar">Customer login</Knapp>
</Link>
</div>
</div>
</div>
</Ram>
);
}