@xiwebdesign/xicms-core 7.16.2 → 7.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,8 @@ import { formatCents } from "../../schema/shop.js";
20
20
  import { Badge, Button, Card, Input } from "../ui/primitives.js";
21
21
  import { useToast } from "../ui/toast.js";
22
22
  import { useAdminNav } from "../navContext.js";
23
+ import { BalkjesLijst, DagStaven } from "../ui/charts.js";
24
+ import { dagenInMaand, dagReeks, deltaTovVorige, huidigMaandId, maandLabel, topPaden } from "../stats/lib.js";
23
25
  function updatedMs(v) {
24
26
  if (v && typeof v.toDate === "function") return v.toDate().getTime();
25
27
  if (typeof v === "string") {
@@ -60,6 +62,7 @@ function DashboardPanel({ siteId }) {
60
62
  const magInstellen = canManageSite(role);
61
63
  const [paginas, setPaginas] = useState(null);
62
64
  const [maand, setMaand] = useState(void 0);
65
+ const [vorigeMaand, setVorigeMaand] = useState(null);
63
66
  const [orders, setOrders] = useState(null);
64
67
  const [formulieren, setFormulieren] = useState(null);
65
68
  const [links, setLinks] = useState([]);
@@ -79,9 +82,12 @@ function DashboardPanel({ siteId }) {
79
82
  snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt)).slice(0, 5)
80
83
  )
81
84
  ).catch(() => setPaginas([]));
82
- const nu = /* @__PURE__ */ new Date();
83
- const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, "0")}`;
85
+ const maandId = huidigMaandId();
86
+ const [j, m] = maandId.split("-").map(Number);
87
+ const vd = new Date(Date.UTC(j, (m ?? 1) - 2, 1));
88
+ const vorigeId = `${vd.getUTCFullYear()}-${String(vd.getUTCMonth() + 1).padStart(2, "0")}`;
84
89
  getDoc(doc(db, "sites", siteId, "stats", maandId)).then((snap) => setMaand(snap.exists() ? snap.data() : null)).catch(() => setMaand(null));
90
+ getDoc(doc(db, "sites", siteId, "stats", vorigeId)).then((snap) => setVorigeMaand(snap.exists() ? snap.data() : null)).catch(() => setVorigeMaand(null));
85
91
  if (heeftShop) {
86
92
  getDocs(query(collection(db, "sites", siteId, "orders"), orderBy("createdAt", "desc"), limit(5))).then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...d.data() })))).catch(() => setOrders([]));
87
93
  } else {
@@ -106,14 +112,13 @@ function DashboardPanel({ siteId }) {
106
112
  setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);
107
113
  }).catch(() => setLinks([]));
108
114
  }, [siteId, heeftShop]);
109
- const vandaag = useMemo(() => {
110
- const dag = String((/* @__PURE__ */ new Date()).getUTCDate()).padStart(2, "0");
111
- return maand?.days?.[dag] ?? 0;
112
- }, [maand]);
113
- const topPaden = useMemo(
114
- () => Object.entries(maand?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 3),
115
- [maand]
115
+ const vandaagDag = (/* @__PURE__ */ new Date()).getUTCDate();
116
+ const dagen = useMemo(() => dagReeks(maand ? { id: "", ...maand } : void 0, dagenInMaand(huidigMaandId())), [maand]);
117
+ const delta = useMemo(
118
+ () => maand && vorigeMaand ? deltaTovVorige({ id: "", ...maand }, { id: "", ...vorigeMaand }, vandaagDag) : null,
119
+ [maand, vorigeMaand, vandaagDag]
116
120
  );
121
+ const paden = useMemo(() => topPaden(maand ? { id: "", ...maand } : void 0, 3), [maand]);
117
122
  async function slaLinksOp() {
118
123
  if (!linksBewerken) return;
119
124
  const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());
@@ -169,23 +174,39 @@ function DashboardPanel({ siteId }) {
169
174
  /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
170
175
  /* @__PURE__ */ jsxs(Card, { children: [
171
176
  /* @__PURE__ */ jsx(WidgetKop, { titel: "Bezoek deze maand", tab: "stats", tabLabel: "Statistieken" }),
172
- maand === void 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) : maand === null ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen bezoekersdata deze maand." }) : /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
173
- /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-4", children: [
177
+ maand === void 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) : maand === null ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen bezoekersdata deze maand." }) : /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
178
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-5 gap-y-1", children: [
174
179
  /* @__PURE__ */ jsxs("p", { children: [
175
- /* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold text-slate-900", children: maand.total ?? 0 }),
180
+ /* @__PURE__ */ jsx("span", { className: "text-3xl font-semibold text-slate-900", children: (maand.total ?? 0).toLocaleString("nl-NL") }),
176
181
  " ",
177
- /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "weergaven deze maand" })
182
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-slate-500", children: [
183
+ "weergaven in ",
184
+ maandLabel(huidigMaandId())
185
+ ] })
178
186
  ] }),
179
187
  /* @__PURE__ */ jsxs("p", { children: [
180
- /* @__PURE__ */ jsx("span", { className: "text-lg font-semibold text-slate-700", children: vandaag }),
188
+ /* @__PURE__ */ jsx("span", { className: "text-lg font-semibold text-slate-700", children: (dagen[vandaagDag - 1] ?? 0).toLocaleString("nl-NL") }),
181
189
  " ",
182
190
  /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "vandaag" })
191
+ ] }),
192
+ delta != null && Number.isFinite(delta) && /* @__PURE__ */ jsxs("p", { className: `text-xs font-medium ${delta >= 0 ? "text-emerald-700" : "text-red-700"}`, children: [
193
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: delta >= 0 ? "\u25B2" : "\u25BC" }),
194
+ " ",
195
+ Math.abs(Math.round(delta)),
196
+ "%",
197
+ /* @__PURE__ */ jsx("span", { className: "font-normal text-slate-500", children: " t.o.v. vorige maand" })
183
198
  ] })
184
199
  ] }),
185
- topPaden.length > 0 && /* @__PURE__ */ jsx("ul", { className: "grid gap-1 text-sm text-slate-600", children: topPaden.map(([pad, n]) => /* @__PURE__ */ jsxs("li", { className: "flex justify-between gap-2", children: [
186
- /* @__PURE__ */ jsx("span", { className: "truncate", title: pad, children: pad }),
187
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-slate-400", children: n })
188
- ] }, pad)) })
200
+ /* @__PURE__ */ jsx(
201
+ DagStaven,
202
+ {
203
+ waarden: dagen,
204
+ accentIndex: vandaagDag - 1,
205
+ hoogte: 110,
206
+ titel: `Weergaven per dag in ${maandLabel(huidigMaandId(), true)}`
207
+ }
208
+ ),
209
+ paden.length > 0 && /* @__PURE__ */ jsx(BalkjesLijst, { items: paden })
189
210
  ] })
190
211
  ] }),
191
212
  /* @__PURE__ */ jsxs(Card, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n /** Timestamp uit de editor, ISO-string uit import-scripts — zie updatedMs(). */\n updatedAt?: unknown;\n}\ninterface MaandStat {\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\n/**\n * `updatedAt` is in de praktijk een Firestore-Timestamp (editor-saves) ÓF een ISO-string\n * (import-/migratiescripts schrijven via REST) — live aangetroffen op christelijkkraamcadeau,\n * dóór elkaar in dezelfde collectie. Firestore sorteert die in aparte typegroepen, dus een\n * server-side orderBy geeft dan een fout beeld; we normaliseren en sorteren daarom client-side.\n */\nfunction updatedMs(v: unknown): number {\n if (v && typeof (v as Timestamp).toDate === \"function\") return (v as Timestamp).toDate().getTime();\n if (typeof v === \"string\") {\n const t = Date.parse(v);\n return Number.isNaN(t) ? 0 : t;\n }\n return 0;\n}\n\nfunction fmtDatum(v: unknown): string {\n const ms = updatedMs(v);\n if (!ms) return \"—\";\n return new Date(ms).toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. Bewust GEEN server-side orderBy op updatedAt: dat veld is een\n // mengsel van Timestamps en ISO-strings (zie updatedMs) en Firestore sorteert per typegroep.\n // Drafts-collecties zijn klein; alles ophalen en zelf sorteren is hier de betrouwbare weg.\n getDocs(collection(db, \"sites\", siteId, \"drafts\"))\n .then((snap) =>\n setPaginas(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\">) }))\n .sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt))\n .slice(0, 5),\n ),\n )\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand (zelfde bron als het Statistieken-paneel).\n const nu = new Date();\n const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const [count, laatste] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaag = useMemo(() => {\n const dag = String(new Date().getUTCDate()).padStart(2, \"0\");\n return maand?.days?.[dag] ?? 0;\n }, [maand]);\n const topPaden = useMemo(\n () => Object.entries(maand?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 3),\n [maand],\n );\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina&apos;s, het\n menu, foto&apos;s, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-3\">\n <div className=\"flex items-baseline gap-4\">\n <p><span className=\"text-2xl font-semibold text-slate-900\">{maand.total ?? 0}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven deze maand</span></p>\n <p><span className=\"text-lg font-semibold text-slate-700\">{vandaag}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span></p>\n </div>\n {topPaden.length > 0 && (\n <ul className=\"grid gap-1 text-sm text-slate-600\">\n {topPaden.map(([pad, n]) => (\n <li key={pad} className=\"flex justify-between gap-2\">\n <span className=\"truncate\" title={pad}>{pad}</span>\n <span className=\"shrink-0 text-slate-400\">{n}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina&apos;s.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\nimport { BalkjesLijst, DagStaven } from \"../ui/charts\";\nimport { dagenInMaand, dagReeks, deltaTovVorige, huidigMaandId, maandLabel, topPaden, type MaandStat as StatsMaand } from \"../stats/lib\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n /** Timestamp uit de editor, ISO-string uit import-scripts — zie updatedMs(). */\n updatedAt?: unknown;\n}\ntype MaandStat = Omit<StatsMaand, \"id\">;\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\n/**\n * `updatedAt` is in de praktijk een Firestore-Timestamp (editor-saves) ÓF een ISO-string\n * (import-/migratiescripts schrijven via REST) — live aangetroffen op christelijkkraamcadeau,\n * dóór elkaar in dezelfde collectie. Firestore sorteert die in aparte typegroepen, dus een\n * server-side orderBy geeft dan een fout beeld; we normaliseren en sorteren daarom client-side.\n */\nfunction updatedMs(v: unknown): number {\n if (v && typeof (v as Timestamp).toDate === \"function\") return (v as Timestamp).toDate().getTime();\n if (typeof v === \"string\") {\n const t = Date.parse(v);\n return Number.isNaN(t) ? 0 : t;\n }\n return 0;\n}\n\nfunction fmtDatum(v: unknown): string {\n const ms = updatedMs(v);\n if (!ms) return \"—\";\n return new Date(ms).toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [vorigeMaand, setVorigeMaand] = useState<MaandStat | null>(null);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. Bewust GEEN server-side orderBy op updatedAt: dat veld is een\n // mengsel van Timestamps en ISO-strings (zie updatedMs) en Firestore sorteert per typegroep.\n // Drafts-collecties zijn klein; alles ophalen en zelf sorteren is hier de betrouwbare weg.\n getDocs(collection(db, \"sites\", siteId, \"drafts\"))\n .then((snap) =>\n setPaginas(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\">) }))\n .sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt))\n .slice(0, 5),\n ),\n )\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand + vorige maand (voor de eerlijke delta) — zelfde bron als Statistieken.\n const maandId = huidigMaandId();\n const [j, m] = maandId.split(\"-\").map(Number);\n const vd = new Date(Date.UTC(j, (m ?? 1) - 2, 1));\n const vorigeId = `${vd.getUTCFullYear()}-${String(vd.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n getDoc(doc(db, \"sites\", siteId, \"stats\", vorigeId))\n .then((snap) => setVorigeMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setVorigeMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const [count, laatste] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaagDag = new Date().getUTCDate();\n const dagen = useMemo(() => dagReeks(maand ? { id: \"\", ...maand } : undefined, dagenInMaand(huidigMaandId())), [maand]);\n const delta = useMemo(\n () =>\n maand && vorigeMaand\n ? deltaTovVorige({ id: \"\", ...maand }, { id: \"\", ...vorigeMaand }, vandaagDag)\n : null,\n [maand, vorigeMaand, vandaagDag],\n );\n const paden = useMemo(() => topPaden(maand ? { id: \"\", ...maand } : undefined, 3), [maand]);\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina&apos;s, het\n menu, foto&apos;s, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-4\">\n {/* Hero-cijfer met context: het totaal leidt, vandaag en de delta lopen mee. */}\n <div className=\"flex flex-wrap items-baseline gap-x-5 gap-y-1\">\n <p>\n <span className=\"text-3xl font-semibold text-slate-900\">{(maand.total ?? 0).toLocaleString(\"nl-NL\")}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven in {maandLabel(huidigMaandId())}</span>\n </p>\n <p>\n <span className=\"text-lg font-semibold text-slate-700\">{(dagen[vandaagDag - 1] ?? 0).toLocaleString(\"nl-NL\")}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span>\n </p>\n {delta != null && Number.isFinite(delta) && (\n <p className={`text-xs font-medium ${delta >= 0 ? \"text-emerald-700\" : \"text-red-700\"}`}>\n <span aria-hidden=\"true\">{delta >= 0 ? \"▲\" : \"▼\"}</span> {Math.abs(Math.round(delta))}%\n <span className=\"font-normal text-slate-500\"> t.o.v. vorige maand</span>\n </p>\n )}\n </div>\n <DagStaven\n waarden={dagen}\n accentIndex={vandaagDag - 1}\n hoogte={110}\n titel={`Weergaven per dag in ${maandLabel(huidigMaandId(), true)}`}\n />\n {paden.length > 0 && <BalkjesLijst items={paden} />}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina&apos;s.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,9 @@
1
- /** Eenvoudige, privacy-vriendelijke bezoekersstatistieken (eigen teller). */
1
+ /**
2
+ * Bezoekersstatistieken (7.17) — van drie kale tellers naar een leesbaar verhaal: tegels met
3
+ * context (delta t.o.v. vorige maand, t/m dezelfde dag), een dag-grafiek met hover, de
4
+ * maandtrend als lijn, en de toppagina's als balkjes. Zelfde privacy-vriendelijke bron
5
+ * (`sites/{siteId}/stats`, eigen teller, geen cookies) — er is alleen beter naar gekeken.
6
+ */
2
7
  export declare function StatsPanel({ siteId }: {
3
8
  siteId: string;
4
9
  }): import("react").JSX.Element;
@@ -1,12 +1,23 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import { collection, getDocs } from "firebase/firestore";
5
5
  import { getDb } from "../../firebase/client.js";
6
6
  import { Card } from "../ui/primitives.js";
7
+ import { Select } from "../ui/select.js";
8
+ import { AlsTabel, BalkjesLijst, DagStaven, MaandLijn, StatTegel } from "../ui/charts.js";
9
+ import {
10
+ dagenInMaand,
11
+ dagReeks,
12
+ deltaTovVorige,
13
+ huidigMaandId,
14
+ maandLabel,
15
+ topPaden
16
+ } from "./lib.js";
7
17
  function StatsPanel({ siteId }) {
8
18
  const [months, setMonths] = useState([]);
9
19
  const [loading, setLoading] = useState(true);
20
+ const [gekozen, setGekozen] = useState(huidigMaandId());
10
21
  useEffect(() => {
11
22
  const db = getDb();
12
23
  if (!db) {
@@ -16,60 +27,104 @@ function StatsPanel({ siteId }) {
16
27
  let active = true;
17
28
  getDocs(collection(db, "sites", siteId, "stats")).then((snap) => {
18
29
  if (!active) return;
19
- setMonths(snap.docs.map((d) => ({ id: d.id, ...d.data() })));
30
+ const uit = snap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((m) => /^\d{4}-\d{2}$/.test(m.id)).sort((a, b) => a.id.localeCompare(b.id));
31
+ setMonths(uit);
32
+ if (uit.length && !uit.some((m) => m.id === huidigMaandId())) setGekozen(uit[uit.length - 1].id);
20
33
  setLoading(false);
21
34
  }).catch(() => active && setLoading(false));
22
35
  return () => {
23
36
  active = false;
24
37
  };
25
38
  }, [siteId]);
26
- const total = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);
27
- const current = useMemo(() => {
28
- const now = /* @__PURE__ */ new Date();
29
- const id = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
30
- return months.find((m) => m.id === id);
31
- }, [months]);
32
- const days = current?.days ?? {};
33
- const maxDay = Math.max(1, ...Object.values(days));
34
- const topPaths = Object.entries(current?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 8);
39
+ const maand = months.find((m) => m.id === gekozen);
40
+ const vorigeId = useMemo(() => {
41
+ const [j, m] = gekozen.split("-").map(Number);
42
+ const d = new Date(Date.UTC(j, (m ?? 1) - 2, 1));
43
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
44
+ }, [gekozen]);
45
+ const vorige = months.find((m) => m.id === vorigeId);
46
+ const isHuidige = gekozen === huidigMaandId();
47
+ const nDagen = dagenInMaand(gekozen);
48
+ const vandaagDag = (/* @__PURE__ */ new Date()).getUTCDate();
49
+ const dagen = useMemo(() => dagReeks(maand, nDagen), [maand, nDagen]);
50
+ const totaalMaand = maand?.total ?? 0;
51
+ const totaalOoit = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);
52
+ const verstreken = isHuidige ? Math.max(1, vandaagDag) : nDagen;
53
+ const delta = deltaTovVorige(maand, vorige, isHuidige ? vandaagDag : nDagen);
54
+ const paden = useMemo(() => topPaden(maand, 8), [maand]);
55
+ const trend = useMemo(
56
+ () => months.slice(-12).map((m) => ({ label: maandLabel(m.id), waarde: m.total ?? 0 })),
57
+ [months]
58
+ );
35
59
  if (loading) return /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" });
36
60
  if (months.length === 0) {
37
61
  return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-600", children: "Nog geen bezoekersdata. Zodra de live site bezocht wordt, verschijnen hier de cijfers." }) });
38
62
  }
39
63
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
40
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-3", children: [
41
- /* @__PURE__ */ jsxs(Card, { children: [
42
- /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-500", children: "Totaal bezoeken" }),
43
- /* @__PURE__ */ jsx("div", { className: "text-3xl font-bold", children: total.toLocaleString("nl-NL") })
44
- ] }),
45
- /* @__PURE__ */ jsxs(Card, { children: [
46
- /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-500", children: "Deze maand" }),
47
- /* @__PURE__ */ jsx("div", { className: "text-3xl font-bold", children: (current?.total ?? 0).toLocaleString("nl-NL") })
64
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
65
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-500", children: [
66
+ "Bezoek in ",
67
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-700", children: maandLabel(gekozen, true) }),
68
+ isHuidige ? " (loopt nog)" : ""
48
69
  ] }),
49
- /* @__PURE__ */ jsxs(Card, { children: [
50
- /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-500", children: "Maanden gemeten" }),
51
- /* @__PURE__ */ jsx("div", { className: "text-3xl font-bold", children: months.length })
52
- ] })
70
+ /* @__PURE__ */ jsx(
71
+ Select,
72
+ {
73
+ value: gekozen,
74
+ onValueChange: setGekozen,
75
+ placeholder: "Maand",
76
+ className: "w-44",
77
+ options: [...months].reverse().map((m) => ({ value: m.id, label: maandLabel(m.id, true) }))
78
+ }
79
+ )
53
80
  ] }),
54
- /* @__PURE__ */ jsxs(Card, { children: [
55
- /* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Bezoeken per dag (deze maand)" }),
56
- Object.keys(days).length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data deze maand." }) : /* @__PURE__ */ jsx("div", { className: "flex items-end gap-1", style: { height: 140 }, children: Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, "0")).map((d) => /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col items-center justify-end", title: `Dag ${d}: ${days[d] ?? 0}`, children: /* @__PURE__ */ jsx(
57
- "div",
81
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: [
82
+ /* @__PURE__ */ jsx(
83
+ StatTegel,
58
84
  {
59
- className: "w-full rounded-t bg-brand-500",
60
- style: { height: `${(days[d] ?? 0) / maxDay * 120}px` }
85
+ label: "Weergaven deze maand",
86
+ waarde: totaalMaand,
87
+ delta,
88
+ deltaLabel: `t.o.v. ${maandLabel(vorigeId)}${isHuidige ? " (t/m vandaag)" : ""}`
61
89
  }
62
- ) }, d)) })
90
+ ),
91
+ isHuidige ? /* @__PURE__ */ jsx(StatTegel, { label: "Vandaag", waarde: dagen[vandaagDag - 1] ?? 0 }) : /* @__PURE__ */ jsx(StatTegel, { label: "Drukste dag", waarde: Math.max(0, ...dagen) }),
92
+ /* @__PURE__ */ jsx(StatTegel, { label: "Gemiddeld per dag", waarde: Math.round(totaalMaand / verstreken) }),
93
+ /* @__PURE__ */ jsx(StatTegel, { label: "Totaal gemeten", waarde: totaalOoit })
63
94
  ] }),
