CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (pull_request) Successful in 6s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (push) Successful in 26s
masterplan-lock / check (push) Successful in 26s
CI (SIAX Cloud) / security (push) Successful in 54s
CI (SIAX Cloud) / quality (push) Successful in 1m15s
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / integration (pull_request) Successful in 56s
CI (SIAX Cloud) / security (pull_request) Successful in 1m35s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m14s
Rensar pre-existing typecheck-röd på main (blocks CI quality-jobbet).
175 lines
5.9 KiB
TypeScript
175 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { DataArtifact } from "@/components/data-artifact";
|
|
import { useT } from "@/components/locale-provider";
|
|
|
|
/**
|
|
* Shared loading / empty / error states for the 0pt portal.
|
|
* Mono, square, radius 0. One red accent per screen, used at most once
|
|
* (the single EmptyState CTA). No fetching here — pure presentation.
|
|
*
|
|
* LoadingSkeleton: hairline rows with a slow left-to-right sweep (not pulsing
|
|
* blobs). EmptyState: centered, generous air, faint DataArtifact "grid" motif.
|
|
* ErrorState: terse interface voice + "Försök igen".
|
|
*/
|
|
|
|
/** Hairline shimmer rows — a slow sweep travels left→right across each bar. */
|
|
export function LoadingSkeleton({ rows = 5 }: { rows?: number }) {
|
|
const t = useT();
|
|
return (
|
|
<div
|
|
className="w-full border border-border bg-background"
|
|
role="status"
|
|
aria-busy="true"
|
|
aria-label={t({ en: "Loading", sv: "Laddar" })}
|
|
>
|
|
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
|
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
{t({ en: "loading", sv: "laddar" })}
|
|
</span>
|
|
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-border">
|
|
·
|
|
</span>
|
|
</div>
|
|
{Array.from({ length: Math.max(1, rows) }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center gap-4 border-b border-border px-4 py-3.5 last:border-b-0"
|
|
>
|
|
<span className="opt-shimmer h-2.5 w-2.5 shrink-0" />
|
|
<span
|
|
className="opt-shimmer h-3"
|
|
style={{ width: `${68 - (i % 3) * 14}%` }}
|
|
/>
|
|
<span className="opt-shimmer ml-auto h-3 w-16 shrink-0" />
|
|
</div>
|
|
))}
|
|
<span className="sr-only">{t({ en: "Loading content", sv: "Laddar innehåll" })}</span>
|
|
|
|
<style>{`
|
|
.opt-shimmer {
|
|
position: relative;
|
|
overflow: hidden;
|
|
background-color: var(--secondary);
|
|
}
|
|
.opt-shimmer::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
transform: translateX(-100%);
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent 0%,
|
|
color-mix(in srgb, var(--foreground) 9%, transparent) 50%,
|
|
transparent 100%
|
|
);
|
|
animation: opt-sweep 2.1s ease-in-out infinite;
|
|
}
|
|
@keyframes opt-sweep {
|
|
100% {
|
|
transform: translateX(100%);
|
|
}
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.opt-shimmer::after {
|
|
animation: none;
|
|
}
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Nothing here yet — centered, lots of air, faint grid motif, one red CTA max. */
|
|
export function EmptyState({
|
|
title,
|
|
line,
|
|
ctaHref,
|
|
ctaLabel,
|
|
}: {
|
|
title: string;
|
|
line: string;
|
|
ctaHref?: string;
|
|
ctaLabel?: string;
|
|
}) {
|
|
const t = useT();
|
|
return (
|
|
<div className="flex w-full flex-col items-center justify-center border border-border bg-background px-6 py-20 text-center sm:py-28">
|
|
<div
|
|
aria-hidden
|
|
className="pointer-events-none relative mb-12 w-full max-w-xs opacity-40"
|
|
>
|
|
<DataArtifact variant="grid" />
|
|
</div>
|
|
|
|
<p className="font-mono text-[10px] uppercase tracking-[0.22em] text-muted-foreground">
|
|
{t({ en: "empty", sv: "tomt" })}
|
|
</p>
|
|
<h2 className="mt-4 max-w-md text-balance text-3xl font-semibold leading-[1.04] tracking-tight sm:text-4xl">
|
|
{title}
|
|
</h2>
|
|
<p className="mt-4 max-w-sm text-pretty text-[15px] leading-relaxed text-muted-foreground">
|
|
{line}
|
|
</p>
|
|
|
|
{ctaHref && ctaLabel ? (
|
|
<a
|
|
href={ctaHref}
|
|
className="mt-9 inline-flex items-center gap-2 border px-5 py-2.5 font-mono text-[11px] uppercase tracking-[0.12em] text-background transition-opacity duration-200 hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
style={{
|
|
backgroundColor: "var(--accent-red)",
|
|
borderColor: "var(--accent-red)",
|
|
}}
|
|
>
|
|
{ctaLabel}
|
|
<span aria-hidden>→</span>
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Something broke — interface voice, single retry. */
|
|
export function ErrorState({
|
|
title,
|
|
onRetry,
|
|
}: {
|
|
title?: string;
|
|
onRetry?: () => void;
|
|
}) {
|
|
const t = useT();
|
|
const resolvedTitle = title ?? t({ en: "Something went wrong", sv: "Något gick fel" });
|
|
return (
|
|
<div
|
|
role="alert"
|
|
className="flex w-full flex-col items-center justify-center border border-border bg-background px-6 py-20 text-center sm:py-28"
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className="mb-8 inline-block h-3 w-3"
|
|
style={{ backgroundColor: "var(--accent-red)" }}
|
|
/>
|
|
|
|
<p className="font-mono text-[10px] uppercase tracking-[0.22em] text-muted-foreground">
|
|
{t({ en: "error", sv: "fel" })}
|
|
</p>
|
|
<h2 className="mt-4 max-w-md text-balance text-3xl font-semibold leading-[1.04] tracking-tight sm:text-4xl">
|
|
{resolvedTitle}
|
|
</h2>
|
|
<p className="mt-4 max-w-sm text-pretty text-[15px] leading-relaxed text-muted-foreground">
|
|
{t({ en: "Couldn't fetch data. No changes were saved.", sv: "Kunde inte hämta data. Ingen ändring sparad." })}
|
|
</p>
|
|
|
|
{onRetry ? (
|
|
<button
|
|
type="button"
|
|
onClick={onRetry}
|
|
className="mt-9 inline-flex items-center gap-2 border border-foreground bg-background px-5 py-2.5 font-mono text-[11px] uppercase tracking-[0.12em] text-foreground transition-colors duration-200 hover:bg-foreground hover:text-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
>
|
|
{t({ en: "Try again", sv: "Försök igen" })}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|