diff --git a/felsokning/app/src/alva/komponenter.tsx b/felsokning/app/src/alva/komponenter.tsx index 8c3fe51..f260abe 100644 --- a/felsokning/app/src/alva/komponenter.tsx +++ b/felsokning/app/src/alva/komponenter.tsx @@ -341,7 +341,19 @@ export function Knapp({ // `h-full` och inte bara `w-full`: lika breda knappar där den ena // texten bryts till två rader blir olika HÖGA, vilket är samma // obalans en rad ned. Bägge fyller sin cell. - className={`border px-4 py-2 text-[13px] font-semibold uppercase tracking-[0.08em] disabled:opacity-40${ + // Gemener, inte versaler. + // + // Etiketter och rubriker är versala i ALVA — de är uppmärkning, och + // ett tekniskt dokument märker upp sina fält så. En knapp är inte + // uppmärkning utan en uppmaning, och i arbetsvyn är uppmaningen ofta + // en hel mening: "Photograph the instrument panel". Versal sats är + // långsammare att läsa och bryts sämre, och just den knappen trycks + // med handskar bredvid ett fordon. + // + // Regeln gäller båda ytorna. Att låta portalen behålla versaler hade + // gett två knappspråk i samma produkt, vilket är precis det som + // skulle bort. + className={`border px-4 py-2 text-[13px] font-semibold tracking-[0.02em] disabled:opacity-40${ bred ? " h-full w-full" : "" }`} style={{ diff --git a/felsokning/app/src/felsokning/Bilagevisning.tsx b/felsokning/app/src/felsokning/Bilagevisning.tsx index 885eec1..bccf028 100644 --- a/felsokning/app/src/felsokning/Bilagevisning.tsx +++ b/felsokning/app/src/felsokning/Bilagevisning.tsx @@ -28,7 +28,7 @@ function useKalla(bilaga: Bilaga, delningskod?: string): string | null | "laddar function Platshallare({ text }: { text: string }) { return ( -
+
{text}
); diff --git a/felsokning/app/src/felsokning/DelatArendeVy.tsx b/felsokning/app/src/felsokning/DelatArendeVy.tsx index 56cd270..70ef5c9 100644 --- a/felsokning/app/src/felsokning/DelatArendeVy.tsx +++ b/felsokning/app/src/felsokning/DelatArendeVy.tsx @@ -28,8 +28,8 @@ function BeslutsKnappar({ const [fel, setFel] = useState(""); return ( -+
Your decision to the workshop
{fel}
} @@ -66,7 +66,7 @@ function BeslutsKnappar({ > {skickar ? "Sending …" : "Send decision"} -+
The decision is recorded in the case and cannot be changed here — contact the workshop if you change your mind.
> @@ -88,14 +88,14 @@ function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boole ["Status", avslutat ? "Closed" : "Diagnosis in progress"], ]; return ( -{idn.beskrivning}
}{falt .filter(([, varde]) => varde) .map(([etikett, varde]) => ( - {etikett}: {varde} + {etikett}: {varde} ))}
@@ -145,8 +145,8 @@ export function DelatArendeVy({ rubrik={b.objekt?.beskrivning ?? "Case"} hoger={ {avslutat ? "Closed" : "Diagnosis in progress"} @@ -185,7 +185,7 @@ export function DelatArendeVy({ {rader.map(([etikett, text]) => (+
{notis}
@@ -212,20 +212,20 @@ export function DelatArendeVy({{h.beskrivning}
{h.uppskattadKostnad && ( -Uppskattad kostnad: {h.uppskattadKostnad}
+Uppskattad kostnad: {h.uppskattadKostnad}
)}+
Ditt besked: {KUNDBESLUT_LABEL[beslut.handelse.beslut]}{" "} - (registrerat via {beslut.handelse.kanal}) + (registrerat via {beslut.handelse.kanal})
) : vidBeslut ? (+
Awaiting your decision — contact the workshop to approve or decline.
)} @@ -234,11 +234,11 @@ export function DelatArendeVy({- {tidKlockslag(post.tidpunkt)}{" "} +
+ {tidKlockslag(post.tidpunkt)}{" "} {handelseRubrik(post)}
))} @@ -305,13 +305,13 @@ export function DelatArendeVy({{b.totalArbetstid}
{tidsfordelningsRader(arende, nu).map((r) => ( -+
{r.label}: {r.tid}
))}+
Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log
diff --git a/felsokning/app/src/felsokning/Mikrofon.tsx b/felsokning/app/src/felsokning/Mikrofon.tsx index 6cdc173..bac9dd7 100644 --- a/felsokning/app/src/felsokning/Mikrofon.tsx +++ b/felsokning/app/src/felsokning/Mikrofon.tsx @@ -48,10 +48,10 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) { type="button" onClick={() => (lyssnar ? stoppa() : starta())} aria-label={lyssnar ? "Stop recording" : "Start voice input"} - className={`min-h-8 min-w-11 border px-3 text-[14px] font-semibold transition-colors ${ + className={`min-h-8 min-w-11 border px-4 text-[14px] font-semibold ${ lyssnar - ? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white" - : "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]" + ? "border-[#8B1A1A] bg-[#8B1A1A] text-white" + : "border-[#D7DCE2] bg-white text-[#1B1E22] hover:border-[#005CA9]" }`} > {lyssnar ? "■" :+
{f.fraga}
-+
{fel ? fel.text : f.hjalp}
diff --git a/felsokning/app/src/felsokning/__tests__/ai.test.ts b/felsokning/app/src/felsokning/__tests__/ai.test.ts index 36b3647..da532cd 100644 --- a/felsokning/app/src/felsokning/__tests__/ai.test.ts +++ b/felsokning/app/src/felsokning/__tests__/ai.test.ts @@ -147,7 +147,7 @@ describe("AI-orkestern", () => { "Reläet klickar inte.", ); expect(prompt).toContain("Volvo XC60 D4 2019"); - expect(prompt).toContain("Felbeskrivning:"); + expect(prompt).toContain("Fault description:"); expect(prompt).toContain("Kontrollera lufttryck"); expect(prompt).toContain("Ej kontrollerat enligt metodiken:"); expect(prompt).toContain("Teknikerns nya inmatning: Reläet klickar inte."); @@ -156,8 +156,8 @@ describe("AI-orkestern", () => { it("granskningsprompten innehåller hela arbetsloggen men ingen ny inmatning", () => { const arende = byggDemoArende(1); const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK); - expect(prompt).toContain("Fullständig arbetslogg:"); - expect(prompt).toContain("Arbete överlämnat från Anna till Johan"); + expect(prompt).toContain("Full work log:"); + expect(prompt).toContain("Work handed over from Anna to Johan"); expect(prompt).not.toContain("Teknikerns nya inmatning"); }); diff --git a/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts b/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts index 8b91a7a..6918ade 100644 --- a/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts +++ b/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts @@ -14,7 +14,16 @@ 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"]; +// ---- Vad testet omfattar ------------------------------------------------ +// +// Arbetsvyn låg länge utanför. Följden var förutsägbar och blev verklig: +// portalen höll åtta färger, arbetsvyn fyrtiosju, och de två delarna av +// samma produkt såg ut som två produkter. Ett designsystem som bara gäller +// halva ytan är en stilguide, inte ett system. +// +// Katalogerna nedan är därför hela ALVA-ytan — marknadswebb, portal OCH +// det gränssnitt teknikern faktiskt arbetar i. +const KATALOGER = ["src/alva", "src/pages/alva", "src/felsokning", "src/pages/felsokning"]; const filer = KATALOGER.flatMap((katalog) => readdirSync(katalog) @@ -28,7 +37,17 @@ const utanKommentarer = (kod: string) => describe("ALVA-ytan följer designsystemet", () => { it("hittar filerna att granska", () => { - expect(filer.length).toBeGreaterThanOrEqual(7); + // Höjt från 7 när arbetsvyn kom med. Talet är motvikten mot en + // katalog som råkar tömmas: ett test som granskar noll filer är + // grönt av fel skäl. + expect(filer.length).toBeGreaterThanOrEqual(25); + }); + + it("granskar både portalen och arbetsvyn", () => { + // Det som gick fel var att halva produkten stod utanför regeln. + expect(filer.some(([n]) => n.startsWith("src/pages/alva/"))).toBe(true); + expect(filer.some(([n]) => n.startsWith("src/pages/felsokning/"))).toBe(true); + expect(filer.some(([n]) => n === "src/felsokning/ui.tsx")).toBe(true); }); it.each(filer)("%s använder ingen gradient, glaseffekt eller skugga", (_namn, kod) => { diff --git a/felsokning/app/src/felsokning/__tests__/demo.test.ts b/felsokning/app/src/felsokning/__tests__/demo.test.ts index a758bde..84a1083 100644 --- a/felsokning/app/src/felsokning/__tests__/demo.test.ts +++ b/felsokning/app/src/felsokning/__tests__/demo.test.ts @@ -25,7 +25,7 @@ describe("demoärendet", () => { it("har cirka 1,5 timmars loggad arbetstid och en överlämning", () => { const b = brief(arende, VIBRATION_METODIK); - expect(b.totalArbetstid).toBe("1 tim 35 min"); + expect(b.totalArbetstid).toBe("1 h 35 min"); expect(arende.handelser.some((p) => p.handelse.typ === "overlamning")).toBe(true); expect(new Set(arende.handelser.map((p) => p.anvandare)).size).toBeGreaterThanOrEqual(2); }); diff --git a/felsokning/app/src/felsokning/__tests__/ecm.test.ts b/felsokning/app/src/felsokning/__tests__/ecm.test.ts index e32727e..6513d39 100644 --- a/felsokning/app/src/felsokning/__tests__/ecm.test.ts +++ b/felsokning/app/src/felsokning/__tests__/ecm.test.ts @@ -380,7 +380,7 @@ describe("Videoevidens (E3) och signerat avslut", () => { it("avslutet signeras och grinden redovisar signaturen", async () => { const { handelseRubrik, nyLoggPost: ny } = await import("../domain"); - expect(handelseRubrik(ny("Erik", { typ: "arende_avslutat", signatur: "Erik" }))).toContain("signerad av Erik"); + expect(handelseRubrik(ny("Erik", { typ: "arende_avslutat", signatur: "Erik" }))).toContain("signed by Erik"); const stangd = byggArende([OBJEKT, ...PREDIAG, { typ: "arende_avslutat", signatur: "Erik" }]); const rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering"); expect(rad?.ok).toBe(true); diff --git a/felsokning/app/src/felsokning/ai.ts b/felsokning/app/src/felsokning/ai.ts index 18f95b7..55d38a2 100644 --- a/felsokning/app/src/felsokning/ai.ts +++ b/felsokning/app/src/felsokning/ai.ts @@ -54,9 +54,9 @@ export interface AiSvarMedModell extends AiSvar { export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?: string): string { const rader: string[] = []; - rader.push(`Metodik: ${metodikNamn}`); - if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`); - if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`); + rader.push(`Methodology: ${metodikNamn}`); + if (brief.objekt) rader.push(`Object: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`); + if (brief.felbeskrivning) rader.push(`Fault description: ${brief.felbeskrivning}`); if (brief.utfordaKontroller.length > 0) { rader.push("Checks performed:"); for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`); @@ -81,7 +81,7 @@ export function byggGranskningsPrompt(arende: Arende, metodik: Metodik): string const logg = arende.handelser .map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`) .join("\n"); - return `${kontext}\n\nFullständig arbetslogg:\n${logg}`; + return `${kontext}\n\nFull work log:\n${logg}`; } export function tolkaAiSvar(data: unknown): AiSvar { diff --git a/felsokning/app/src/felsokning/demo.ts b/felsokning/app/src/felsokning/demo.ts index 1ad2ca5..f16ed32 100644 --- a/felsokning/app/src/felsokning/demo.ts +++ b/felsokning/app/src/felsokning/demo.ts @@ -16,14 +16,14 @@ function demoFoto(text: string): string { canvas.height = 360; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("canvas saknas"); - ctx.fillStyle = "#27272a"; + ctx.fillStyle = "#1B1E22"; ctx.fillRect(0, 0, 480, 360); - ctx.strokeStyle = "#52525b"; + ctx.strokeStyle = "#4D5662"; ctx.lineWidth = 22; ctx.beginPath(); ctx.arc(240, 170, 110, 0, Math.PI * 2); ctx.stroke(); - ctx.strokeStyle = "#a1a1aa"; + ctx.strokeStyle = "#4D5662"; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(240, 170, 58, 0, Math.PI * 2); @@ -35,11 +35,11 @@ function demoFoto(text: string): string { ctx.lineTo(240 + Math.cos(v) * 58, 170 + Math.sin(v) * 58); ctx.stroke(); } - ctx.fillStyle = "#fbbf24"; + ctx.fillStyle = "#8A5A00"; ctx.font = "bold 22px sans-serif"; ctx.textAlign = "center"; ctx.fillText(text, 240, 330); - ctx.fillStyle = "#71717a"; + ctx.fillStyle = "#4D5662"; ctx.font = "14px sans-serif"; ctx.fillText("Demobild", 240, 26); return canvas.toDataURL("image/jpeg", 0.7); diff --git a/felsokning/app/src/felsokning/domain.ts b/felsokning/app/src/felsokning/domain.ts index c846094..5cb6769 100644 --- a/felsokning/app/src/felsokning/domain.ts +++ b/felsokning/app/src/felsokning/domain.ts @@ -279,18 +279,18 @@ export function handelseRubrik(post: LoggPost): string { case "objekt_identifierat": return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`; case "arbetsorder_skannad": - return `Arbetsorder skannad — ${h.falt.length} fält tolkade`; + return `Work order scanned — ${h.falt.length} fields interpreted`; case "felbeskrivning": return `Felbeskrivning registrerad`; case "fraga_besvarad": return `${h.fraga} — ${h.svar}`; case "kontroll_utford": if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`; - return h.resultat ? `${h.text} — ${h.resultat}` : `${h.text} — utförd`; + return h.resultat ? `${h.text} — ${h.resultat}` : `${h.text} — performed`; case "observation": return `Observation: ${h.text}`; case "matvarde": - return `Mätvärde: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`; + return `Measurement: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`; case "hypotes": return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`; case "foto": @@ -304,18 +304,18 @@ export function handelseRubrik(post: LoggPost): string { case "inaktivitet_forklarad": return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`; case "overlamning": - return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`; + return h.till ? `Work handed over from ${h.fran} to ${h.till}` : `Work handed over by ${h.fran}`; case "ansvarig_satt": return `Ansvarig tekniker: ${h.ansvarig}`; case "arendetyp_satt": - return `Ärendetyp: ${h.arendetyp}`; + return `Case type: ${h.arendetyp}`; case "historik_kontrollerad": return h.kontrollerad ? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}` : `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`; case "matarstallning": - if (h.undantag) return `Mätarställning (${h.lage === "ingaende" ? "in" : "ut"}) kunde inte dokumenteras: ${h.undantag}`; - return `Mätarställning ${h.lage === "ingaende" ? "in" : "ut"}: ${h.varde}`; + if (h.undantag) return `Odometer reading (${h.lage === "ingaende" ? "in" : "out"}) could not be documented: ${h.undantag}`; + return `Odometer ${h.lage === "ingaende" ? "in" : "out"}: ${h.varde}`; case "reproducering": return h.status === "ja" ? `Symptomet reproducerat: ${h.beskrivning}` @@ -325,22 +325,22 @@ export function handelseRubrik(post: LoggPost): string { case "felorsak": return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse} — ${h.orsaker.join(", ")}`; case "atgardsforslag": - return `Åtgärdsförslag till kund: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`; + return `Proposed action for the customer: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`; case "kundbeslut": return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? ` — ${h.kommentar}` : ""}`; case "atgard_utford": return h.utford - ? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}` - : `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`; + ? `Action performed: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}` + : `No action performed — ${h.motivering ?? "reason missing"}`; case "kvalitetskontroll": return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`; case "export_skapad": return `Export skapad: ${h.format}, version ${h.version}`; case "ai_svar": { const forsta = h.rader[0]; - return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`; + return `AI: ${forsta ? `${forsta.text} ` : ""}— Next step: ${h.nastaSteg}`; } case "arende_avslutat": - return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Diagnosis closed"; + return h.signatur ? `Diagnosis closed — signed by ${h.signatur}` : "Diagnosis closed"; } } diff --git a/felsokning/app/src/felsokning/ecm.ts b/felsokning/app/src/felsokning/ecm.ts index a41e3da..12f396b 100644 --- a/felsokning/app/src/felsokning/ecm.ts +++ b/felsokning/app/src/felsokning/ecm.ts @@ -751,7 +751,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { rubrik: "Photographs present for checks that require them", ok: fotoKravUtanFoto === 0, kravs: true, - detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined, + detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} check(s) requiring a photograph have no image in the log.` : undefined, }); // Compliance Engine: ärendetypens regler ur det aktiva regelpaketet @@ -794,7 +794,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { rader.push({ id: "evidensniva", - rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`, + rubrik: `Evidence level: ${EVIDENS_LABEL[evidensNiva(arende)]}`, ok: evidensNiva(arende) !== "E0", kravs: true, detalj: evidensNiva(arende) === "E0" ? "No evidence in the log yet." : undefined, diff --git a/felsokning/app/src/felsokning/projektioner.ts b/felsokning/app/src/felsokning/projektioner.ts index f863011..6263228 100644 --- a/felsokning/app/src/felsokning/projektioner.ts +++ b/felsokning/app/src/felsokning/projektioner.ts @@ -252,7 +252,7 @@ export function formateraTid(ms: number): string { const h = Math.floor(minuter / 60); const m = minuter % 60; if (h === 0) return `${m} min`; - return `${h} tim ${m} min`; + return `${h} h ${m} min`; } export interface TillforlitlighetsRad { @@ -328,9 +328,9 @@ export function brief(arende: Arende, metodik: Metodik, nu?: string): Brief { export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string): string { const b = brief(arende, metodik, nu); const rader: string[] = []; - rader.push(`ÖVERLÄMNING – Ärende #${arende.nummer}`); - if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Kund: ${b.objekt.kund}` : ""}`); - if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`); + rader.push(`HANDOVER – Case #${arende.nummer}`); + if (b.objekt) rader.push(`Object: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Customer: ${b.objekt.kund}` : ""}`); + if (b.felbeskrivning) rader.push(`Fault description: ${b.felbeskrivning}`); rader.push(""); rader.push("Checks performed:"); if (b.utfordaKontroller.length === 0) rader.push(" (none yet)"); diff --git a/felsokning/app/src/felsokning/ui.tsx b/felsokning/app/src/felsokning/ui.tsx index 1ed2cd6..c49a290 100644 --- a/felsokning/app/src/felsokning/ui.tsx +++ b/felsokning/app/src/felsokning/ui.tsx @@ -1,9 +1,32 @@ // Delade UI-komponenter för Guidad Felsökning. -// Designfilosofi: industriellt verkstadsverktyg i ETKA-tradition — -// plana ytor, skarpa kanter, tät informationslayout, dämpad palett -// (ljusgrå ytor, djup marinblå som primärfärg), inga dekorationer. -// Verktygsrad ~44 px, rektangulära knappar (max 4 px radie), kompakta -// formulär med vänsterställda etiketter. +// +// ---- Ett system, inte två ----------------------------------------------- +// +// Det här filhuvudet beskrev tidigare en egen designfilosofi: "industriellt +// verkstadsverktyg i ETKA-tradition" med egen palett, eget snitt och egen +// rytm. Den beskrivningen var inte fel i sak — men den var en ANDRA +// beskrivning, vid sidan av ALVA-SPEC-001, och två designsystem i samma +// produkt blir aldrig lika. De blev det inte heller: portalen höll åtta +// färger, arbetsvyn fyrtiosju. +// +// Skillnaden syntes värst i typsnittet. Arbetsvyn låg utanför `.alva-yta` +// och ärvde därför värdapplikationens antikva — samma fynd som en gång +// gjordes om portalen, fast här hade det stått kvar. En tekniker som +// klickar från portalen in i ett ärende bytte alltså både färg och +// bokstavsform, och läste det som två produkter. +// +// Komponenterna nedan bär nu ALVA:s språk: FARG-paletten, 8 px-rutnätet, +// versala sektionsetiketter, inga skuggor, ingen rörelse. +// +// ---- Det som ändå skiljer, och varför ------------------------------------ +// +// STORLEK. Knappar och träffytor är större här än i portalen. Det är inte +// en annan estetik utan ett annat bruk: portalen läses vid ett skrivbord, +// arbetsvyn används med handskar bredvid ett fordon. Måtten följer samma +// rutnät, de är bara fler steg på det. +// +// TÄTHET. Arbetsvyn visar mer per skärm. Samma skäl: teknikern ska se +// ärendets tillstånd utan att bläddra. import { Link } from "react-router-dom"; import { useRef, type ReactNode } from "react"; @@ -11,13 +34,8 @@ import type { Tillforlitlighet } from "./domain"; import { TILLFORLITLIGHET_LABEL } from "./domain"; import { MikrofonKnapp } from "./Mikrofon"; import { IkonPunkt } from "./ikoner"; - -// Industriell palett (inspirerad av tyska verkstadssystem): -// bakgrund #ECECEC · paneler #F7F7F7 · kanter #C6C6C6 -// primär (djup blå) #00437A · markerad rad #D6E4F2 -// varning #9A6700 · fel #8B1A1A · ok #1E6B34 - -const TYPSNITT = "'Inter', 'IBM Plex Sans', 'Segoe UI', Roboto, system-ui, sans-serif"; +import { Etikett, FARG } from "@/alva/komponenter"; +import { ALVA } from "@/alva/system"; export function FelsokningSkal({ rubrik, @@ -34,32 +52,62 @@ export function FelsokningSkal({ }) { const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl"; return ( + // `alva-yta` bär typografin ur ALVA-SPEC-001 §6 och håller den skild + // från värdapplikationens antikva. Utan den klassen ärver arbetsvyn + // butikens snitt, vilket är exakt vad som hände innan.