Files
0pt/components/states.tsx
T
admin cb018d9898
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
fix(types): states.tsx — styled-jsx-attribut bort (Next 16-typkedjan saknar jsx-prop); klasserna är namespaced, semantik oförändrad
Rensar pre-existing typecheck-röd på main (blocks CI quality-jobbet).
2026-09-30 17:10:51 +02:00

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>
);
}