64
95
  /* @__PURE__ */ jsxs(Card, { children: [
65
- /* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Best bezochte pagina's (deze maand)" }),
66
- topPaths.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data." }) : /* @__PURE__ */ jsx("ul", { className: "text-sm", children: topPaths.map(([p, n]) => /* @__PURE__ */ jsxs("li", { className: "flex justify-between border-b border-slate-100 py-1 last:border-0", children: [
67
- /* @__PURE__ */ jsxs("span", { className: "truncate text-slate-700", children: [
68
- "/",
69
- p === "home" ? "" : p.replace(/_/g, "/")
70
- ] }),
71
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: n.toLocaleString("nl-NL") })
72
- ] }, p)) })
96
+ /* @__PURE__ */ jsx("h3", { className: "mb-4 text-sm font-semibold text-slate-800", children: "Per dag" }),
97
+ totaalMaand === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data in deze maand." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
98
+ /* @__PURE__ */ jsx(
99
+ DagStaven,
100
+ {
101
+ waarden: dagen,
102
+ accentIndex: isHuidige ? vandaagDag - 1 : void 0,
103
+ hoogte: 170,
104
+ titel: `Weergaven per dag in ${maandLabel(gekozen, true)}`
105
+ }
106
+ ),
107
+ /* @__PURE__ */ jsx(
108
+ AlsTabel,
109
+ {
110
+ kop: ["Dag", "Weergaven"],
111
+ rijen: dagen.map((v, i) => [`${i + 1} ${maandLabel(gekozen)}`, v]).filter(([, v]) => v > 0)
112
+ }
113
+ )
114
+ ] })
115
+ ] }),
116
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
117
+ /* @__PURE__ */ jsxs(Card, { children: [
118
+ /* @__PURE__ */ jsx("h3", { className: "mb-4 text-sm font-semibold text-slate-800", children: "Maand op maand" }),
119
+ trend.length < 2 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog te weinig maanden voor een trend \u2014 die groeit hier vanzelf." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
120
+ /* @__PURE__ */ jsx(MaandLijn, { punten: trend, hoogte: 170, titel: "Weergaven per maand, laatste twaalf maanden" }),
121
+ /* @__PURE__ */ jsx(AlsTabel, { kop: ["Maand", "Weergaven"], rijen: trend.map((p) => [p.label, p.waarde]) })
122
+ ] })
123
+ ] }),
124
+ /* @__PURE__ */ jsxs(Card, { children: [
125
+ /* @__PURE__ */ jsx("h3", { className: "mb-4 text-sm font-semibold text-slate-800", children: "Best bezochte pagina's" }),
126
+ paden.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data in deze maand." }) : /* @__PURE__ */ jsx(BalkjesLijst, { items: paden })
127
+ ] })
73
128
  ] }),
74
129
  /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: "Privacy-vriendelijk geteld (geen cookies, geen persoonsgegevens) \u2014 \xE9\xE9n telling per bezoeker-sessie per pagina." })
