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:
Generated
+20
@@ -8,6 +8,8 @@
|
||||
"name": "guidad-felsokning",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
"@fontsource/ibm-plex-sans": "^5.3.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
@@ -781,6 +783,24 @@
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource/ibm-plex-mono": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
|
||||
"integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/ibm-plex-sans": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz",
|
||||
"integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"genomgang": "node e2e/genomgang.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
"@fontsource/ibm-plex-sans": "^5.3.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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?:/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 på {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"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,18 @@ export default defineConfig(({ mode }) => ({
|
||||
},
|
||||
},
|
||||
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
||||
build: {
|
||||
// Typsnitten måste ligga i CSS:en som data, inte som separata filer.
|
||||
//
|
||||
// ALVA publiceras bland annat som en självbärande sida bakom en
|
||||
// strikt CSP där varje extern begäran blockeras. En @font-face som
|
||||
// pekar på en assetfil skulle då tyst falla tillbaka till enhetens
|
||||
// eget snitt — vilket är precis det inbäddningen ska avskaffa.
|
||||
//
|
||||
// Bara woff2 bakas in. Bilderna i värdapplikationen är megabyte
|
||||
// stora och ska fortsatt vara egna filer.
|
||||
assetsInlineLimit: (filePath: string) => (filePath.endsWith(".woff2") ? true : undefined),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
Reference in New Issue
Block a user