CI (SIAX Cloud) / integration (push) Skipped
masterplan-lock / check (push) Successful in 7s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / security (push) Successful in 47s
masterplan-lock / check (pull_request) Successful in 10s
CI (SIAX Cloud) / quality (push) Successful in 1m11s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 32s
CI (SIAX Cloud) / security (pull_request) Successful in 42s
CI (SIAX Cloud) / integration (pull_request) Successful in 56s
CI (SIAX Cloud) / quality (pull_request) Successful in 59s
axe-core 4.10.2 mot 0pt.app 2026-09-26: en enda regel, color-contrast, impact=serious, 82 noder. Estatets största enskilda a11y-defekt. Alla 82 har samma form: en Tailwind-alfamodifierare på en textfärg. Uppmätt WCAG 2.2-kontrast mot bägge temabakgrunderna i styles/globals.css: text-muted-foreground 4.73:1 ljust 7.63:1 mörkt GODKÄNT text-muted-foreground/70 2.71:1 4.20:1 faller text-muted-foreground/60 2.29:1 3.37:1 faller text-muted-foreground/50 1.96:1 2.67:1 faller text-muted-foreground/40 1.69:1 2.11:1 faller text-foreground/20 1.57:1 1.74:1 faller Den omodifierade token klarar AA i bägge teman, så åtgärden är helt enkelt att aldrig tona den. Elva textställen bytte till text-muted-foreground. Kopieringsknappen i app/portal går från 1.57:1 till 4.73:1 i viloläge och 19.79:1 vid hover — den föll även 1.4.11 för icke-textkomponenter. Toastens stängknapp klarade redan 1.4.11 (3.71:1) men blir 4.73:1. Kvar med alfa är endast den dekorativa masthead-siffran i app/bestall, som redan bär aria-hidden + select-none + pointer-events-none. Den är undantagen både av WCAG och av axe, och guarden nedan tillåter den på just den grunden. scripts/ci/check-text-contrast.mjs är en blockerande källkodsgrind i quality-jobbet. Den är avsiktligt inte en axe-körning: den fullständiga axe-jobb som UI-09 också begär kräver build + serve i CI och spåras separat. Tills det finns stoppar den här grinden regressionen vid den form som orsakade defekten. Refs: UI-09 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
7.7 KiB
TypeScript
123 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useState } from "react";
|
|
import { useReveal } from "@/hooks/use-reveal";
|
|
import { useT } from "@/components/locale-provider";
|
|
import { ProvenanceBadge } from "@/components/provenance-badge";
|
|
import type { ProvLevel } from "@/lib/data";
|
|
|
|
const FILTERS = ["Alla", "Bolag", "Beslutsfattare", "Fordon", "Finansiellt"] as const;
|
|
type Filter = (typeof FILTERS)[number];
|
|
|
|
const FILTER_LABELS: Record<Filter, { en: string; sv: string }> = {
|
|
Alla: { en: "All", sv: "Alla" },
|
|
Bolag: { en: "Companies", sv: "Bolag" },
|
|
Beslutsfattare: { en: "Decision-makers", sv: "Beslutsfattare" },
|
|
Fordon: { en: "Vehicles", sv: "Fordon" },
|
|
Finansiellt: { en: "Financial", sv: "Finansiellt" },
|
|
};
|
|
|
|
type Localized = { en: string; sv: string };
|
|
|
|
interface Dataset {
|
|
id: string;
|
|
markets: string;
|
|
title: Localized;
|
|
desc: Localized;
|
|
count: Localized;
|
|
freshness: Localized;
|
|
level: ProvLevel;
|
|
source: Localized;
|
|
cat: Exclude<Filter, "Alla">;
|
|
}
|
|
|
|
const DATASETS: Dataset[] = [
|
|
{ id: "nordic-companies", markets: "SE · NO · DK · FI", title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, desc: { en: "Official registers. Per field.", sv: "Officiella register. Per fält." }, count: { en: "4.9M records", sv: "4,9M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, cat: "Bolag" },
|
|
{ id: "tech-decisionmakers", markets: "SE · NO · DK · FI", title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, desc: { en: "Role and path. Source per field.", sv: "Roll och väg. Källa per fält." }, count: { en: "182k records", sv: "182k poster" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "public web", sv: "publik webb" }, cat: "Beslutsfattare"},
|
|
{ id: "vehicles-fleet", markets: "SE", title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, desc: { en: "Technical data. The VehIQ synergy.", sv: "Tekniska data. VehIQ-synergin." }, count: { en: "6.1M records", sv: "6,1M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Fordon" },
|
|
{ id: "financial-series", markets: "SE · NO · DK · FI", title: { en: "Financial series", sv: "Finansiella serier" }, desc: { en: "Revenue, profit, key figures.", sv: "Omsättning, resultat, nyckeltal."}, count: { en: "12 years", sv: "12 år" }, freshness: { en: "annual", sv: "årlig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Finansiellt" },
|
|
{ id: "buying-signals", markets: "Norden", title: { en: "Buying signals", sv: "Köpsignaler" }, desc: { en: "Ads, news, technology changes.", sv: "Annonser, nyheter, teknikbyten." }, count: { en: "continuous", sv: "löpande" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "web", sv: "webb" }, cat: "Bolag" },
|
|
{ id: "sanctions-pep", markets: "EU", title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" }, desc: { en: "Official lists. Screening.", sv: "Officiella listor. Screening." }, count: { en: "global", sv: "global" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "OpenSanctions", sv: "OpenSanctions" }, cat: "Bolag" },
|
|
];
|
|
|
|
export function FeaturedProductsSection() {
|
|
const ref = useReveal<HTMLElement>();
|
|
const t = useT();
|
|
const [filter, setFilter] = useState<Filter>("Alla");
|
|
const shown = DATASETS.filter((d) => filter === "Alla" || d.cat === filter);
|
|
|
|
return (
|
|
<section ref={ref} id="katalog" className="relative overflow-hidden border-b border-border bg-background">
|
|
{/* oversized faint section numeral — graphic texture */}
|
|
<span
|
|
aria-hidden
|
|
className="pointer-events-none absolute -right-4 top-8 select-none font-mono text-[18rem] leading-none tracking-[-0.05em] text-foreground/[0.025] sm:text-[24rem]"
|
|
>
|
|
02
|
|
</span>
|
|
|
|
<div className="relative mx-auto max-w-[80rem] px-6 py-28 lg:py-40">
|
|
<div className="reveal flex flex-wrap items-end justify-between gap-10 border-b border-border pb-10">
|
|
<div className="max-w-2xl">
|
|
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground">{t({ en: "02 / Catalog", sv: "02 / Katalog" })}</p>
|
|
<h2 className="mt-5 text-[clamp(2.2rem,5vw,4rem)] font-medium leading-[0.98] tracking-[-0.03em]">
|
|
{t({ en: "Ready-made datasets.", sv: "Färdiga dataset." })}
|
|
<br />
|
|
<span className="text-muted-foreground">{t({ en: "Ready to ship.", sv: "Redo att leverera." })}</span>
|
|
</h2>
|
|
</div>
|
|
<div role="group" aria-label={t({ en: "Filter by type", sv: "Filtrera efter typ" })} className="flex flex-wrap gap-px bg-border">
|
|
{FILTERS.map((f) => (
|
|
<button
|
|
key={f}
|
|
aria-pressed={filter === f}
|
|
onClick={() => setFilter(f)}
|
|
className="bg-background px-4 py-2 font-mono text-[11px] uppercase tracking-[0.1em] transition-colors"
|
|
style={{
|
|
backgroundColor: filter === f ? "var(--foreground)" : "var(--background)",
|
|
color: filter === f ? "var(--background)" : "var(--foreground)",
|
|
}}
|
|
>
|
|
{t(FILTER_LABELS[f])}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-px grid grid-cols-1 gap-px border-x border-b border-border bg-border md:grid-cols-3">
|
|
{shown.map((d, i) => (
|
|
<div key={d.title.sv} className={`reveal-scale delay-${(i % 6) + 1} group relative flex flex-col bg-background p-7 transition-colors hover:bg-secondary/40`}>
|
|
<span className="absolute left-0 top-0 h-px w-0 transition-all duration-300 group-hover:w-full" style={{ backgroundColor: "var(--accent-red)" }} />
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">{d.markets}</p>
|
|
<span className="font-mono text-[10px] tabular-nums text-muted-foreground">{String(i + 1).padStart(2, "0")}</span>
|
|
</div>
|
|
<h3 className="mt-5 text-xl font-semibold tracking-[-0.01em] transition-transform duration-300 group-hover:translate-x-1">{t(d.title)}</h3>
|
|
<p className="mt-2 text-sm text-muted-foreground">{t(d.desc)}</p>
|
|
|
|
<div className="mt-6 flex items-center gap-3 border-t border-border pt-4 font-mono text-[11px] text-muted-foreground">
|
|
<span className="text-foreground">{t(d.count)}</span>
|
|
<span className="text-border">·</span>
|
|
<span>{t(d.freshness)}</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<ProvenanceBadge level={d.level} source={t(d.source)} freshness={t(d.freshness)} />
|
|
</div>
|
|
|
|
<div className="mt-auto flex items-center justify-between pt-7">
|
|
<Link href={`/katalog/${d.id}`} className="font-mono text-[11px] uppercase tracking-[0.1em] underline-offset-4 hover:underline">
|
|
{t({ en: "View →", sv: "Visa →" })}
|
|
</Link>
|
|
<Link href="/bestall" className="border border-foreground px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.1em] transition-colors hover:bg-foreground hover:text-background">
|
|
{t({ en: "Order", sv: "Beställ" })}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|