75
130
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/stats/StatsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { Card } from \"../ui/primitives\";\n\ninterface MonthStat {\n id: string;\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\n\n/** Eenvoudige, privacy-vriendelijke bezoekersstatistieken (eigen teller). */\nexport function StatsPanel({ siteId }: { siteId: string }) {\n const [months, setMonths] = useState<MonthStat[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoading(false);\n return;\n }\n let active = true;\n getDocs(collection(db, \"sites\", siteId, \"stats\"))\n .then((snap) => {\n if (!active) return;\n setMonths(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<MonthStat, \"id\">) })));\n setLoading(false);\n })\n .catch(() => active && setLoading(false));\n return () => {\n active = false;\n };\n }, [siteId]);\n\n const total = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);\n const current = useMemo(() => {\n const now = new Date();\n const id = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, \"0\")}`;\n return months.find((m) => m.id === id);\n }, [months]);\n\n const days = current?.days ?? {};\n const maxDay = Math.max(1, ...Object.values(days));\n const topPaths = Object.entries(current?.paths ?? {})\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8);\n\n if (loading) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n if (months.length === 0) {\n return (\n <Card>\n <p className=\"text-sm text-slate-600\">\n Nog geen bezoekersdata. Zodra de live site bezocht wordt, verschijnen hier de cijfers.\n </p>\n </Card>\n );\n }\n\n return (\n <div className=\"grid gap-4\">\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <Card>\n <div className=\"text-sm text-slate-500\">Totaal bezoeken</div>\n <div className=\"text-3xl font-bold\">{total.toLocaleString(\"nl-NL\")}</div>\n </Card>\n <Card>\n <div className=\"text-sm text-slate-500\">Deze maand</div>\n <div className=\"text-3xl font-bold\">{(current?.total ?? 0).toLocaleString(\"nl-NL\")}</div>\n </Card>\n <Card>\n <div className=\"text-sm text-slate-500\">Maanden gemeten</div>\n <div className=\"text-3xl font-bold\">{months.length}</div>\n </Card>\n </div>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Bezoeken per dag (deze maand)</h3>\n {Object.keys(days).length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data deze maand.</p>\n ) : (\n <div className=\"flex items-end gap-1\" style={{ height: 140 }}>\n {Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, \"0\")).map((d) => (\n <div key={d} className=\"flex flex-1 flex-col items-center justify-end\" title={`Dag ${d}: ${days[d] ?? 0}`}>\n <div\n className=\"w-full rounded-t bg-brand-500\"\n style={{ height: `${((days[d] ?? 0) / maxDay) * 120}px` }}\n />\n </div>\n ))}\n </div>\n )}\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Best bezochte pagina's (deze maand)</h3>\n {topPaths.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data.</p>\n ) : (\n <ul className=\"text-sm\">\n {topPaths.map(([p, n]) => (\n <li key={p} className=\"flex justify-between border-b border-slate-100 py-1 last:border-0\">\n <span className=\"truncate text-slate-700\">/{p === \"home\" ? \"\" : p.replace(/_/g, \"/\")}</span>\n <span className=\"font-medium\">{n.toLocaleString(\"nl-NL\")}</span>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n <p className=\"text-xs text-slate-400\">\n Privacy-vriendelijk geteld (geen cookies, geen persoonsgegevens) — één telling per\n bezoeker-sessie per pagina.\n </p>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/stats/StatsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { Card } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { AlsTabel, BalkjesLijst, DagStaven, MaandLijn, StatTegel } from \"../ui/charts\";\nimport {\n dagenInMaand,\n dagReeks,\n deltaTovVorige,\n huidigMaandId,\n maandLabel,\n topPaden,\n type MaandStat,\n} from \"./lib\";\n\n/**\n * Bezoekersstatistieken (7.17) — van drie kale tellers naar een leesbaar verhaal: tegels met\n * context (delta t.o.v. vorige maand, t/m dezelfde dag), een dag-grafiek met hover, de\n * maandtrend als lijn, en de toppagina's als balkjes. Zelfde privacy-vriendelijke bron\n * (`sites/{siteId}/stats`, eigen teller, geen cookies) — er is alleen beter naar gekeken.\n */\nexport function StatsPanel({ siteId }: { siteId: string }) {\n const [months, setMonths] = useState<MaandStat[]>([]);\n const [loading, setLoading] = useState(true);\n const [gekozen, setGekozen] = useState<string>(huidigMaandId());\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoading(false);\n return;\n }\n let active = true;\n getDocs(collection(db, \"sites\", siteId, \"stats\"))\n .then((snap) => {\n if (!active) return;\n const uit = snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<MaandStat, \"id\">) }))\n .filter((m) => /^\\d{4}-\\d{2}$/.test(m.id))\n .sort((a, b) => a.id.localeCompare(b.id));\n setMonths(uit);\n // Standaard de nieuwste maand mét data (meestal de huidige).\n if (uit.length && !uit.some((m) => m.id === huidigMaandId())) setGekozen(uit[uit.length - 1].id);\n setLoading(false);\n })\n .catch(() => active && setLoading(false));\n return () => {\n active = false;\n };\n }, [siteId]);\n\n const maand = months.find((m) => m.id === gekozen);\n const vorigeId = useMemo(() => {\n const [j, m] = gekozen.split(\"-\").map(Number);\n const d = new Date(Date.UTC(j, (m ?? 1) - 2, 1));\n return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, \"0\")}`;\n }, [gekozen]);\n const vorige = months.find((m) => m.id === vorigeId);\n\n const isHuidige = gekozen === huidigMaandId();\n const nDagen = dagenInMaand(gekozen);\n const vandaagDag = new Date().getUTCDate();\n const dagen = useMemo(() => dagReeks(maand, nDagen), [maand, nDagen]);\n const totaalMaand = maand?.total ?? 0;\n const totaalOoit = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);\n // Gemiddelde over de dagen die er al ZIJN: een lopende maand door 31 delen praat het cijfer omlaag.\n const verstreken = isHuidige ? Math.max(1, vandaagDag) : nDagen;\n const delta = deltaTovVorige(maand, vorige, isHuidige ? vandaagDag : nDagen);\n const paden = useMemo(() => topPaden(maand, 8), [maand]);\n const trend = useMemo(\n () => months.slice(-12).map((m) => ({ label: maandLabel(m.id), waarde: m.total ?? 0 })),\n [months],\n );\n\n if (loading) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n if (months.length === 0) {\n return (\n <Card>\n <p className=\"text-sm text-slate-600\">\n Nog geen bezoekersdata. Zodra de live site bezocht wordt, verschijnen hier de cijfers.\n </p>\n </Card>\n );\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* Eén filterregel boven de inhoud: de maand waar alles hieronder over gaat. */}\n <div className=\"flex items-center justify-between gap-3\">\n <p className=\"text-sm text-slate-500\">\n Bezoek in <span className=\"font-medium text-slate-700\">{maandLabel(gekozen, true)}</span>\n {isHuidige ? \" (loopt nog)\" : \"\"}\n </p>\n <Select\n value={gekozen}\n onValueChange={setGekozen}\n placeholder=\"Maand\"\n className=\"w-44\"\n options={[...months].reverse().map((m) => ({ value: m.id, label: maandLabel(m.id, true) }))}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <StatTegel\n label=\"Weergaven deze maand\"\n waarde={totaalMaand}\n delta={delta}\n deltaLabel={`t.o.v. ${maandLabel(vorigeId)}${isHuidige ? \" (t/m vandaag)\" : \"\"}`}\n />\n {isHuidige ? (\n <StatTegel label=\"Vandaag\" waarde={dagen[vandaagDag - 1] ?? 0} />\n ) : (\n <StatTegel label=\"Drukste dag\" waarde={Math.max(0, ...dagen)} />\n )}\n <StatTegel label=\"Gemiddeld per dag\" waarde={Math.round(totaalMaand / verstreken)} />\n <StatTegel label=\"Totaal gemeten\" waarde={totaalOoit} />\n </div>\n\n <Card>\n <h3 className=\"mb-4 text-sm font-semibold text-slate-800\">Per dag</h3>\n {totaalMaand === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data in deze maand.</p>\n ) : (\n <>\n <DagStaven\n waarden={dagen}\n accentIndex={isHuidige ? vandaagDag - 1 : undefined}\n hoogte={170}\n titel={`Weergaven per dag in ${maandLabel(gekozen, true)}`}\n />\n <AlsTabel\n kop={[\"Dag\", \"Weergaven\"]}\n rijen={dagen.map((v, i) => [`${i + 1} ${maandLabel(gekozen)}`, v] as [string, number]).filter(([, v]) => v > 0)}\n />\n </>\n )}\n </Card>\n\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-4 text-sm font-semibold text-slate-800\">Maand op maand</h3>\n {trend.length < 2 ? (\n <p className=\"text-sm text-slate-500\">Nog te weinig maanden voor een trend — die groeit hier vanzelf.</p>\n ) : (\n <>\n <MaandLijn punten={trend} hoogte={170} titel=\"Weergaven per maand, laatste twaalf maanden\" />\n <AlsTabel kop={[\"Maand\", \"Weergaven\"]} rijen={trend.map((p) => [p.label, p.waarde] as [string, number])} />\n </>\n )}\n </Card>\n\n <Card>\n <h3 className=\"mb-4 text-sm font-semibold text-slate-800\">Best bezochte pagina&apos;s</h3>\n {paden.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data in deze maand.</p>\n ) : (\n <BalkjesLijst items={paden} />\n )}\n </Card>\n </div>\n\n <p className=\"text-xs text-slate-400\">\n Privacy-vriendelijk geteld (geen cookies, geen persoonsgegevens) — één telling per\n bezoeker-sessie per pagina.\n </p>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Gedeelde stats-helpers — gebruikt door het Statistieken-paneel én de Start-widget, zodat
3
+ * paden en maanden op beide plekken hetzelfde heten.
4
+ */
5
+ export interface MaandStat {
6
+ id: string;
7
+ total?: number;
8
+ days?: Record<string, number>;
9
+ paths?: Record<string, number>;
10
+ }
11
+ /** trackView slaat paden veilig op: "home" voor "/" en "_" voor "/". Terug naar leesbaar. */
12
+ export declare function padLabel(sleutel: string): string;
13
+ /** "2026-09" → "sep 2026" (kort, voor as-labels en de maandkiezer). */
14
+ export declare function maandLabel(id: string, lang?: boolean): string;
15
+ /** Het maand-id van nu (UTC — trackView telt in UTC). */
16
+ export declare function huidigMaandId(): string;
17
+ /** Aantal dagen in een "YYYY-MM"-maand. */
18
+ export declare function dagenInMaand(id: string): number;
19
+ /** days-record → array per dag (index 0 = dag 1), met nullen voor ontbrekende dagen. */
20
+ export declare function dagReeks(stat: MaandStat | undefined, nDagen: number): number[];
21
+ /** Top-N paden van een maand, genormaliseerd en gesorteerd. */
22
+ export declare function topPaden(stat: MaandStat | undefined, n: number): {
23
+ label: string;
24
+ waarde: number;
25
+ }[];
26
+ /**
27
+ * Eerlijke maand-op-maand-delta: vergelijk t/m dezelfde dag. Halverwege de maand het hele
28
+ * vorige maandtotaal ernaast leggen leest als "−50%" terwijl het gewoon de 15e is.
29
+ */
30
+ export declare function deltaTovVorige(huidig: MaandStat | undefined, vorige: MaandStat | undefined, totDag: number): number | null;
@@ -0,0 +1,40 @@
1
+ function padLabel(sleutel) {
2
+ return sleutel === "home" ? "/" : `/${sleutel.replace(/_/g, "/")}`;
3
+ }
4
+ function maandLabel(id, lang = false) {
5
+ const [j, m] = id.split("-").map(Number);
6
+ const d = new Date(Date.UTC(j, (m ?? 1) - 1, 1));
7
+ return d.toLocaleDateString("nl-NL", { month: lang ? "long" : "short", year: "numeric", timeZone: "UTC" });
8
+ }
9
+ function huidigMaandId() {
10
+ const nu = /* @__PURE__ */ new Date();
11
+ return `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, "0")}`;
12
+ }
13
+ function dagenInMaand(id) {
14
+ const [j, m] = id.split("-").map(Number);
15
+ return new Date(Date.UTC(j, m ?? 1, 0)).getUTCDate();
16
+ }
17
+ function dagReeks(stat, nDagen) {
18
+ return Array.from({ length: nDagen }, (_, i) => stat?.days?.[String(i + 1).padStart(2, "0")] ?? 0);
19
+ }
20
+ function topPaden(stat, n) {
21
+ return Object.entries(stat?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, n).map(([p, w]) => ({ label: padLabel(p), waarde: w }));
22
+ }
23
+ function deltaTovVorige(huidig, vorige, totDag) {
24
+ if (!huidig || !vorige) return null;
25
+ const som = (s) => Object.entries(s.days ?? {}).reduce((acc, [d, n]) => Number(d) <= totDag ? acc + n : acc, 0);
26
+ const nu = som(huidig);
27
+ const toen = som(vorige);
28
+ if (toen === 0) return null;
29
+ return (nu - toen) / toen * 100;
30
+ }
31
+ export {
32
+ dagReeks,
33
+ dagenInMaand,
34
+ deltaTovVorige,
35
+ huidigMaandId,
36
+ maandLabel,
37
+ padLabel,
38
+ topPaden
39
+ };
40
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/stats/lib.ts"],"sourcesContent":["/**\n * Gedeelde stats-helpers — gebruikt door het Statistieken-paneel én de Start-widget, zodat\n * paden en maanden op beide plekken hetzelfde heten.\n */\n\nexport interface MaandStat {\n id: string; // \"YYYY-MM\"\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\n\n/** trackView slaat paden veilig op: \"home\" voor \"/\" en \"_\" voor \"/\". Terug naar leesbaar. */\nexport function padLabel(sleutel: string): string {\n return sleutel === \"home\" ? \"/\" : `/${sleutel.replace(/_/g, \"/\")}`;\n}\n\n/** \"2026-09\" → \"sep 2026\" (kort, voor as-labels en de maandkiezer). */\nexport function maandLabel(id: string, lang = false): string {\n const [j, m] = id.split(\"-\").map(Number);\n const d = new Date(Date.UTC(j, (m ?? 1) - 1, 1));\n return d.toLocaleDateString(\"nl-NL\", { month: lang ? \"long\" : \"short\", year: \"numeric\", timeZone: \"UTC\" });\n}\n\n/** Het maand-id van nu (UTC — trackView telt in UTC). */\nexport function huidigMaandId(): string {\n const nu = new Date();\n return `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, \"0\")}`;\n}\n\n/** Aantal dagen in een \"YYYY-MM\"-maand. */\nexport function dagenInMaand(id: string): number {\n const [j, m] = id.split(\"-\").map(Number);\n return new Date(Date.UTC(j, m ?? 1, 0)).getUTCDate();\n}\n\n/** days-record → array per dag (index 0 = dag 1), met nullen voor ontbrekende dagen. */\nexport function dagReeks(stat: MaandStat | undefined, nDagen: number): number[] {\n return Array.from({ length: nDagen }, (_, i) => stat?.days?.[String(i + 1).padStart(2, \"0\")] ?? 0);\n}\n\n/** Top-N paden van een maand, genormaliseerd en gesorteerd. */\nexport function topPaden(stat: MaandStat | undefined, n: number): { label: string; waarde: number }[] {\n return Object.entries(stat?.paths ?? {})\n .sort((a, b) => b[1] - a[1])\n .slice(0, n)\n .map(([p, w]) => ({ label: padLabel(p), waarde: w }));\n}\n\n/**\n * Eerlijke maand-op-maand-delta: vergelijk t/m dezelfde dag. Halverwege de maand het hele\n * vorige maandtotaal ernaast leggen leest als \"−50%\" terwijl het gewoon de 15e is.\n */\nexport function deltaTovVorige(huidig: MaandStat | undefined, vorige: MaandStat | undefined, totDag: number): number | null {\n if (!huidig || !vorige) return null;\n const som = (s: MaandStat) =>\n Object.entries(s.days ?? {}).reduce((acc, [d, n]) => (Number(d) <= totDag ? acc + n : acc), 0);\n const nu = som(huidig);\n const toen = som(vorige);\n if (toen === 0) return null;\n return ((nu - toen) / toen) * 100;\n}\n"],"mappings":"AAaO,SAAS,SAAS,SAAyB;AAChD,SAAO,YAAY,SAAS,MAAM,IAAI,QAAQ,QAAQ,MAAM,GAAG,CAAC;AAClE;AAGO,SAAS,WAAW,IAAY,OAAO,OAAe;AAC3D,QAAM,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,EAAE,IAAI,MAAM;AACvC,QAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC;AAC/C,SAAO,EAAE,mBAAmB,SAAS,EAAE,OAAO,OAAO,SAAS,SAAS,MAAM,WAAW,UAAU,MAAM,CAAC;AAC3G;AAGO,SAAS,gBAAwB;AACtC,QAAM,KAAK,oBAAI,KAAK;AACpB,SAAO,GAAG,GAAG,eAAe,CAAC,IAAI,OAAO,GAAG,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAChF;AAGO,SAAS,aAAa,IAAoB;AAC/C,QAAM,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,EAAE,IAAI,MAAM;AACvC,SAAO,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,EAAE,WAAW;AACrD;AAGO,SAAS,SAAS,MAA6B,QAA0B;AAC9E,SAAO,MAAM,KAAK,EAAE,QAAQ,OAAO,GAAG,CAAC,GAAG,MAAM,MAAM,OAAO,OAAO,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC;AACnG;AAGO,SAAS,SAAS,MAA6B,GAAgD;AACpG,SAAO,OAAO,QAAQ,MAAM,SAAS,CAAC,CAAC,EACpC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,EAC1B,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,SAAS,CAAC,GAAG,QAAQ,EAAE,EAAE;AACxD;AAMO,SAAS,eAAe,QAA+B,QAA+B,QAA+B;AAC1H,MAAI,CAAC,UAAU,CAAC,OAAQ,QAAO;AAC/B,QAAM,MAAM,CAAC,MACX,OAAO,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,MAAO,OAAO,CAAC,KAAK,SAAS,MAAM,IAAI,KAAM,CAAC;AAC/F,QAAM,KAAK,IAAI,MAAM;AACrB,QAAM,OAAO,IAAI,MAAM;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,UAAS,KAAK,QAAQ,OAAQ;AAChC;","names":[]}
@@ -0,0 +1,51 @@
1
+ export declare const CHART: {
2
+ readonly serie: "#3987e5";
3
+ readonly accent: "#184f95";
4
+ readonly wash: "rgba(57, 135, 229, 0.10)";
5
+ readonly track: "#e9f1fc";
6
+ readonly grid: "#e2e8f0";
7
+ readonly as: "#cbd5e1";
8
+ };
9
+ /**
10
+ * Kolomgrafiek voor één maand: dag 1..N. `accentIndex` (bv. vandaag) kleurt één kolom in de
11
+ * donkere stap van dezelfde ramp — zelfde reeks, gemarkeerd, geen tweede kleur.
12
+ */
13
+ export declare function DagStaven({ waarden, accentIndex, hoogte, titel, labelVan, }: {
14
+ /** Waarde per dag, index 0 = dag 1. */
15
+ waarden: number[];
16
+ accentIndex?: number;
17
+ hoogte?: number;
18
+ titel: string;
19
+ /** Tooltiplabel per index (standaard "dag N"). */
20
+ labelVan?: (i: number) => string;
21
+ }): import("react").JSX.Element;
22
+ /** Lijngrafiek over maanden: 2px lijn, wash-vulling, crosshair-tooltip, eindpunt gemarkeerd. */
23
+ export declare function MaandLijn({ punten, hoogte, titel, }: {
24
+ punten: {
25
+ label: string;
26
+ waarde: number;
27
+ }[];
28
+ hoogte?: number;
29
+ titel: string;
30
+ }): import("react").JSX.Element | null;
31
+ /** Horizontale balkjeslijst (toppagina's): label · waarde · balk. De track is een lichtere stap
32
+ * van dezelfde ramp, zodat einden vergelijkbaar zijn zonder raster. */
33
+ export declare function BalkjesLijst({ items }: {
34
+ items: {
35
+ label: string;
36
+ waarde: number;
37
+ }[];
38
+ }): import("react").JSX.Element;
39
+ /** Stat-tegel: label · grote waarde (proportionele cijfers) · optionele delta t.o.v. een genoemde periode. */
40
+ export declare function StatTegel({ label, waarde, delta, deltaLabel, }: {
41
+ label: string;
42
+ waarde: number | string;
43
+ /** Relatief verschil in procenten (bv. +12) — kleur volgt de richting (meer bezoek = goed). */
44
+ delta?: number | null;
45
+ deltaLabel?: string;
46
+ }): import("react").JSX.Element;
47
+ /** Toegankelijke tabelweergave achter een uitklapper — alles wat een grafiek toont, zonder muis. */
48
+ export declare function AlsTabel({ kop, rijen }: {
49
+ kop: [string, string];
50
+ rijen: [string, number][];
51
+ }): import("react").JSX.Element;
@@ -0,0 +1,240 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useRef, useState } from "react";
4
+ const CHART = {
5
+ serie: "#3987e5",
6
+ accent: "#184f95",
7
+ wash: "rgba(57, 135, 229, 0.10)",
8
+ track: "#e9f1fc",
9
+ grid: "#e2e8f0",
10
+ as: "#cbd5e1"
11
+ };
12
+ const nf = new Intl.NumberFormat("nl-NL");
13
+ function netteMax(max) {
14
+ if (max <= 4) return 4;
15
+ const macht = 10 ** Math.floor(Math.log10(max));
16
+ for (const f of [1, 2, 5, 10]) if (max <= f * macht) return f * macht;
17
+ return 10 * macht;
18
+ }
19
+ function Tooltip({ t }) {
20
+ if (!t) return null;
21
+ return /* @__PURE__ */ jsxs(
22
+ "div",
23
+ {
24
+ className: "pointer-events-none absolute z-10 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 whitespace-nowrap shadow-md",
25
+ style: { left: t.x, top: Math.max(26, t.y), transform: "translate(-50%, calc(-100% - 8px))" },
26
+ role: "status",
27
+ children: [
28
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-slate-900", children: nf.format(t.waarde) }),
29
+ /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-xs text-slate-500", children: t.titel })
30
+ ]
31
+ }
32
+ );
33
+ }
34
+ function Raster({ plotH }) {
35
+ return /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0", style: { height: plotH }, children: [
36
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0", style: { top: 0, borderTop: `1px solid ${CHART.grid}` } }),
37
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0", style: { top: plotH / 2, borderTop: `1px solid ${CHART.grid}` } }),
38
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0", style: { top: plotH, borderTop: `1px solid ${CHART.as}` } })
39
+ ] });
40
+ }
41
+ function DagStaven({
42
+ waarden,
43
+ accentIndex,
44
+ hoogte = 150,
45
+ titel,
46
+ labelVan
47
+ }) {
48
+ const ref = useRef(null);
49
+ const [tip, setTip] = useState(null);
50
+ const [hover, setHover] = useState(null);
51
+ const uid = useId();
52
+ const n = Math.max(1, waarden.length);
53
+ const max = netteMax(Math.max(1, ...waarden));
54
+ const plotH = hoogte - 18;
55
+ const maxWaarde = Math.max(...waarden);
56
+ const maxIdx = waarden.indexOf(maxWaarde);
57
+ const label = labelVan ?? ((i) => `dag ${i + 1}`);
58
+ const move = (i) => {
59
+ const host = ref.current;
60
+ if (!host) return;
61
+ const r = host.getBoundingClientRect();
62
+ setHover(i);
63
+ setTip({ x: (i + 0.5) / n * r.width, y: plotH * (1 - waarden[i] / max), titel: label(i), waarde: waarden[i] });
64
+ };
65
+ const leave = () => {
66
+ setHover(null);
67
+ setTip(null);
68
+ };
69
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative", role: "img", "aria-labelledby": `${uid}-t`, style: { height: hoogte }, children: [
70
+ /* @__PURE__ */ jsx("span", { id: `${uid}-t`, className: "sr-only", children: titel }),
71
+ /* @__PURE__ */ jsx(Tooltip, { t: tip }),
72
+ /* @__PURE__ */ jsx(Raster, { plotH }),
73
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 top-0 flex items-end", style: { height: plotH, gap: 2 }, children: waarden.map((v, i) => /* @__PURE__ */ jsx(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ tabIndex: v > 0 ? 0 : -1,
78
+ "aria-label": `${label(i)}: ${nf.format(v)} weergaven`,
79
+ className: "flex h-full min-w-0 flex-1 cursor-default items-end justify-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600",
80
+ onPointerMove: () => move(i),
81
+ onFocus: () => move(i),
82
+ onPointerLeave: leave,
83
+ onBlur: leave,
84
+ children: v > 0 && /* @__PURE__ */ jsx(
85
+ "span",
86
+ {
87
+ "aria-hidden": "true",
88
+ className: "block w-full",
89
+ style: {
90
+ maxWidth: 24,
91
+ height: Math.max(3, v / max * (plotH - 6)),
92
+ borderRadius: "4px 4px 0 0",
93
+ background: i === accentIndex ? CHART.accent : CHART.serie,
94
+ opacity: hover === null || hover === i ? 1 : 0.45,
95
+ transition: "opacity 0.1s"
96
+ }
97
+ }
98
+ )
99
+ },
100
+ i
101
+ )) }),
102
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0", style: { top: plotH + 3 }, children: [0, 9, 19, n - 1].filter((v, i, a) => v >= 0 && v < n && a.indexOf(v) === i).map((i) => /* @__PURE__ */ jsx("span", { className: "absolute -translate-x-1/2 text-[10px] text-slate-400", style: { left: `${(i + 0.5) / n * 100}%` }, children: i + 1 }, i)) }),
103
+ maxWaarde > 0 && hover === null && /* @__PURE__ */ jsx(
104
+ "span",
105
+ {
106
+ "aria-hidden": "true",
107
+ className: "pointer-events-none absolute -translate-x-1/2 text-[10px] font-medium text-slate-500",
108
+ style: { left: `${(maxIdx + 0.5) / n * 100}%`, top: Math.max(0, plotH * (1 - maxWaarde / max) - 16) },
109
+ children: nf.format(maxWaarde)
110
+ }
111
+ ),
112
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute -top-0.5 right-0 bg-white pl-1 text-[10px] text-slate-400", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(max) })
113
+ ] });
114
+ }
115
+ function MaandLijn({
116
+ punten,
117
+ hoogte = 150,
118
+ titel
119
+ }) {
120
+ const ref = useRef(null);
121
+ const [tip, setTip] = useState(null);
122
+ const [kruis, setKruis] = useState(null);
123
+ const uid = useId();
124
+ const n = punten.length;
125
+ const max = netteMax(Math.max(1, ...punten.map((p) => p.waarde)));
126
+ const plotH = hoogte - 18;
127
+ const px = (i) => n === 1 ? 50 : 2 + i / (n - 1) * 96;
128
+ const py = (v) => 6 + (plotH - 12) * (1 - v / max);
129
+ const pad = punten.map((p, i) => `${i === 0 ? "M" : "L"} ${px(i)} ${py(p.waarde)}`).join(" ");
130
+ const move = (e) => {
131
+ const host = ref.current;
132
+ if (!host || n === 0) return;
133
+ const r = host.getBoundingClientRect();
134
+ const fx = (e.clientX - r.left) / r.width * 100;
135
+ const i = Math.max(0, Math.min(n - 1, Math.round((fx - 2) / 96 * (n - 1))));
136
+ setKruis(i);
137
+ setTip({ x: px(i) / 100 * r.width, y: py(punten[i].waarde), titel: punten[i].label, waarde: punten[i].waarde });
138
+ };
139
+ const leave = () => {
140
+ setKruis(null);
141
+ setTip(null);
142
+ };
143
+ if (n === 0) return null;
144
+ const focus = kruis ?? n - 1;
145
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative", role: "img", "aria-labelledby": `${uid}-t`, style: { height: hoogte }, onPointerMove: move, onPointerLeave: leave, children: [
146
+ /* @__PURE__ */ jsx("span", { id: `${uid}-t`, className: "sr-only", children: titel }),
147
+ /* @__PURE__ */ jsx(Tooltip, { t: tip }),
148
+ /* @__PURE__ */ jsx(Raster, { plotH }),
149
+ /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 100 ${plotH}`, preserveAspectRatio: "none", className: "absolute inset-x-0 top-0 block w-full", style: { height: plotH }, "aria-hidden": "true", children: [
150
+ /* @__PURE__ */ jsx("path", { d: `${pad} L ${px(n - 1)} ${plotH} L ${px(0)} ${plotH} Z`, fill: CHART.wash }),
151
+ /* @__PURE__ */ jsx("path", { d: pad, fill: "none", stroke: CHART.serie, strokeWidth: "2", strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" }),
152
+ kruis !== null && /* @__PURE__ */ jsx("line", { x1: px(kruis), x2: px(kruis), y1: "0", y2: plotH, stroke: CHART.as, strokeWidth: "1", vectorEffect: "non-scaling-stroke" })
153
+ ] }),
154
+ /* @__PURE__ */ jsx(
155
+ "span",
156
+ {
157
+ "aria-hidden": "true",
158
+ className: "pointer-events-none absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full",
159
+ style: {
160
+ left: `${px(focus)}%`,
161
+ top: py(punten[focus].waarde),
162
+ background: focus === n - 1 ? CHART.accent : CHART.serie,
163
+ boxShadow: "0 0 0 2px #ffffff"
164
+ }
165
+ }
166
+ ),
167
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0", style: { top: plotH + 3 }, children: [0, Math.floor((n - 1) / 2), n - 1].filter((v, i, a) => a.indexOf(v) === i).map((i) => /* @__PURE__ */ jsx(
168
+ "span",
169
+ {
170
+ className: "absolute text-[10px] text-slate-400",
171
+ style: { left: `${px(i)}%`, transform: i === 0 ? "none" : i === n - 1 ? "translateX(-100%)" : "translateX(-50%)" },
172
+ children: punten[i].label
173
+ },
174
+ i
175
+ )) }),
176
+ kruis === null && /* @__PURE__ */ jsx(
177
+ "span",
178
+ {
179
+ "aria-hidden": "true",
180
+ className: "pointer-events-none absolute text-[11px] font-semibold text-slate-700",
181
+ style: { left: `${px(n - 1)}%`, top: Math.max(0, py(punten[n - 1].waarde) - 20), transform: "translateX(-100%)" },
182
+ children: nf.format(punten[n - 1].waarde)
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute -top-0.5 right-0 bg-white pl-1 text-[10px] text-slate-400", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(max) })
186
+ ] });
187
+ }
188
+ function BalkjesLijst({ items }) {
189
+ const max = Math.max(1, ...items.map((i) => i.waarde));
190
+ return /* @__PURE__ */ jsx("ul", { className: "grid gap-2.5", children: items.map((it) => /* @__PURE__ */ jsxs("li", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1", children: [
191
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-slate-700", title: it.label, children: it.label }),
192
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-slate-900", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(it.waarde) }),
193
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "col-span-2 block h-1.5 w-full overflow-hidden rounded-full", style: { background: CHART.track }, children: /* @__PURE__ */ jsx("span", { className: "block h-full rounded-full", style: { width: `${it.waarde / max * 100}%`, background: CHART.serie } }) })
194
+ ] }, it.label)) });
195
+ }
196
+ function StatTegel({
197
+ label,
198
+ waarde,
199
+ delta,
200
+ deltaLabel
201
+ }) {
202
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-slate-200 bg-white p-4", children: [
203
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: label }),
204
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-3xl font-semibold text-slate-900", children: typeof waarde === "number" ? nf.format(waarde) : waarde }),
205
+ delta != null && Number.isFinite(delta) && /* @__PURE__ */ jsxs("p", { className: `mt-1 text-xs font-medium ${delta >= 0 ? "text-emerald-700" : "text-red-700"}`, children: [
206
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: delta >= 0 ? "\u25B2" : "\u25BC" }),
207
+ " ",
208
+ Math.abs(Math.round(delta)),
209
+ "%",
210
+ deltaLabel ? /* @__PURE__ */ jsxs("span", { className: "font-normal text-slate-500", children: [
211
+ " ",
212
+ deltaLabel
213
+ ] }) : null
214
+ ] })
215
+ ] });
216
+ }
217
+ function AlsTabel({ kop, rijen }) {
218
+ return /* @__PURE__ */ jsxs("details", { className: "mt-2", children: [
219
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer text-xs text-slate-400 hover:text-slate-600", children: "Bekijk als tabel" }),
220
+ /* @__PURE__ */ jsxs("table", { className: "mt-2 w-full text-sm", children: [
221
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-left text-xs text-slate-500", children: [
222
+ /* @__PURE__ */ jsx("th", { className: "py-1 font-medium", children: kop[0] }),
223
+ /* @__PURE__ */ jsx("th", { className: "py-1 text-right font-medium", children: kop[1] })
224
+ ] }) }),
225
+ /* @__PURE__ */ jsx("tbody", { children: rijen.map(([l, w]) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-slate-100", children: [
226
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-slate-700", children: l }),
227
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right text-slate-900", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(w) })
228
+ ] }, l)) })
229
+ ] })
230
+ ] });
231
+ }
232
+ export {
233
+ AlsTabel,
234
+ BalkjesLijst,
235
+ CHART,
236
+ DagStaven,
237
+ MaandLijn,
238
+ StatTegel
239
+ };
240
+ //# sourceMappingURL=charts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/ui/charts.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Lichte grafieken voor het beheer (7.17) — geen chart-library: dit is een editor-chunk en\n * drie vormen (kolommen, lijn, balkjeslijst) rechtvaardigen geen dependency in elke klantsite.\n *\n * Vaste GEREEDSCHAPSKLEUREN, bewust niet de merkkleur: `--color-brand` volgt `--xicms-primary`\n * van de klant en kan licht zijn (goud, lime) — dan verdwijnen de staven op een witte kaart.\n * Dit blauwpaar is met de dataviz-validator doorgerekend op wit (lichtheid, chroma, CVD-ΔE 18.9,\n * normaal-zicht-ΔE 19.2, contrast ≥ 3:1 — alles groen). Zelfde filosofie als de editor-balk:\n * gereedschap ziet er op elke site hetzelfde uit.\n *\n * Vormregels die hier vastliggen (dataviz-methode):\n * - kolommen: ronde datakant (4px), vlakke voet op de basislijn, ≤ 24px dik, 2px lucht ertussen;\n * - lijn: 2px met ronde joins, vulling als wash (10%), punten ≥ 8px met witte ring;\n * - raster: haarlijnen één stap van het oppervlak af, nooit gestreept, recessief;\n * - tekst draagt tekstkleuren, nooit de seriekleur — identiteit zit in de mark ernaast;\n * - hover hoort erbij: ruime hit-zones met tooltip, en alles wat de tooltip toont is óók\n * zonder muis bereikbaar (as-ticks, selectieve labels, de tabel-uitklapper).\n *\n * Bewust HTML-marks (divs) waar het kan: een SVG met `preserveAspectRatio=\"none\"` schaalt\n * niet-uniform, waardoor rondingen en cirkels vervormen — divs met border-radius blijven\n * pixel-zuiver op elke kaartbreedte. Alleen het lijnpad zelf is SVG (met non-scaling-stroke).\n */\nimport { useId, useRef, useState } from \"react\";\n\nexport const CHART = {\n serie: \"#3987e5\",\n accent: \"#184f95\",\n wash: \"rgba(57, 135, 229, 0.10)\",\n track: \"#e9f1fc\",\n grid: \"#e2e8f0\",\n as: \"#cbd5e1\",\n} as const;\n\nconst nf = new Intl.NumberFormat(\"nl-NL\");\n\n/** Nette bovengrens voor de y-as (1/2/5 × macht van 10), zodat de schaal-tick rond is. */\nfunction netteMax(max: number): number {\n if (max <= 4) return 4;\n const macht = 10 ** Math.floor(Math.log10(max));\n for (const f of [1, 2, 5, 10]) if (max <= f * macht) return f * macht;\n return 10 * macht;\n}\n\ninterface TooltipStand {\n x: number;\n y: number;\n titel: string;\n waarde: number;\n}\n\n/** Zwevende uitlezing. Waarde voorop en zwaar, label secundair — de lezer wil het getal. */\nfunction Tooltip({ t }: { t: TooltipStand | null }) {\n if (!t) return null;\n return (\n <div\n className=\"pointer-events-none absolute z-10 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 whitespace-nowrap shadow-md\"\n style={{ left: t.x, top: Math.max(26, t.y), transform: \"translate(-50%, calc(-100% - 8px))\" }}\n role=\"status\"\n >\n <span className=\"text-sm font-semibold text-slate-900\">{nf.format(t.waarde)}</span>\n <span className=\"ml-1.5 text-xs text-slate-500\">{t.titel}</span>\n </div>\n );\n}\n\n/** Recessief raster: haarlijnen op 50% en 100% van de schaal + de basislijn. */\nfunction Raster({ plotH }: { plotH: number }) {\n return (\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-0 top-0\" style={{ height: plotH }}>\n <div className=\"absolute inset-x-0\" style={{ top: 0, borderTop: `1px solid ${CHART.grid}` }} />\n <div className=\"absolute inset-x-0\" style={{ top: plotH / 2, borderTop: `1px solid ${CHART.grid}` }} />\n <div className=\"absolute inset-x-0\" style={{ top: plotH, borderTop: `1px solid ${CHART.as}` }} />\n </div>\n );\n}\n\n/**\n * Kolomgrafiek voor één maand: dag 1..N. `accentIndex` (bv. vandaag) kleurt één kolom in de\n * donkere stap van dezelfde ramp — zelfde reeks, gemarkeerd, geen tweede kleur.\n */\nexport function DagStaven({\n waarden,\n accentIndex,\n hoogte = 150,\n titel,\n labelVan,\n}: {\n /** Waarde per dag, index 0 = dag 1. */\n waarden: number[];\n accentIndex?: number;\n hoogte?: number;\n titel: string;\n /** Tooltiplabel per index (standaard \"dag N\"). */\n labelVan?: (i: number) => string;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const [tip, setTip] = useState<TooltipStand | null>(null);\n const [hover, setHover] = useState<number | null>(null);\n const uid = useId();\n\n const n = Math.max(1, waarden.length);\n const max = netteMax(Math.max(1, ...waarden));\n const plotH = hoogte - 18;\n const maxWaarde = Math.max(...waarden);\n const maxIdx = waarden.indexOf(maxWaarde);\n const label = labelVan ?? ((i: number) => `dag ${i + 1}`);\n\n const move = (i: number) => {\n const host = ref.current;\n if (!host) return;\n const r = host.getBoundingClientRect();\n setHover(i);\n setTip({ x: ((i + 0.5) / n) * r.width, y: plotH * (1 - waarden[i] / max), titel: label(i), waarde: waarden[i] });\n };\n const leave = () => {\n setHover(null);\n setTip(null);\n };\n\n return (\n <div ref={ref} className=\"relative\" role=\"img\" aria-labelledby={`${uid}-t`} style={{ height: hoogte }}>\n <span id={`${uid}-t`} className=\"sr-only\">{titel}</span>\n <Tooltip t={tip} />\n <Raster plotH={plotH} />\n {/* De kolommen zelf: flex-sloten met 2px lucht; de balk groeit vanaf de basislijn en is\n nooit dikker dan 24px — de rest van het slot blijft lucht. */}\n <div className=\"absolute inset-x-0 top-0 flex items-end\" style={{ height: plotH, gap: 2 }}>\n {waarden.map((v, i) => (\n <button\n key={i}\n type=\"button\"\n tabIndex={v > 0 ? 0 : -1}\n aria-label={`${label(i)}: ${nf.format(v)} weergaven`}\n className=\"flex h-full min-w-0 flex-1 cursor-default items-end justify-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600\"\n onPointerMove={() => move(i)}\n onFocus={() => move(i)}\n onPointerLeave={leave}\n onBlur={leave}\n >\n {v > 0 && (\n <span\n aria-hidden=\"true\"\n className=\"block w-full\"\n style={{\n maxWidth: 24,\n height: Math.max(3, (v / max) * (plotH - 6)),\n borderRadius: \"4px 4px 0 0\",\n background: i === accentIndex ? CHART.accent : CHART.serie,\n opacity: hover === null || hover === i ? 1 : 0.45,\n transition: \"opacity 0.1s\",\n }}\n />\n )}\n </button>\n ))}\n </div>\n {/* Daglabels: spaarzaam (1 · 10 · 20 · einde). */}\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-0\" style={{ top: plotH + 3 }}>\n {[0, 9, 19, n - 1]\n .filter((v, i, a) => v >= 0 && v < n && a.indexOf(v) === i)\n .map((i) => (\n <span key={i} className=\"absolute -translate-x-1/2 text-[10px] text-slate-400\" style={{ left: `${((i + 0.5) / n) * 100}%` }}>\n {i + 1}\n </span>\n ))}\n </div>\n {/* Selectief direct label: alleen de piek — de rest draagt de tooltip en de schaal-tick. */}\n {maxWaarde > 0 && hover === null && (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute -translate-x-1/2 text-[10px] font-medium text-slate-500\"\n style={{ left: `${((maxIdx + 0.5) / n) * 100}%`, top: Math.max(0, plotH * (1 - maxWaarde / max) - 16) }}\n >\n {nf.format(maxWaarde)}\n </span>\n )}\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute -top-0.5 right-0 bg-white pl-1 text-[10px] text-slate-400\" style={{ fontVariantNumeric: \"tabular-nums\" }}>\n {nf.format(max)}\n </span>\n </div>\n );\n}\n\n/** Lijngrafiek over maanden: 2px lijn, wash-vulling, crosshair-tooltip, eindpunt gemarkeerd. */\nexport function MaandLijn({\n punten,\n hoogte = 150,\n titel,\n}: {\n punten: { label: string; waarde: number }[];\n hoogte?: number;\n titel: string;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const [tip, setTip] = useState<TooltipStand | null>(null);\n const [kruis, setKruis] = useState<number | null>(null);\n const uid = useId();\n\n const n = punten.length;\n const max = netteMax(Math.max(1, ...punten.map((p) => p.waarde)));\n const plotH = hoogte - 18;\n // x in procenten (2% marge), y in pixels — de SVG rekt alleen horizontaal.\n const px = (i: number) => (n === 1 ? 50 : 2 + (i / (n - 1)) * 96);\n const py = (v: number) => 6 + (plotH - 12) * (1 - v / max);\n const pad = punten.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${px(i)} ${py(p.waarde)}`).join(\" \");\n\n const move = (e: React.PointerEvent) => {\n const host = ref.current;\n if (!host || n === 0) return;\n const r = host.getBoundingClientRect();\n const fx = ((e.clientX - r.left) / r.width) * 100;\n const i = Math.max(0, Math.min(n - 1, Math.round(((fx - 2) / 96) * (n - 1))));\n setKruis(i);\n setTip({ x: (px(i) / 100) * r.width, y: py(punten[i].waarde), titel: punten[i].label, waarde: punten[i].waarde });\n };\n const leave = () => {\n setKruis(null);\n setTip(null);\n };\n\n if (n === 0) return null;\n const focus = kruis ?? n - 1;\n\n return (\n <div ref={ref} className=\"relative\" role=\"img\" aria-labelledby={`${uid}-t`} style={{ height: hoogte }} onPointerMove={move} onPointerLeave={leave}>\n <span id={`${uid}-t`} className=\"sr-only\">{titel}</span>\n <Tooltip t={tip} />\n <Raster plotH={plotH} />\n <svg viewBox={`0 0 100 ${plotH}`} preserveAspectRatio=\"none\" className=\"absolute inset-x-0 top-0 block w-full\" style={{ height: plotH }} aria-hidden=\"true\">\n {/* Wash onder de lijn: de seriekleur op 10% — een waas, nooit een vlak. */}\n <path d={`${pad} L ${px(n - 1)} ${plotH} L ${px(0)} ${plotH} Z`} fill={CHART.wash} />\n <path d={pad} fill=\"none\" stroke={CHART.serie} strokeWidth=\"2\" strokeLinejoin=\"round\" strokeLinecap=\"round\" vectorEffect=\"non-scaling-stroke\" />\n {kruis !== null && (\n <line x1={px(kruis)} x2={px(kruis)} y1=\"0\" y2={plotH} stroke={CHART.as} strokeWidth=\"1\" vectorEffect=\"non-scaling-stroke\" />\n )}\n </svg>\n {/* Punt als HTML-dot (8px + 2px witte ring): een SVG-cirkel vervormt onder de niet-uniforme\n schaal. Alleen het focuspunt draagt een dot — een dot op elk punt is ruis. */}\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full\"\n style={{\n left: `${px(focus)}%`,\n top: py(punten[focus].waarde),\n background: focus === n - 1 ? CHART.accent : CHART.serie,\n boxShadow: \"0 0 0 2px #ffffff\",\n }}\n />\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-0\" style={{ top: plotH + 3 }}>\n {[0, Math.floor((n - 1) / 2), n - 1]\n .filter((v, i, a) => a.indexOf(v) === i)\n .map((i) => (\n <span\n key={i}\n className=\"absolute text-[10px] text-slate-400\"\n style={{ left: `${px(i)}%`, transform: i === 0 ? \"none\" : i === n - 1 ? \"translateX(-100%)\" : \"translateX(-50%)\" }}\n >\n {punten[i].label}\n </span>\n ))}\n </div>\n {/* Direct label op het eindpunt — de waarde waar het verhaal om draait. */}\n {kruis === null && (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute text-[11px] font-semibold text-slate-700\"\n style={{ left: `${px(n - 1)}%`, top: Math.max(0, py(punten[n - 1].waarde) - 20), transform: \"translateX(-100%)\" }}\n >\n {nf.format(punten[n - 1].waarde)}\n </span>\n )}\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute -top-0.5 right-0 bg-white pl-1 text-[10px] text-slate-400\" style={{ fontVariantNumeric: \"tabular-nums\" }}>\n {nf.format(max)}\n </span>\n </div>\n );\n}\n\n/** Horizontale balkjeslijst (toppagina's): label · waarde · balk. De track is een lichtere stap\n * van dezelfde ramp, zodat einden vergelijkbaar zijn zonder raster. */\nexport function BalkjesLijst({ items }: { items: { label: string; waarde: number }[] }) {\n const max = Math.max(1, ...items.map((i) => i.waarde));\n return (\n <ul className=\"grid gap-2.5\">\n {items.map((it) => (\n <li key={it.label} className=\"grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1\">\n <span className=\"truncate text-sm text-slate-700\" title={it.label}>{it.label}</span>\n <span className=\"text-sm font-medium text-slate-900\" style={{ fontVariantNumeric: \"tabular-nums\" }}>\n {nf.format(it.waarde)}\n </span>\n <span aria-hidden=\"true\" className=\"col-span-2 block h-1.5 w-full overflow-hidden rounded-full\" style={{ background: CHART.track }}>\n <span className=\"block h-full rounded-full\" style={{ width: `${(it.waarde / max) * 100}%`, background: CHART.serie }} />\n </span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** Stat-tegel: label · grote waarde (proportionele cijfers) · optionele delta t.o.v. een genoemde periode. */\nexport function StatTegel({\n label,\n waarde,\n delta,\n deltaLabel,\n}: {\n label: string;\n waarde: number | string;\n /** Relatief verschil in procenten (bv. +12) — kleur volgt de richting (meer bezoek = goed). */\n delta?: number | null;\n deltaLabel?: string;\n}) {\n return (\n <div className=\"rounded-xl border border-slate-200 bg-white p-4\">\n <p className=\"text-sm text-slate-500\">{label}</p>\n <p className=\"mt-1 text-3xl font-semibold text-slate-900\">\n {typeof waarde === \"number\" ? nf.format(waarde) : waarde}\n </p>\n {delta != null && Number.isFinite(delta) && (\n <p className={`mt-1 text-xs font-medium ${delta >= 0 ? \"text-emerald-700\" : \"text-red-700\"}`}>\n <span aria-hidden=\"true\">{delta >= 0 ? \"▲\" : \"▼\"}</span> {Math.abs(Math.round(delta))}%\n {deltaLabel ? <span className=\"font-normal text-slate-500\"> {deltaLabel}</span> : null}\n </p>\n )}\n </div>\n );\n}\n\n/** Toegankelijke tabelweergave achter een uitklapper — alles wat een grafiek toont, zonder muis. */\nexport function AlsTabel({ kop, rijen }: { kop: [string, string]; rijen: [string, number][] }) {\n return (\n <details className=\"mt-2\">\n <summary className=\"cursor-pointer text-xs text-slate-400 hover:text-slate-600\">Bekijk als tabel</summary>\n <table className=\"mt-2 w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 font-medium\">{kop[0]}</th>\n <th className=\"py-1 text-right font-medium\">{kop[1]}</th>\n </tr>\n </thead>\n <tbody>\n {rijen.map(([l, w]) => (\n <tr key={l} className=\"border-t border-slate-100\">\n <td className=\"py-1 text-slate-700\">{l}</td>\n <td className=\"py-1 text-right text-slate-900\" style={{ fontVariantNumeric: \"tabular-nums\" }}>{nf.format(w)}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </details>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/admin.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-indigo-500:oklch(58.5% .233 277.117);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--color-brand-50:#eef2ff;--color-brand-500:#4f46e5;--color-brand-600:#4f46e5;--color-brand-700:#3730a3}}@scope(.xicms-admin){@layer utilities{.visible{visibility:visible!important}.absolute{position:absolute!important}.fixed{position:fixed!important}.relative{position:relative!important}.static{position:static!important}.sticky{position:sticky!important}.-top-2{top:calc(var(--spacing) * -2)!important}.top-0{top:0!important}.-right-2{right:calc(var(--spacing) * -2)!important}.bottom-0{bottom:0!important}.z-10{z-index:10!important}.container{width:100%!important}@media (min-width:40rem){.container{max-width:40rem!important}}@media (min-width:48rem){.container{max-width:48rem!important}}@media (min-width:64rem){.container{max-width:64rem!important}}@media (min-width:80rem){.container{max-width:80rem!important}}@media (min-width:96rem){.container{max-width:96rem!important}}.-mx-1{margin-inline:calc(var(--spacing) * -1)!important}.-mx-4{margin-inline:calc(var(--spacing) * -4)!important}.mt-0\.5{margin-top:calc(var(--spacing) * .5)!important}.mt-1{margin-top:var(--spacing)!important}.mt-2{margin-top:calc(var(--spacing) * 2)!important}.mt-3{margin-top:calc(var(--spacing) * 3)!important}.mt-4{margin-top:calc(var(--spacing) * 4)!important}.mt-5{margin-top:calc(var(--spacing) * 5)!important}.mt-6{margin-top:calc(var(--spacing) * 6)!important}.mt-auto{margin-top:auto!important}.mr-1{margin-right:var(--spacing)!important}.mr-2{margin-right:calc(var(--spacing) * 2)!important}.mb-1{margin-bottom:var(--spacing)!important}.mb-2{margin-bottom:calc(var(--spacing) * 2)!important}.mb-3{margin-bottom:calc(var(--spacing) * 3)!important}.mb-4{margin-bottom:calc(var(--spacing) * 4)!important}.ml-1{margin-left:var(--spacing)!important}.ml-auto{margin-left:auto!important}.block{display:block!important}.contents{display:contents!important}.flex{display:flex!important}.grid{display:grid!important}.hidden{display:none!important}.inline{display:inline!important}.inline-block{display:inline-block!important}.inline-flex{display:inline-flex!important}.table{display:table!important}.aspect-square{aspect-ratio:1!important}.h-5{height:calc(var(--spacing) * 5)!important}.h-6{height:calc(var(--spacing) * 6)!important}.h-8{height:calc(var(--spacing) * 8)!important}.h-9{height:calc(var(--spacing) * 9)!important}.h-10{height:calc(var(--spacing) * 10)!important}.h-14{height:calc(var(--spacing) * 14)!important}.h-16{height:calc(var(--spacing) * 16)!important}.h-28{height:calc(var(--spacing) * 28)!important}.h-32{height:calc(var(--spacing) * 32)!important}.h-\[70vh\]{height:70vh!important}.h-fit{height:fit-content!important}.max-h-\[55vh\]{max-height:55vh!important}.max-h-\[60vh\]{max-height:60vh!important}.min-h-\[8rem\]{min-height:8rem!important}.min-h-screen{min-height:100vh!important}.w-5{width:calc(var(--spacing) * 5)!important}.w-6{width:calc(var(--spacing) * 6)!important}.w-8{width:calc(var(--spacing) * 8)!important}.w-14{width:calc(var(--spacing) * 14)!important}.w-16{width:calc(var(--spacing) * 16)!important}.w-20{width:calc(var(--spacing) * 20)!important}.w-24{width:calc(var(--spacing) * 24)!important}.w-28{width:calc(var(--spacing) * 28)!important}.w-32{width:calc(var(--spacing) * 32)!important}.w-36{width:calc(var(--spacing) * 36)!important}.w-56{width:calc(var(--spacing) * 56)!important}.w-fit{width:fit-content!important}.w-full{width:100%!important}.max-w-3xl{max-width:var(--container-3xl)!important}.max-w-4xl{max-width:var(--container-4xl)!important}.max-w-md{max-width:var(--container-md)!important}.max-w-sm{max-width:var(--container-sm)!important}.max-w-xs{max-width:var(--container-xs)!important}.min-w-0{min-width:0!important}.min-w-8{min-width:calc(var(--spacing) * 8)!important}.flex-1{flex:1!important}.shrink-0{flex-shrink:0!important}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.cursor-pointer{cursor:pointer!important}.resize{resize:both!important}.list-decimal{list-style-type:decimal!important}.list-disc{list-style-type:disc!important}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.grid-cols-\[1fr_1\.4fr_auto\]{grid-template-columns:1fr 1.4fr auto!important}.grid-cols-\[120px_1fr\]{grid-template-columns:120px 1fr!important}.flex-col{flex-direction:column!important}.flex-wrap{flex-wrap:wrap!important}.place-items-center{place-items:center!important}.items-baseline{align-items:baseline!important}.items-center{align-items:center!important}.items-end{align-items:flex-end!important}.items-start{align-items:flex-start!important}.justify-between{justify-content:space-between!important}.justify-center{justify-content:center!important}.justify-end{justify-content:flex-end!important}.gap-0\.5{gap:calc(var(--spacing) * .5)!important}.gap-1{gap:var(--spacing)!important}.gap-1\.5{gap:calc(var(--spacing) * 1.5)!important}.gap-2{gap:calc(var(--spacing) * 2)!important}.gap-2\.5{gap:calc(var(--spacing) * 2.5)!important}.gap-3{gap:calc(var(--spacing) * 3)!important}.gap-4{gap:calc(var(--spacing) * 4)!important}.gap-6{gap:calc(var(--spacing) * 6)!important}.self-center{align-self:center!important}.self-end{align-self:flex-end!important}.truncate{text-overflow:ellipsis!important;white-space:nowrap!important;overflow:hidden!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-x-auto{overflow-x:auto!important}.overflow-y-auto{overflow-y:auto!important}.rounded{border-radius:.25rem!important}.rounded-full{border-radius:3.40282e38px!important}.rounded-lg{border-radius:var(--radius-lg)!important}.rounded-md{border-radius:var(--radius-md)!important}.rounded-xl{border-radius:var(--radius-xl)!important}.rounded-t{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-t-lg{border-top-left-radius:var(--radius-lg)!important;border-top-right-radius:var(--radius-lg)!important}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg)!important;border-bottom-left-radius:var(--radius-lg)!important}.border{border-style:var(--tw-border-style)!important;border-width:1px!important}.border-t{border-top-style:var(--tw-border-style)!important;border-top-width:1px!important}.border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-b{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:1px!important}.border-amber-300{border-color:var(--color-amber-300)!important}.border-brand-500{border-color:var(--color-brand-500)!important}.border-red-200{border-color:var(--color-red-200)!important}.border-slate-100{border-color:var(--color-slate-100)!important}.border-slate-200{border-color:var(--color-slate-200)!important}.border-slate-300{border-color:var(--color-slate-300)!important}.bg-amber-50{background-color:var(--color-amber-50)!important}.bg-brand-50{background-color:var(--color-brand-50)!important}.bg-brand-500{background-color:var(--color-brand-500)!important}.bg-red-50{background-color:var(--color-red-50)!important}.bg-slate-50{background-color:var(--color-slate-50)!important}.bg-slate-100{background-color:var(--color-slate-100)!important}.bg-slate-200{background-color:var(--color-slate-200)!important}.bg-slate-700{background-color:var(--color-slate-700)!important}.bg-white{background-color:var(--color-white)!important}.object-cover{object-fit:cover!important}.p-0\.5{padding:calc(var(--spacing) * .5)!important}.p-1{padding:var(--spacing)!important}.p-2{padding:calc(var(--spacing) * 2)!important}.p-3{padding:calc(var(--spacing) * 3)!important}.p-4{padding:calc(var(--spacing) * 4)!important}.p-5{padding:calc(var(--spacing) * 5)!important}.p-6{padding:calc(var(--spacing) * 6)!important}.px-1{padding-inline:var(--spacing)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)!important}.px-2{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)!important}.px-4{padding-inline:calc(var(--spacing) * 4)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)!important}.py-1{padding-block:var(--spacing)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)!important}.py-2{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)!important}.py-8{padding-block:calc(var(--spacing) * 8)!important}.py-16{padding-block:calc(var(--spacing) * 16)!important}.pt-0{padding-top:0!important}.pt-1{padding-top:var(--spacing)!important}.pt-3{padding-top:calc(var(--spacing) * 3)!important}.pt-4{padding-top:calc(var(--spacing) * 4)!important}.pt-5{padding-top:calc(var(--spacing) * 5)!important}.pr-2{padding-right:calc(var(--spacing) * 2)!important}.pl-5{padding-left:calc(var(--spacing) * 5)!important}.text-center{text-align:center!important}.text-left{text-align:left!important}.text-right{text-align:right!important}.align-top{vertical-align:top!important}.font-mono{font-family:var(--font-mono)!important}.text-2xl{font-size:var(--text-2xl)!important;line-height:var(--tw-leading,var(--text-2xl--line-height))!important}.text-3xl{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.text-lg{font-size:var(--text-lg)!important;line-height:var(--tw-leading,var(--text-lg--line-height))!important}.text-sm{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.text-xl{font-size:var(--text-xl)!important;line-height:var(--tw-leading,var(--text-xl--line-height))!important}.text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-\[11px\]{font-size:11px!important}.font-bold{--tw-font-weight:var(--font-weight-bold)!important;font-weight:var(--font-weight-bold)!important}.font-medium{--tw-font-weight:var(--font-weight-medium)!important;font-weight:var(--font-weight-medium)!important}.font-semibold{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.tracking-wide{--tw-tracking:var(--tracking-wide)!important;letter-spacing:var(--tracking-wide)!important}.whitespace-pre-wrap{white-space:pre-wrap!important}.text-amber-600{color:var(--color-amber-600)!important}.text-amber-700{color:var(--color-amber-700)!important}.text-amber-800{color:var(--color-amber-800)!important}.text-amber-900{color:var(--color-amber-900)!important}.text-brand-700{color:var(--color-brand-700)!important}.text-brand-700\/70{color:#3730a3b3!important}@supports (color:color-mix(in lab, red, red)){.text-brand-700\/70{color:color-mix(in oklab, var(--color-brand-700) 70%, transparent)!important}}.text-red-600{color:var(--color-red-600)!important}.text-red-700{color:var(--color-red-700)!important}.text-slate-400{color:var(--color-slate-400)!important}.text-slate-500{color:var(--color-slate-500)!important}.text-slate-600{color:var(--color-slate-600)!important}.text-slate-700{color:var(--color-slate-700)!important}.text-slate-800{color:var(--color-slate-800)!important}.text-slate-900{color:var(--color-slate-900)!important}.text-white{color:var(--color-white)!important}.uppercase{text-transform:uppercase!important}.italic{font-style:italic!important}.line-through{text-decoration-line:line-through!important}.underline{text-decoration-line:underline!important}.opacity-70{opacity:.7!important}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.outline{outline-style:var(--tw-outline-style)!important;outline-width:1px!important}.blur{--tw-blur:blur(8px)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.grayscale{--tw-grayscale:grayscale(100%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-\[filter\]{transition-property:filter!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.ease-out{--tw-ease:var(--ease-out)!important;transition-timing-function:var(--ease-out)!important}.outline-none{--tw-outline-style:none!important;outline-style:none!important}@media (hover:hover){.group-hover\:opacity-80:is(:where(.group):hover *){opacity:.8!important}}.last\:border-0:last-child{border-style:var(--tw-border-style)!important;border-width:0!important}@media (hover:hover){.hover\:border-brand-500:hover{border-color:var(--color-brand-500)!important}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)!important}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)!important}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)!important}.hover\:text-slate-700:hover{color:var(--color-slate-700)!important}.hover\:text-slate-900:hover{color:var(--color-slate-900)!important}.hover\:underline:hover{text-decoration-line:underline!important}.hover\:brightness-95:hover{--tw-brightness:brightness(95%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)!important}.focus\:outline-none:focus{--tw-outline-style:none!important;outline-style:none!important}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus-visible\:ring-brand-500:focus-visible{--tw-ring-color:var(--color-brand-500)!important}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none!important;outline-style:none!important}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed!important}.disabled\:opacity-50:disabled{opacity:.5!important}.aria-\[current\=page\]\:bg-slate-200[aria-current=page]{background-color:var(--color-slate-200)!important}.aria-\[current\=page\]\:text-slate-900[aria-current=page]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:border-b-2[data-state=active]{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:2px!important}.data-\[state\=active\]\:border-brand-600[data-state=active]{border-color:var(--color-brand-600)!important}.data-\[state\=active\]\:bg-slate-100[data-state=active]{background-color:var(--color-slate-100)!important}.data-\[state\=active\]\:text-brand-700[data-state=active]{color:var(--color-brand-700)!important}.data-\[state\=active\]\:text-slate-900[data-state=active]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:shadow-\[inset_2px_0_0_0_var\(--color-brand-500\)\][data-state=active]{--tw-shadow:inset 2px 0 0 0 var(--tw-shadow-color,var(--color-brand-500))!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}@media (min-width:48rem){.md\:sticky{position:sticky!important}.md\:top-0{top:0!important}.md\:col-span-5{grid-column:span 5/span 5!important}.md\:flex{display:flex!important}.md\:grid{display:grid!important}.md\:h-screen{height:100vh!important}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}.md\:grid-cols-\[1fr_1fr_140px_auto_auto\]{grid-template-columns:1fr 1fr 140px auto auto!important}.md\:grid-cols-\[1fr_110px\]{grid-template-columns:1fr 110px!important}.md\:grid-cols-\[1fr_130px_90px_110px_auto\]{grid-template-columns:1fr 130px 90px 110px auto!important}.md\:grid-cols-\[1fr_140px_1fr_150px\]{grid-template-columns:1fr 140px 1fr 150px!important}.md\:grid-cols-\[248px_1fr\]{grid-template-columns:248px 1fr!important}.md\:grid-cols-\[260px_1fr\]{grid-template-columns:260px 1fr!important}.md\:grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr!important}.md\:grid-cols-\[300px_1fr\]{grid-template-columns:300px 1fr!important}.md\:grid-cols-\[340px_1fr\]{grid-template-columns:340px 1fr!important}.md\:flex-col{flex-direction:column!important}.md\:overflow-visible{overflow:visible!important}.md\:border-r{border-right-style:var(--tw-border-style)!important;border-right-width:1px!important}.md\:border-b-0{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:0!important}.md\:p-8{padding:calc(var(--spacing) * 8)!important}.md\:px-8{padding-inline:calc(var(--spacing) * 8)!important}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2!important}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}}}.xicms-admin,.xicms-admin *,.xicms-admin :before,.xicms-admin :after{box-sizing:border-box}.xicms-admin{-webkit-font-smoothing:antialiased;--color-white:#fff;--color-slate-50:#faf9f6;--color-slate-100:#f3f1ec;--color-slate-200:#e5e2da;--color-slate-300:#d5d1c7;--color-slate-400:#a8a399;--color-slate-500:#6e6a62;--color-slate-600:#57534b;--color-slate-700:#403c36;--color-slate-900:#1b1a17;--color-brand:var(--xicms-primary,#4f46e5);--color-brand-500:var(--xicms-primary,#4f46e5);--color-brand-600:var(--xicms-primary,#4f46e5);--color-brand-700:var(--xicms-primary,#4f46e5);font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-700:color-mix(in srgb, var(--xicms-primary,#4f46e5) 78%, #1b1a17)}}.xicms-admin{--color-brand-50:var(--xicms-primary,#4f46e5)}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-50:color-mix(in srgb, var(--xicms-primary,#4f46e5) 10%, #fff)}}.xicms-admin{--admin-canvas:#f6f5f2;--admin-sidebar:#f0eee9;--color-background-body:var(--color-white);--color-background-surface:var(--color-white);--color-background-card:var(--color-white);--color-background-muted:var(--color-slate-100);--color-text-primary:var(--color-slate-900);--color-text-secondary:var(--color-slate-500);--color-border:var(--color-slate-200);--color-accent:var(--color-brand-500);--color-on-accent:#fff;--color-error:#dc2626;color-scheme:light;color:var(--color-text-primary);background:var(--admin-canvas)}.xicms-admin h1,.xicms-admin h2,.xicms-admin h3,.xicms-admin p,.xicms-admin figure{margin:0}.xicms-admin button{font:inherit;cursor:pointer}.xicms-admin input,.xicms-admin textarea,.xicms-admin select{font:inherit;color:inherit}.xicms-admin .ProseMirror{outline:none;min-height:8rem}.xicms-admin .ProseMirror p{margin:0 0 .6rem}.xicms-admin .ProseMirror:focus{outline:none}@media (prefers-reduced-motion:reduce){.xicms-admin *,.xicms-admin :before,.xicms-admin :after,.xicms-ve *,.xicms-ve :before,.xicms-ve :after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.xicms-admin__midden,.xicms-admin__raster{min-height:100vh}.xicms-admin--ingebed .xicms-admin__midden{min-height:50vh}.xicms-admin--ingebed .xicms-admin__raster{min-height:60vh;display:block}.xicms-admin--ingebed .xicms-admin__rail{border-bottom-width:1px;border-right:0;flex-flow:wrap;align-items:center;gap:1rem;height:auto;padding:.5rem 1rem;position:static}.xicms-admin--ingebed .xicms-admin__lijst{flex-direction:row;overflow-x:auto}.xicms-admin--ingebed .xicms-admin__voet{border-top:0;flex-direction:row;align-items:center;gap:.75rem;margin-top:0;margin-left:auto;padding-top:0;display:flex}.xicms-admin--ingebed .xicms-admin__kop{position:static}.xicms-admin--ingebed{width:100vw;max-width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-emerald-700:oklch(50.8% .118 165.612);--color-blue-600:oklch(54.6% .245 262.881);--color-indigo-500:oklch(58.5% .233 277.117);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--color-brand:#4f46e5;--color-brand-50:#eef2ff;--color-brand-500:#4f46e5;--color-brand-600:#4f46e5;--color-brand-700:#3730a3}}@scope(.xicms-admin){@layer utilities{.pointer-events-none{pointer-events:none!important}.visible{visibility:visible!important}.sr-only{clip-path:inset(50%)!important;white-space:nowrap!important;border-width:0!important;width:1px!important;height:1px!important;margin:-1px!important;padding:0!important;position:absolute!important;overflow:hidden!important}.absolute{position:absolute!important}.fixed{position:fixed!important}.relative{position:relative!important}.static{position:static!important}.sticky{position:sticky!important}.inset-x-0{inset-inline:0!important}.-top-0\.5{top:calc(var(--spacing) * -.5)!important}.-top-2{top:calc(var(--spacing) * -2)!important}.top-0{top:0!important}.-right-2{right:calc(var(--spacing) * -2)!important}.right-0{right:0!important}.bottom-0{bottom:0!important}.z-10{z-index:10!important}.col-span-2{grid-column:span 2/span 2!important}.container{width:100%!important}@media (min-width:40rem){.container{max-width:40rem!important}}@media (min-width:48rem){.container{max-width:48rem!important}}@media (min-width:64rem){.container{max-width:64rem!important}}@media (min-width:80rem){.container{max-width:80rem!important}}@media (min-width:96rem){.container{max-width:96rem!important}}.-mx-1{margin-inline:calc(var(--spacing) * -1)!important}.-mx-4{margin-inline:calc(var(--spacing) * -4)!important}.mt-0\.5{margin-top:calc(var(--spacing) * .5)!important}.mt-1{margin-top:var(--spacing)!important}.mt-2{margin-top:calc(var(--spacing) * 2)!important}.mt-3{margin-top:calc(var(--spacing) * 3)!important}.mt-4{margin-top:calc(var(--spacing) * 4)!important}.mt-5{margin-top:calc(var(--spacing) * 5)!important}.mt-6{margin-top:calc(var(--spacing) * 6)!important}.mt-auto{margin-top:auto!important}.mr-1{margin-right:var(--spacing)!important}.mr-2{margin-right:calc(var(--spacing) * 2)!important}.mb-1{margin-bottom:var(--spacing)!important}.mb-2{margin-bottom:calc(var(--spacing) * 2)!important}.mb-3{margin-bottom:calc(var(--spacing) * 3)!important}.mb-4{margin-bottom:calc(var(--spacing) * 4)!important}.ml-1{margin-left:var(--spacing)!important}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)!important}.ml-auto{margin-left:auto!important}.block{display:block!important}.contents{display:contents!important}.flex{display:flex!important}.grid{display:grid!important}.hidden{display:none!important}.inline{display:inline!important}.inline-block{display:inline-block!important}.inline-flex{display:inline-flex!important}.table{display:table!important}.aspect-square{aspect-ratio:1!important}.h-1\.5{height:calc(var(--spacing) * 1.5)!important}.h-2{height:calc(var(--spacing) * 2)!important}.h-5{height:calc(var(--spacing) * 5)!important}.h-6{height:calc(var(--spacing) * 6)!important}.h-8{height:calc(var(--spacing) * 8)!important}.h-9{height:calc(var(--spacing) * 9)!important}.h-10{height:calc(var(--spacing) * 10)!important}.h-14{height:calc(var(--spacing) * 14)!important}.h-16{height:calc(var(--spacing) * 16)!important}.h-28{height:calc(var(--spacing) * 28)!important}.h-32{height:calc(var(--spacing) * 32)!important}.h-\[70vh\]{height:70vh!important}.h-fit{height:fit-content!important}.h-full{height:100%!important}.max-h-\[55vh\]{max-height:55vh!important}.max-h-\[60vh\]{max-height:60vh!important}.min-h-\[8rem\]{min-height:8rem!important}.min-h-screen{min-height:100vh!important}.w-2{width:calc(var(--spacing) * 2)!important}.w-5{width:calc(var(--spacing) * 5)!important}.w-6{width:calc(var(--spacing) * 6)!important}.w-8{width:calc(var(--spacing) * 8)!important}.w-14{width:calc(var(--spacing) * 14)!important}.w-16{width:calc(var(--spacing) * 16)!important}.w-20{width:calc(var(--spacing) * 20)!important}.w-24{width:calc(var(--spacing) * 24)!important}.w-28{width:calc(var(--spacing) * 28)!important}.w-32{width:calc(var(--spacing) * 32)!important}.w-36{width:calc(var(--spacing) * 36)!important}.w-44{width:calc(var(--spacing) * 44)!important}.w-56{width:calc(var(--spacing) * 56)!important}.w-fit{width:fit-content!important}.w-full{width:100%!important}.max-w-3xl{max-width:var(--container-3xl)!important}.max-w-4xl{max-width:var(--container-4xl)!important}.max-w-md{max-width:var(--container-md)!important}.max-w-sm{max-width:var(--container-sm)!important}.max-w-xs{max-width:var(--container-xs)!important}.min-w-0{min-width:0!important}.min-w-8{min-width:calc(var(--spacing) * 8)!important}.flex-1{flex:1!important}.shrink-0{flex-shrink:0!important}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.cursor-default{cursor:default!important}.cursor-pointer{cursor:pointer!important}.resize{resize:both!important}.list-decimal{list-style-type:decimal!important}.list-disc{list-style-type:disc!important}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.grid-cols-\[1fr_1\.4fr_auto\]{grid-template-columns:1fr 1.4fr auto!important}.grid-cols-\[120px_1fr\]{grid-template-columns:120px 1fr!important}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto!important}.flex-col{flex-direction:column!important}.flex-wrap{flex-wrap:wrap!important}.place-items-center{place-items:center!important}.items-baseline{align-items:baseline!important}.items-center{align-items:center!important}.items-end{align-items:flex-end!important}.items-start{align-items:flex-start!important}.justify-between{justify-content:space-between!important}.justify-center{justify-content:center!important}.justify-end{justify-content:flex-end!important}.gap-0\.5{gap:calc(var(--spacing) * .5)!important}.gap-1{gap:var(--spacing)!important}.gap-1\.5{gap:calc(var(--spacing) * 1.5)!important}.gap-2{gap:calc(var(--spacing) * 2)!important}.gap-2\.5{gap:calc(var(--spacing) * 2.5)!important}.gap-3{gap:calc(var(--spacing) * 3)!important}.gap-4{gap:calc(var(--spacing) * 4)!important}.gap-6{gap:calc(var(--spacing) * 6)!important}.gap-x-3{column-gap:calc(var(--spacing) * 3)!important}.gap-x-5{column-gap:calc(var(--spacing) * 5)!important}.gap-y-1{row-gap:var(--spacing)!important}.self-center{align-self:center!important}.self-end{align-self:flex-end!important}.truncate{text-overflow:ellipsis!important;white-space:nowrap!important;overflow:hidden!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-x-auto{overflow-x:auto!important}.overflow-y-auto{overflow-y:auto!important}.rounded{border-radius:.25rem!important}.rounded-full{border-radius:3.40282e38px!important}.rounded-lg{border-radius:var(--radius-lg)!important}.rounded-md{border-radius:var(--radius-md)!important}.rounded-xl{border-radius:var(--radius-xl)!important}.rounded-t-lg{border-top-left-radius:var(--radius-lg)!important;border-top-right-radius:var(--radius-lg)!important}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg)!important;border-bottom-left-radius:var(--radius-lg)!important}.border{border-style:var(--tw-border-style)!important;border-width:1px!important}.border-t{border-top-style:var(--tw-border-style)!important;border-top-width:1px!important}.border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-b{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:1px!important}.border-amber-300{border-color:var(--color-amber-300)!important}.border-brand-500{border-color:var(--color-brand-500)!important}.border-red-200{border-color:var(--color-red-200)!important}.border-slate-100{border-color:var(--color-slate-100)!important}.border-slate-200{border-color:var(--color-slate-200)!important}.border-slate-300{border-color:var(--color-slate-300)!important}.bg-amber-50{background-color:var(--color-amber-50)!important}.bg-brand-50{background-color:var(--color-brand-50)!important}.bg-red-50{background-color:var(--color-red-50)!important}.bg-slate-50{background-color:var(--color-slate-50)!important}.bg-slate-100{background-color:var(--color-slate-100)!important}.bg-slate-200{background-color:var(--color-slate-200)!important}.bg-slate-700{background-color:var(--color-slate-700)!important}.bg-white{background-color:var(--color-white)!important}.object-cover{object-fit:cover!important}.p-0\.5{padding:calc(var(--spacing) * .5)!important}.p-1{padding:var(--spacing)!important}.p-2{padding:calc(var(--spacing) * 2)!important}.p-3{padding:calc(var(--spacing) * 3)!important}.p-4{padding:calc(var(--spacing) * 4)!important}.p-5{padding:calc(var(--spacing) * 5)!important}.p-6{padding:calc(var(--spacing) * 6)!important}.px-1{padding-inline:var(--spacing)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)!important}.px-2{padding-inline:calc(var(--spacing) * 2)!important}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)!important}.px-3{padding-inline:calc(var(--spacing) * 3)!important}.px-4{padding-inline:calc(var(--spacing) * 4)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)!important}.py-1{padding-block:var(--spacing)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)!important}.py-2{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)!important}.py-8{padding-block:calc(var(--spacing) * 8)!important}.py-16{padding-block:calc(var(--spacing) * 16)!important}.pt-0{padding-top:0!important}.pt-1{padding-top:var(--spacing)!important}.pt-3{padding-top:calc(var(--spacing) * 3)!important}.pt-4{padding-top:calc(var(--spacing) * 4)!important}.pt-5{padding-top:calc(var(--spacing) * 5)!important}.pr-2{padding-right:calc(var(--spacing) * 2)!important}.pl-1{padding-left:var(--spacing)!important}.pl-5{padding-left:calc(var(--spacing) * 5)!important}.text-center{text-align:center!important}.text-left{text-align:left!important}.text-right{text-align:right!important}.align-top{vertical-align:top!important}.font-mono{font-family:var(--font-mono)!important}.text-3xl{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.text-lg{font-size:var(--text-lg)!important;line-height:var(--tw-leading,var(--text-lg--line-height))!important}.text-sm{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.text-xl{font-size:var(--text-xl)!important;line-height:var(--tw-leading,var(--text-xl--line-height))!important}.text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-\[10px\]{font-size:10px!important}.text-\[11px\]{font-size:11px!important}.font-bold{--tw-font-weight:var(--font-weight-bold)!important;font-weight:var(--font-weight-bold)!important}.font-medium{--tw-font-weight:var(--font-weight-medium)!important;font-weight:var(--font-weight-medium)!important}.font-normal{--tw-font-weight:var(--font-weight-normal)!important;font-weight:var(--font-weight-normal)!important}.font-semibold{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.tracking-wide{--tw-tracking:var(--tracking-wide)!important;letter-spacing:var(--tracking-wide)!important}.whitespace-nowrap{white-space:nowrap!important}.whitespace-pre-wrap{white-space:pre-wrap!important}.text-amber-600{color:var(--color-amber-600)!important}.text-amber-700{color:var(--color-amber-700)!important}.text-amber-800{color:var(--color-amber-800)!important}.text-amber-900{color:var(--color-amber-900)!important}.text-brand-700{color:var(--color-brand-700)!important}.text-brand-700\/70{color:#3730a3b3!important}@supports (color:color-mix(in lab, red, red)){.text-brand-700\/70{color:color-mix(in oklab, var(--color-brand-700) 70%, transparent)!important}}.text-emerald-700{color:var(--color-emerald-700)!important}.text-red-600{color:var(--color-red-600)!important}.text-red-700{color:var(--color-red-700)!important}.text-slate-400{color:var(--color-slate-400)!important}.text-slate-500{color:var(--color-slate-500)!important}.text-slate-600{color:var(--color-slate-600)!important}.text-slate-700{color:var(--color-slate-700)!important}.text-slate-800{color:var(--color-slate-800)!important}.text-slate-900{color:var(--color-slate-900)!important}.text-white{color:var(--color-white)!important}.uppercase{text-transform:uppercase!important}.italic{font-style:italic!important}.tabular-nums{--tw-numeric-spacing:tabular-nums!important;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)!important}.line-through{text-decoration-line:line-through!important}.underline{text-decoration-line:underline!important}.opacity-70{opacity:.7!important}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.outline{outline-style:var(--tw-outline-style)!important;outline-width:1px!important}.blur{--tw-blur:blur(8px)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.grayscale{--tw-grayscale:grayscale(100%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-\[filter\]{transition-property:filter!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.ease-out{--tw-ease:var(--ease-out)!important;transition-timing-function:var(--ease-out)!important}.outline-none{--tw-outline-style:none!important;outline-style:none!important}@media (hover:hover){.group-hover\:opacity-80:is(:where(.group):hover *){opacity:.8!important}}.last\:border-0:last-child{border-style:var(--tw-border-style)!important;border-width:0!important}@media (hover:hover){.hover\:border-brand-500:hover{border-color:var(--color-brand-500)!important}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)!important}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)!important}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)!important}.hover\:text-slate-600:hover{color:var(--color-slate-600)!important}.hover\:text-slate-700:hover{color:var(--color-slate-700)!important}.hover\:text-slate-900:hover{color:var(--color-slate-900)!important}.hover\:underline:hover{text-decoration-line:underline!important}.hover\:brightness-95:hover{--tw-brightness:brightness(95%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)!important}.focus\:outline-none:focus{--tw-outline-style:none!important;outline-style:none!important}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus-visible\:ring-brand-500:focus-visible{--tw-ring-color:var(--color-brand-500)!important}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style)!important;outline-width:2px!important}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px!important}.focus-visible\:outline-blue-600:focus-visible{outline-color:var(--color-blue-600)!important}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none!important;outline-style:none!important}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed!important}.disabled\:opacity-50:disabled{opacity:.5!important}.aria-\[current\=page\]\:bg-slate-200[aria-current=page]{background-color:var(--color-slate-200)!important}.aria-\[current\=page\]\:text-slate-900[aria-current=page]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:border-b-2[data-state=active]{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:2px!important}.data-\[state\=active\]\:border-brand-600[data-state=active]{border-color:var(--color-brand-600)!important}.data-\[state\=active\]\:bg-slate-100[data-state=active]{background-color:var(--color-slate-100)!important}.data-\[state\=active\]\:text-brand-700[data-state=active]{color:var(--color-brand-700)!important}.data-\[state\=active\]\:text-slate-900[data-state=active]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:shadow-\[inset_2px_0_0_0_var\(--color-brand-500\)\][data-state=active]{--tw-shadow:inset 2px 0 0 0 var(--tw-shadow-color,var(--color-brand-500))!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}@media (min-width:48rem){.md\:sticky{position:sticky!important}.md\:top-0{top:0!important}.md\:col-span-5{grid-column:span 5/span 5!important}.md\:flex{display:flex!important}.md\:grid{display:grid!important}.md\:h-screen{height:100vh!important}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}.md\:grid-cols-\[1fr_1fr_140px_auto_auto\]{grid-template-columns:1fr 1fr 140px auto auto!important}.md\:grid-cols-\[1fr_110px\]{grid-template-columns:1fr 110px!important}.md\:grid-cols-\[1fr_130px_90px_110px_auto\]{grid-template-columns:1fr 130px 90px 110px auto!important}.md\:grid-cols-\[1fr_140px_1fr_150px\]{grid-template-columns:1fr 140px 1fr 150px!important}.md\:grid-cols-\[248px_1fr\]{grid-template-columns:248px 1fr!important}.md\:grid-cols-\[260px_1fr\]{grid-template-columns:260px 1fr!important}.md\:grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr!important}.md\:grid-cols-\[300px_1fr\]{grid-template-columns:300px 1fr!important}.md\:grid-cols-\[340px_1fr\]{grid-template-columns:340px 1fr!important}.md\:flex-col{flex-direction:column!important}.md\:overflow-visible{overflow:visible!important}.md\:border-r{border-right-style:var(--tw-border-style)!important;border-right-width:1px!important}.md\:border-b-0{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:0!important}.md\:p-8{padding:calc(var(--spacing) * 8)!important}.md\:px-8{padding-inline:calc(var(--spacing) * 8)!important}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2!important}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}}}.xicms-admin,.xicms-admin *,.xicms-admin :before,.xicms-admin :after{box-sizing:border-box}.xicms-admin{-webkit-font-smoothing:antialiased;--color-white:#fff;--color-slate-50:#faf9f6;--color-slate-100:#f3f1ec;--color-slate-200:#e5e2da;--color-slate-300:#d5d1c7;--color-slate-400:#a8a399;--color-slate-500:#6e6a62;--color-slate-600:#57534b;--color-slate-700:#403c36;--color-slate-900:#1b1a17;--color-brand:var(--xicms-primary,#4f46e5);--color-brand-500:var(--xicms-primary,#4f46e5);--color-brand-600:var(--xicms-primary,#4f46e5);--color-brand-700:var(--xicms-primary,#4f46e5);font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-700:color-mix(in srgb, var(--xicms-primary,#4f46e5) 78%, #1b1a17)}}.xicms-admin{--color-brand-50:var(--xicms-primary,#4f46e5)}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-50:color-mix(in srgb, var(--xicms-primary,#4f46e5) 10%, #fff)}}.xicms-admin{--admin-canvas:#f6f5f2;--admin-sidebar:#f0eee9;--color-background-body:var(--color-white);--color-background-surface:var(--color-white);--color-background-card:var(--color-white);--color-background-muted:var(--color-slate-100);--color-text-primary:var(--color-slate-900);--color-text-secondary:var(--color-slate-500);--color-border:var(--color-slate-200);--color-accent:var(--color-brand-500);--color-on-accent:#fff;--color-error:#dc2626;color-scheme:light;color:var(--color-text-primary);background:var(--admin-canvas)}.xicms-admin h1,.xicms-admin h2,.xicms-admin h3,.xicms-admin p,.xicms-admin figure{margin:0}.xicms-admin button{font:inherit;cursor:pointer}.xicms-admin input,.xicms-admin textarea,.xicms-admin select{font:inherit;color:inherit}.xicms-admin .ProseMirror{outline:none;min-height:8rem}.xicms-admin .ProseMirror p{margin:0 0 .6rem}.xicms-admin .ProseMirror:focus{outline:none}@media (prefers-reduced-motion:reduce){.xicms-admin *,.xicms-admin :before,.xicms-admin :after,.xicms-ve *,.xicms-ve :before,.xicms-ve :after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.xicms-admin__midden,.xicms-admin__raster{min-height:100vh}.xicms-admin--ingebed .xicms-admin__midden{min-height:50vh}.xicms-admin--ingebed .xicms-admin__raster{min-height:60vh;display:block}.xicms-admin--ingebed .xicms-admin__rail{border-bottom-width:1px;border-right:0;flex-flow:wrap;align-items:center;gap:1rem;height:auto;padding:.5rem 1rem;position:static}.xicms-admin--ingebed .xicms-admin__lijst{flex-direction:row;overflow-x:auto}.xicms-admin--ingebed .xicms-admin__voet{border-top:0;flex-direction:row;align-items:center;gap:.75rem;margin-top:0;margin-left:auto;padding-top:0;display:flex}.xicms-admin--ingebed .xicms-admin__kop{position:static}.xicms-admin--ingebed{width:100vw;max-width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const CORE_VERSION = true ? "7.16.2" : "0.0.0-dev";
1
+ const CORE_VERSION = true ? "7.17.0" : "0.0.0-dev";
2
2
  const CORE_PACKAGE = "@xiwebdesign/xicms-core";
3
3
  function isNieuwer(beschikbaar, huidig) {
4
4
  const delen = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiwebdesign/xicms-core",
3
- "version": "7.16.2",
3
+ "version": "7.17.0",
4
4
  "description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {