Bäddad typografi, tätare spärrning, räta hörn och ritningsstämpel

Fyra saker som alla svarar på samma sak: ALVA såg fortfarande inte ut som
det den påstår sig vara.

Snittet som fil. Fallkedjan säkrade ordningen men inte utfallet — utan
DIN installerad föll ytan ned till vad enheten råkade ha, Helvetica på en
iPhone och Roboto på en Android. Ett system som beskriver sig som en
standard får inte se olika ut beroende på var det öppnas. IBM Plex Sans,
specens tredje tillåtna snitt, bakas därför in: latinsk delmängd, tre
textvikter och två för monospace, drygt 100 kB. Licensen följer med.

Vite bakar in woff2 som data-URI. Det är inte en optimering utan ett
krav: ALVA publiceras bland annat som en självbärande sida bakom en CSP
där varje extern begäran blockeras, och en @font-face mot en assetfil
hade tyst fallit tillbaka till enhetens eget snitt — alltså precis det
inbäddningen ska avskaffa. Bara woff2; värdapplikationens bilder är
megabyte stora och förblir egna filer.

Spärrningen ett steg tätare genomgående. DIN-satta OEM-dokument är tätare
än vad ytan var, och med rätt bokstavsform tål den det.

Räta hörn i felsökningen. 69 förekomster av rundad radie, alla ur
Lovable-arvet. Ett verkstadssystem har räta hörn.

Ritningsstämpel sist i rapporten: beteckning, regelpaket, evidensmodell,
plattformsversion, status och utgåvedatum. En granskare måste kunna se
vilka versioner som gällde när ärendet stängdes, inte vilka som gäller
den dag rapporten läses.

Fyra tester låser inbäddningen, inklusive att ingen @font-face pekar mot
en extern värd.

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-06 03:08:30 +00:00
parent 9668cc256b
commit 66762629df
27 changed files with 367 additions and 90 deletions
+6 -6
View File
@@ -41,7 +41,7 @@ export const FARG = {
export function Etikett({ children, ton = "steel" }: { children: ReactNode; ton?: "steel" | "blue" | "graphite" }) {
const färg = ton === "blue" ? FARG.blue : ton === "graphite" ? FARG.graphite : FARG.steel;
return (
<div className="text-[11px] font-semibold uppercase tracking-[0.12em]" style={{ color: färg }}>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: färg }}>
{children}
</div>
);
@@ -51,7 +51,7 @@ export function Rubrik({ children, niva = 2 }: { children: ReactNode; niva?: 1 |
const storlek = niva === 1 ? "text-[32px] leading-[40px]" : niva === 2 ? "text-[22px] leading-[32px]" : "text-[16px] leading-[24px]";
const Tagg = (niva === 1 ? "h1" : niva === 2 ? "h2" : "h3") as "h1";
return (
<Tagg className={`${storlek} font-semibold uppercase tracking-[0.06em]`} style={{ color: FARG.graphite }}>
<Tagg className={`${storlek} font-semibold uppercase tracking-[0.02em]`} style={{ color: FARG.graphite }}>
{children}
</Tagg>
);
@@ -101,7 +101,7 @@ export function Statusmärke({ status }: { status: Status }) {
const ram = status === "failed" || status === "blocked";
return (
<span
className="inline-block border px-2 py-[2px] text-[11px] font-semibold uppercase tracking-[0.1em]"
className="inline-block border px-2 py-[2px] text-[11px] font-semibold uppercase tracking-[0.08em]"
style={{
color: STATUSFARG[status],
borderColor: ram ? FARG.graphite : FARG.lightSteel,
@@ -185,7 +185,7 @@ export function Fasrad({ aktiv, klara = [] }: { aktiv: Fas; klara?: Fas[] }) {
och statussymbolen informationen — vilket är precis vad
A·L·V·A är till för — medan raden ovanför namnger den
aktiva fasen och dess syfte i klartext. */}
<div className="mt-2 hidden text-[11px] font-semibold uppercase tracking-[0.1em] sm:block">{f.namn}</div>
<div className="mt-2 hidden text-[11px] font-semibold uppercase tracking-[0.08em] sm:block">{f.namn}</div>
<span className="sr-only">{f.namn}</span>
<div className="mt-2 hidden text-[11px] leading-[16px] sm:block" style={{ opacity: ärAktiv ? 0.9 : 0.75 }}>
{f.syfte}
@@ -333,7 +333,7 @@ export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNo
<th
key={k}
scope="col"
className="border-b px-4 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.08em]"
style={{ borderColor: FARG.lightSteel, color: FARG.steel }}
>
{k}
@@ -369,7 +369,7 @@ export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNo
{rad.map((cell, j) => (
<div key={j} className="mt-2 flex flex-wrap items-baseline gap-2 first:mt-0">
<dt
className="w-[96px] shrink-0 text-[11px] font-semibold uppercase tracking-[0.1em]"
className="w-[96px] shrink-0 text-[11px] font-semibold uppercase tracking-[0.08em]"
style={{ color: FARG.steel }}
>
{kolumner[j]}
+62
View File
@@ -0,0 +1,62 @@
/* ALVA-SPEC-001 · paragraf 6 — bokstavsformen som fil.
*
* Fallkedjan i src/alva/system.ts sätter DIN 2014 först, därför att en
* organisation som licensierat DIN ska få DIN. Men de flesta har inte
* DIN installerat, och då föll ALVA tidigare ned till vad enheten råkade
* ha — Helvetica på en iPhone, Roboto på en Android, något annat på en
* verkstadsdator. Ett system som beskriver sig som en standard får inte
* se olika ut beroende på var det öppnas.
*
* IBM Plex Sans är därför inbakad. Den är specens tredje tillåtna snitt,
* fritt licensierad under SIL Open Font License (se LICENSE i den här
* katalogen), och delar DIN:s karaktär: rak, teknisk, byggd för
* ingenjörsdokument snarare än för löptext.
*
* Bara latinsk delmängd och tre vikter — 400 för text, 600 för etiketter
* och rubriker, 700 för ordmärket. Plus två vikter Plex Mono för
* beteckningar och mätvärden. Fem filer, drygt 100 kB.
*
* `font-display: swap` är avsiktligt: texten ska synas direkt i
* fallkedjans snitt och bytas när filen kommit fram. Ett tomt fält i en
* verkstad är sämre än ett fält i fel bokstavsform.
*/
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./typsnitt/ibm-plex-sans-latin-400-normal.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(./typsnitt/ibm-plex-sans-latin-600-normal.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(./typsnitt/ibm-plex-sans-latin-700-normal.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./typsnitt/ibm-plex-mono-latin-400-normal.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(./typsnitt/ibm-plex-mono-latin-600-normal.woff2) format("woff2");
}
+93
View File
@@ -0,0 +1,93 @@
Copyright 2019 IBM Corp. All rights reserved. IBMPlexSans-Italic[wdth,wght].ttf: Copyright 2019 IBM Corp. All rights reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -28,7 +28,7 @@ function useKalla(bilaga: Bilaga, delningskod?: string): string | null | "laddar
function Platshallare({ text }: { text: string }) {
return (
<p className="rounded border border-dashed border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#707070]">
<p className="border border-dashed border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#707070]">
{text}
</p>
);
@@ -50,7 +50,7 @@ function BeslutsKnappar({
value={kommentar}
maxLength={500}
onChange={(e) => setKommentar(e.target.value)}
className="w-full rounded border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] focus:border-[#00437A] focus:outline-none"
className="w-full border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] focus:border-[#00437A] focus:outline-none"
/>
</label>
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
@@ -88,7 +88,7 @@ function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boole
["Status", avslutat ? "Avslutat" : "Felsökning pågår"],
];
return (
<div className="sticky top-11 z-10 mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.1)] print:static">
<div className="sticky top-11 z-10 mb-3 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.1)] print:static">
{idn.beskrivning && <p className="text-[13px] font-semibold">{idn.beskrivning}</p>}
<p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]">
{falt
@@ -145,7 +145,7 @@ export function DelatArendeVy({
rubrik={b.objekt?.beskrivning ?? "Ärende"}
hoger={
<span
className={`rounded px-3 py-1 text-[12px] font-semibold uppercase ${
className={`px-3 py-1 text-[12px] font-semibold uppercase ${
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
}`}
>
@@ -193,7 +193,7 @@ export function DelatArendeVy({
);
})()}
<p className="mb-4 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
<p className="mb-4 border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
{notis}
</p>
@@ -246,7 +246,7 @@ export function DelatArendeVy({
<Panel rubrik="Video">
{klipp.map((v, i) => (
<figure key={i} className="mb-2">
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full rounded border border-[#C6C6C6]" />
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
</figure>
))}
@@ -257,7 +257,7 @@ export function DelatArendeVy({
<div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => (
<figure key={i}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="rounded border border-[#C6C6C6]" />
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
</figure>
))}
+1 -1
View File
@@ -48,7 +48,7 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
type="button"
onClick={() => (lyssnar ? stoppa() : starta())}
aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"}
className={`min-h-8 min-w-11 rounded border px-3 text-[14px] font-semibold transition-colors ${
className={`min-h-8 min-w-11 border px-3 text-[14px] font-semibold transition-colors ${
lyssnar
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
@@ -194,3 +194,47 @@ describe("ALVA-ytan bär sin egen typografi", () => {
expect(css).toContain("tabular-nums");
});
});
// ---- Snittet måste följa med i bygget ----------------------------------
//
// Fallkedjan säkrar ordningen men inte utfallet: utan DIN installerad
// föll ALVA ned till vad enheten råkade ha — Helvetica på en iPhone,
// Roboto på en Android. Ett system som beskriver sig som en standard får
// inte se olika ut beroende på var det öppnas.
//
// IBM Plex Sans är därför inbakad som fil. Den måste dessutom ligga som
// data i CSS:en och inte som en separat asset: ALVA publiceras bland
// annat som en självbärande sida bakom en CSP där varje extern begäran
// blockeras, och en @font-face mot en assetfil hade tyst fallit tillbaka
// till enhetens eget snitt — alltså precis det inbäddningen avskaffar.
describe("ALVA:s snitt följer med i bygget", () => {
const typsnitt = readFileSync("src/alva/typsnitt.css", "utf8");
const vite = readFileSync("vite.config.ts", "utf8");
it("de tre textvikterna och två monovikterna finns som filer", () => {
for (const fil of [
"ibm-plex-sans-latin-400-normal.woff2",
"ibm-plex-sans-latin-600-normal.woff2",
"ibm-plex-sans-latin-700-normal.woff2",
"ibm-plex-mono-latin-400-normal.woff2",
"ibm-plex-mono-latin-600-normal.woff2",
]) {
expect(typsnitt, fil).toContain(fil);
expect(readFileSync(`src/alva/typsnitt/${fil}`).byteLength).toBeGreaterThan(8000);
}
});
it("licensen ligger bredvid filerna", () => {
// SIL Open Font License kräver att licenstexten följer med.
expect(readFileSync("src/alva/typsnitt/LICENSE", "utf8")).toMatch(/SIL OPEN FONT LICENSE/i);
});
it("bygget bakar in woff2 som data i stället för som separata filer", () => {
expect(vite).toContain("assetsInlineLimit");
expect(vite).toContain(".woff2");
});
it("ingen @font-face hämtar från en extern värd", () => {
expect(typsnitt).not.toMatch(/url\(\s*["']?https?:/);
});
});
+3 -3
View File
@@ -90,7 +90,7 @@ export function StorKnapp({
type={type}
onClick={onClick}
disabled={disabled}
className={`min-h-10 w-full rounded px-3 py-1.5 text-[13px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${farg} ${className}`}
className={`min-h-10 w-full px-3 py-1.5 text-[13px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${farg} ${className}`}
>
{children}
</button>
@@ -99,7 +99,7 @@ export function StorKnapp({
export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) {
return (
<section className="mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
<section className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-3 shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
{rubrik && (
<h2 className="mb-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
{rubrik}
@@ -147,7 +147,7 @@ export function TextFalt({
const vardeRef = useRef(varde);
vardeRef.current = varde;
const klass =
"w-full rounded border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] text-[#1A1A1A] placeholder-[#9AA0A6] focus:border-[#00437A] focus:outline-none focus:ring-1 focus:ring-[#00437A]";
"w-full border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] text-[#1A1A1A] placeholder-[#9AA0A6] focus:border-[#00437A] focus:outline-none focus:ring-1 focus:ring-[#00437A]";
return (
<label className="mb-2.5 block">
<span className="mb-1 flex min-h-6 items-center justify-between gap-2">
+4
View File
@@ -1,3 +1,7 @@
/* ALVA:s eget snitt, inbakat som data. Se src/alva/typsnitt.css.
@import måste stå före allt annat, inklusive Tailwind-direktiven. */
@import './alva/typsnitt.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
+1 -1
View File
@@ -52,7 +52,7 @@ function Tal({ tal }: { tal: Nyckeltal }) {
</>
)}
</div>
<div className="mt-2 text-[11px] uppercase tracking-[0.1em]" style={{ color: FARG.steel }}>
<div className="mt-2 text-[11px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{tal.riktning === "hogre" ? "↑ bättre" : "↓ bättre"} · {tal.underlag}
</div>
<p className="mt-4 border-t pt-4 text-[12px] leading-[18px]" style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
+2 -2
View File
@@ -89,7 +89,7 @@ export default function Ansokan() {
<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 }}>
<label htmlFor={f.namn} className="block text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{f.etikett}
{f.kravs && <span aria-hidden="true"> ·</span>}
{f.kravs && <span className="sr-only"> (required)</span>}
@@ -105,7 +105,7 @@ export default function Ansokan() {
</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 }}>
<label htmlFor="noteringar" className="block text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
Notes
</label>
<textarea
+2 -2
View File
@@ -44,7 +44,7 @@ export default function LoggaIn() {
<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]"
className="block text-[11px] font-semibold uppercase tracking-[0.08em]"
style={{ color: FARG.steel }}
>
{f.etikett}
@@ -69,7 +69,7 @@ export default function LoggaIn() {
<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 }}>
<a href="/alva/ansokan" className="text-[12px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
Forgot password
</a>
</div>
+3 -3
View File
@@ -34,10 +34,10 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
<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 }}>
<span className="text-[20px] font-semibold tracking-[0.02em]" style={{ color: FARG.graphite }}>
{ALVA.namn}
</span>
<span className="hidden text-[10px] uppercase tracking-[0.16em] sm:inline" style={{ color: FARG.steel }}>
<span className="hidden text-[10px] uppercase tracking-[0.08em] sm:inline" style={{ color: FARG.steel }}>
{ALVA.utlast}
</span>
</Link>
@@ -54,7 +54,7 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
<Link
to={l.till}
aria-current={aktiv ? "page" : undefined}
className="text-[12px] font-semibold uppercase tracking-[0.1em]"
className="text-[12px] font-semibold uppercase tracking-[0.08em]"
style={{ color: aktiv ? FARG.blue : FARG.steel }}
>
{l.text}
+4 -4
View File
@@ -40,10 +40,10 @@ export default function Start() {
<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 }}>
<p className="mt-4 text-[15px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{ALVA.utlast}
</p>
<p className="mt-2 text-[13px] uppercase tracking-[0.14em]" style={{ color: FARG.steel }}>
<p className="mt-2 text-[13px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{ALVA.position}
</p>
@@ -80,7 +80,7 @@ export default function Start() {
{String(i + 1).padStart(2, "0")}
</span>
</div>
<div className="mt-4 text-[13px] font-semibold uppercase tracking-[0.1em]" style={{ color: FARG.graphite }}>
<div className="mt-4 text-[13px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.graphite }}>
{f.namn}
</div>
<p className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}>
@@ -191,7 +191,7 @@ export default function Start() {
<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 }}>
<div className="mt-2 text-[12px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{status}
</div>
</div>
@@ -9,7 +9,7 @@ import { Fasrad } from "@/alva/komponenter";
import { Slutsatspanel } from "@/felsokning/Slutsats";
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
import { grinda } from "../../../../services/gemensam/grind.mjs";
import { arendebeteckning, fasDefinition } from "@/alva/system";
import { PLATTFORMSVERSION, arendebeteckning, fasDefinition } from "@/alva/system";
import {
arAvslutat,
arendeidentitet,
@@ -188,12 +188,12 @@ export default function ArendeSida() {
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
vänster, arbetsyta i mitten, kontextpanel till höger. På smala
skärmar: flikrad + en kolumn. */}
<nav className="mb-3 grid grid-cols-4 gap-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden">
<nav className="mb-3 grid grid-cols-4 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden">
{FLIKAR.map((f) => (
<button
key={f.id}
onClick={() => setFlik(f.id)}
className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
className={`min-h-9 text-[13px] font-semibold transition-colors ${
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
}`}
>
@@ -252,7 +252,7 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
</span>
) : null;
return (
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
{del("AO", idn.arbetsorder)}
{del("Claim", idn.claim)}
{del("Skadenr", idn.skadenummer)}
@@ -267,7 +267,7 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
value={typ}
disabled={idn.avslutat}
onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })}
className="rounded border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none"
className="border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none"
>
{ARENDETYPER.map((t) => (
<option key={t}>{t}</option>
@@ -329,11 +329,11 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
{foto && !laser && (
<div className="mt-2">
{demo && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
Demo-avläsning bildtolkningen kräver inloggning. Kontrollera värdet.
</p>
)}
<img src={foto} alt="Instrumentpanel" className="mb-2 max-h-40 rounded border border-[#C6C6C6]" />
<img src={foto} alt="Instrumentpanel" className="mb-2 max-h-40 border border-[#C6C6C6]" />
<TextFalt label={`Mätarställning (${lage === "ingaende" ? "ingående" : "utgående"})`} varde={varde} satt={setVarde} platshallare="T.ex. 84 320 km" />
<StorKnapp
disabled={!varde.trim()}
@@ -390,7 +390,7 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
if (rader.length === 0) return null;
return (
<div className="mb-2 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
Tidigare ärenden {identifierare} granska innan historiken kvitteras
</p>
@@ -663,7 +663,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
<button
key={o}
onClick={() => vaxla(orsaker, setOrsaker, o)}
className={`min-h-8 rounded border px-1.5 text-[12px] font-medium ${
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
orsaker.includes(o) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
}`}
>
@@ -677,7 +677,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
<button
key={k}
onClick={() => vaxla(underlag, setUnderlag, k)}
className={`min-h-8 rounded border px-1.5 text-[12px] font-medium ${
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
underlag.includes(k) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
}`}
>
@@ -691,7 +691,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
<button
key={n}
onClick={() => setSakerhet(n)}
className={`min-h-9 rounded border text-[12px] font-semibold ${
className={`min-h-9 border text-[12px] font-semibold ${
sakerhet === n ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
@@ -824,7 +824,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{forslagsLage && (
<div className="mb-3 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
<TextFalt
label="Föreslagen åtgärd (visas för kunden i delningsvyn)"
varde={forslagText}
@@ -858,7 +858,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{forslag.length > 0 && !beslut && (
<div className="mb-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2">
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
Registrera kundens besked innan arbetet påbörjas
</p>
@@ -867,7 +867,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={val}
onClick={() => setBeslutsVal(val)}
className={`min-h-9 rounded border text-[12px] font-semibold ${
className={`min-h-9 border text-[12px] font-semibold ${
beslutsVal === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
@@ -883,7 +883,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={k}
onClick={() => setKanal(k)}
className={`min-h-8 rounded border px-1 text-[12px] font-medium ${
className={`min-h-8 border px-1 text-[12px] font-medium ${
kanal === k ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
}`}
>
@@ -973,7 +973,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={val}
onClick={() => setOrsak(val)}
className={`min-h-8 rounded border px-2 text-left text-[12px] font-medium ${
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
}`}
>
@@ -1020,7 +1020,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={val}
onClick={() => setKkResultat(val)}
className={`min-h-9 rounded border px-2 text-[12px] font-semibold ${
className={`min-h-9 border px-2 text-[12px] font-semibold ${
kkResultat === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
@@ -1078,7 +1078,7 @@ function VyTrad({
const steg = nastaSteg(arende, metodik);
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
return (
<nav className="overflow-hidden rounded border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
Ärendevyer
</p>
@@ -1200,7 +1200,7 @@ function InaktivitetsBanner({
const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000);
if (minuter < 20) return null;
return (
<div className="mb-4 rounded border border-[#00437A] bg-[#F7F7F7] p-4">
<div className="mb-4 border border-[#00437A] bg-[#F7F7F7] p-4">
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
</p>
@@ -1472,7 +1472,7 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
<button
key={k}
onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })}
className={`whitespace-nowrap rounded border px-4 py-2 text-[12px] font-semibold transition-colors ${
className={`whitespace-nowrap border px-4 py-2 text-[12px] font-semibold transition-colors ${
k === aktiv
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
@@ -1638,7 +1638,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
);
}
return (
<div className="mt-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2">
<div className="mt-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
Kontrollen dokumenteras utan underlag ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
</p>
@@ -1647,7 +1647,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
<button
key={val}
onClick={() => setOrsak(val)}
className={`min-h-8 rounded border px-2 text-left text-[12px] font-medium ${
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
}`}
>
@@ -1780,7 +1780,7 @@ function SnabbDokumentation({
? instRef.current?.click()
: setTyp(typ === d.id ? null : d.id)
}
className={`min-h-9 rounded border text-[12px] font-semibold transition-colors ${
className={`min-h-9 border text-[12px] font-semibold transition-colors ${
typ === d.id
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
@@ -1836,8 +1836,8 @@ function SnabbDokumentation({
/>
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
{video && (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-2">
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full rounded border border-[#C6C6C6]" />
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#C6C6C6]" />
<TextFalt
label="Vad visar videon? (obligatoriskt — det som låter eller rör sig)"
varde={video.beskrivning}
@@ -1864,9 +1864,9 @@ function SnabbDokumentation({
)}
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Systemet läser av instrumentet </p>}
{avlasning && (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
{avlasning.demo && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
Demo-avläsning bildtolkningen kräver inloggning. Värdena är exempeldata.
</p>
)}
@@ -1954,9 +1954,9 @@ function OverlamningDialog({
return (
<div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center">
<div className="max-h-full w-full max-w-2xl overflow-y-auto rounded border border-[#ADADAD] bg-[#F7F7F7] p-4">
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#ADADAD] bg-[#F7F7F7] p-4">
<h2 className="mb-3 text-[15px] font-semibold">Överlämningsrapport</h2>
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded bg-white p-4 text-[12px] text-[#333333]">
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#333333]">
{text}
</pre>
{aiLage === "vilar" && (
@@ -1969,7 +1969,7 @@ function OverlamningDialog({
<p className="mb-3 font-semibold text-[#8B1A1A]">Kunde inte hämtas kräver inloggning. Överlämningen fungerar ändå.</p>
)}
{aiLage === "klar" && aiRader.length > 0 && (
<div className="mb-3 rounded bg-white p-4">
<div className="mb-3 bg-white p-4">
{aiRader.map((rad, i) => (
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
@@ -2035,7 +2035,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
}
return (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-3">
<div className="mt-3 border border-[#C6C6C6] bg-white p-3">
{fel && <p className="mb-2 font-semibold text-[#8B1A1A]">{fel}</p>}
{delningar.map((delning) => (
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#DDDDDD] py-2 last:border-0">
@@ -2046,7 +2046,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{!delning.aterkallad && (
<span className="flex gap-2">
<button
className="rounded border border-[#ADADAD] px-3 py-1 font-semibold text-[#333333] hover:border-[#00437A]"
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#333333] hover:border-[#00437A]"
onClick={() =>
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
}
@@ -2054,7 +2054,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
Kopiera
</button>
<button
className="rounded border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
onClick={async () => {
await aterkallaDelning(delning.kod);
skicka({ typ: "kommentar", text: `Delningslänk (${NIVA_LABEL[delning.niva]}) återkallad.` });
@@ -2071,7 +2071,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => (
<button
key={niva}
className="min-h-9 rounded border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]"
className="min-h-9 border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]"
onClick={async () => {
try {
await skapaDelning(arende.id, niva);
@@ -2105,10 +2105,10 @@ function LoggFlik({ arende }: { arende: Arende }) {
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
<p className="text-[11px] text-[#707070]">{post.anvandare}</p>
{post.handelse.typ === "foto" && (
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-1 max-h-40 rounded border border-[#C6C6C6]" />
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-1 max-h-40 border border-[#C6C6C6]" />
)}
{post.handelse.typ === "video" && (
<Klipp bilaga={post.handelse} className="mt-1 max-h-40 rounded border border-[#C6C6C6]" />
<Klipp bilaga={post.handelse} className="mt-1 max-h-40 border border-[#C6C6C6]" />
)}
</div>
</li>
@@ -2526,7 +2526,7 @@ function RapportFlik({
<Panel rubrik="Dokumenterad video">
{klipp.map((v, i) => (
<figure key={i} className="mb-2">
<Klipp bilaga={v.bilaga} className="w-full rounded border border-[#C6C6C6]" />
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
</figure>
))}
@@ -2537,7 +2537,7 @@ function RapportFlik({
<div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => (
<figure key={i}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="rounded border border-[#C6C6C6]" />
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
</figure>
))}
@@ -2554,6 +2554,46 @@ function RapportFlik({
<p className="text-center text-[11px] text-[#8A8A8A]">
Genererad ur ärendets händelselogg · Observationer och mätvärden redovisas utan slutsatser som saknar stöd.
</p>
<Ritningsstampel arende={arende} />
</>
);
}
/**
* Ritningsstämpel.
*
* En teknisk ritning bär alltid ett hörn med beteckning, revision och
* status, så att ett papper på en verkstadsbänk går att härleda till
* exakt vilken utgåva det kom ur. En rapport som ska hålla i en tvist
* behöver samma sak: läsaren måste kunna se vilket regelpaket och vilken
* evidensmodell som gällde när ärendet stängdes — inte vilka som gäller
* den dag rapporten läses.
*
* Varje fält är något en granskare efterfrågar innan den litar på resten
* av dokumentet. Inget av dem är dekoration.
*/
function Ritningsstampel({ arende }: { arende: Arende }) {
const paket = aktivtRegelpaket();
const avslutad = [...arende.handelser].reverse().find((p) => p.handelse.typ === "arende_avslutat");
const falt: [string, string][] = [
["Beteckning", arendebeteckning(arende.nummer)],
["Regelpaket", paket.version],
["Evidensmodell", `ECM ${ECM_VERSION}`],
["Plattform", PLATTFORMSVERSION],
["Status", avslutad ? "Closed" : "In progress"],
["Utgåva", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"],
];
return (
<div className="alva-yta mt-3 border border-[#C6C6C6] bg-white">
<div className="grid grid-cols-2 sm:grid-cols-3">
{falt.map(([etikett, varde]) => (
<div key={etikett} className="border-b border-r border-[#DDDDDD] px-2 py-2 last:border-r-0">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#4A5560]">{etikett}</div>
<div className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div>
</div>
))}
</div>
</div>
);
}
@@ -50,7 +50,7 @@ function PlattformInloggning() {
{konto.organisation} synk och beslutsstöd aktiva.
</p>
<button
className="whitespace-nowrap rounded border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
onClick={() => {
loggaUtPlattform();
setKonto(null);
@@ -189,7 +189,7 @@ function AnvandarAdmin() {
</p>
<button
type="button"
className={`shrink-0 rounded border px-2 py-1 text-[12px] font-semibold ${
className={`shrink-0 border px-2 py-1 text-[12px] font-semibold ${
anv.aktiv === false
? "border-[#1E6B34] bg-white text-[#1E6B34]"
: "border-[#6E1414] bg-white text-[#8B1A1A]"
@@ -236,7 +236,7 @@ function AnvandarAdmin() {
key={valbar}
type="button"
onClick={() => setRoll(valbar)}
className={`min-h-9 rounded border text-[12px] font-semibold ${
className={`min-h-9 border text-[12px] font-semibold ${
roll === valbar
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
@@ -312,12 +312,12 @@ export default function Arendelista() {
</Link>
{alla.length > 0 && (
<div className="mb-4 grid grid-cols-3 gap-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-1">
<div className="mb-4 grid grid-cols-3 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1">
{FILTER.map((f) => (
<button
key={f.id}
onClick={() => setFilter(f.id)}
className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
className={`min-h-9 text-[13px] font-semibold transition-colors ${
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
}`}
>
@@ -369,13 +369,13 @@ export default function Arendelista() {
const b = brief(arende, metodikForArende(arende), new Date().toISOString());
return (
<Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block">
<div className="mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
<div className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
<div className="flex items-center justify-between gap-2">
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
Ärende #{arende.nummer}
</span>
<span
className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`}
>
@@ -41,7 +41,7 @@ function Vallista({
<button
key={val}
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
className={`min-h-10 rounded border px-2 font-semibold transition-colors ${
className={`min-h-10 border px-2 font-semibold transition-colors ${
aktiv
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
@@ -90,7 +90,7 @@ function Integrationer() {
plattformen och används bara av servern de skickas aldrig till teknikernas enheter.
</p>
{!krypteringKlar && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
Kryptering är inte konfigurerad i driften (INTEGRATION_NYCKEL) uppgifter kan inte sparas förrän
nyckeln finns.
</p>
@@ -119,7 +119,7 @@ function Integrationer() {
</div>
{oppen === lev.id ? (
<div className="mt-2 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mt-2 border border-[#C6C6C6] bg-white p-2">
{lev.falt.map((falt) => (
<TextFalt
key={falt.nyckel}
@@ -216,7 +216,7 @@ export default function NyttArende() {
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
>
{tolkning.demo && (
<p className="mb-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
<p className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
Demo-tolkning plattformens dokumenttolkning kräver inloggning. Värdena nedan är exempeldata.
</p>
)}
@@ -273,7 +273,7 @@ export default function NyttArende() {
/>
{skannarKod ? (
<div>
<video ref={videoRef} playsInline muted className="mb-2 w-full rounded border border-[#C6C6C6]" />
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
<p className="mb-2 text-[12px] text-[#707070]">
Rikta kameran mot QR-koden, streckkoden eller VIN-etiketten.
</p>
@@ -405,10 +405,10 @@ function GranskaTolkning({
<div className="lg:grid lg:grid-cols-2 lg:items-start lg:gap-3">
<Panel rubrik="Skannad arbetsorder">
<div className="relative">
<img src={foto} alt="Skannad arbetsorder" className="w-full rounded border border-[#C6C6C6]" />
<img src={foto} alt="Skannad arbetsorder" className="w-full border border-[#C6C6C6]" />
{vald?.omrade && (
<div
className="pointer-events-none absolute rounded-sm border-2 border-[#00437A] bg-[#00437A]/15"
className="pointer-events-none absolute border-2 border-[#00437A] bg-[#00437A]/15"
style={{
left: `${vald.omrade.x * 100}%`,
top: `${vald.omrade.y * 100}%`,
@@ -437,7 +437,7 @@ function GranskaTolkning({
<div
key={rad.id}
onClick={() => setMarkerad(rad.id)}
className={`mb-1.5 cursor-pointer rounded border p-2 ${
className={`mb-1.5 cursor-pointer border p-2 ${
markerad === rad.id ? "border-[#00437A]" : "border-transparent"
} ${niva === "gul" ? "bg-[#FFF8E1]" : niva === "rod" ? "bg-[#FBE9E9]" : "bg-white"}`}
>
@@ -457,7 +457,7 @@ function GranskaTolkning({
<input
value={rad.varde}
onChange={(e) => uppdatera(rad.id, { varde: e.target.value, bekraftad: true })}
className="w-full rounded border border-[#ADADAD] bg-white px-2 py-1 text-[13px] focus:border-[#00437A] focus:outline-none"
className="w-full border border-[#ADADAD] bg-white px-2 py-1 text-[13px] focus:border-[#00437A] focus:outline-none"
/>
{niva === "rod" && !rad.bekraftad && (
<button
@@ -465,7 +465,7 @@ function GranskaTolkning({
e.stopPropagation();
uppdatera(rad.id, { bekraftad: true });
}}
className="whitespace-nowrap rounded border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
>
Bekräfta
</button>
@@ -45,13 +45,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
);
}
return (
<div className="col-span-2 rounded border border-[#C6C6C6] bg-white p-2">
<div className="col-span-2 border border-[#C6C6C6] bg-white p-2">
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">Ny ansvarig tekniker</p>
<div className="grid grid-cols-2 gap-2">
{personer.map((person) => (
<button
key={person.id}
className="min-h-9 rounded border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]"
className="min-h-9 border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]"
onClick={async () => {
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
typ: "ansvarig_satt",
@@ -69,7 +69,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
</button>
))}
<button
className="min-h-9 rounded border border-[#ADADAD] font-semibold text-[#4A5560]"
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]"
onClick={() => setOppen(false)}
>
Avbryt
@@ -188,7 +188,7 @@ export default function Oversikt() {
<div key={rad.orsak} className="flex items-center gap-2 py-0.5 text-[13px]">
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span>
<span
className="h-3 rounded-sm bg-[#00437A]"
className="h-3 bg-[#00437A]"
style={{ width: `${Math.min(100, (rad.antal / felorsaker[0].antal) * 100)}%`, minWidth: 6 }}
/>
<span className="font-semibold">{rad.antal}</span>
@@ -203,13 +203,13 @@ export default function Oversikt() {
{rader.length === 0 && <p className="text-center text-[14px] text-[#4A5560]">Inga ärenden i organisationen ännu.</p>}
{rader.map((rad) => (
<div key={rad.id} className="mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-4">
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
<div className="flex items-center justify-between gap-2">
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
Ärende #{rad.nummer}
</span>
<span
className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`}
>