feat(ui): Dunkelmodus fuer Shop und Admin mit Umschalter

Rollen-Farben statt reiner Markenfarben, damit hell und dunkel aus einem
Satz Klassen entstehen:
- heading (Ueberschriften, Links, Rahmen): hell Waldgruen, dunkel Creme
- surface (abgesetzte Flaeche): hell Creme, dunkel angehobenes Dunkelgruen
- forest bleibt die dunkle Markenflaeche (Hero, Baender, Knoepfe,
  Admin-Leiste), cream der helle Text darauf
- honey-deep als fester Honigton fuer Flaechen; honey-dark (Text) wird
  im Dunkelmodus heller und bleibt lesbar
Codemod ueber src: text-/border-forest -> *-heading, bg-cream ->
bg-surface, bg-honey-dark -> bg-honey-deep. Im hellen Modus haben die
Rollen exakt die bisherigen Werte - das Erscheinungsbild bleibt gleich.

Umschalter (components/theme-toggle.tsx) Automatisch -> Hell -> Dunkel in
Kopfzeile, Mobilmenue und Admin-Leiste. Die Wahl liegt im Cookie "theme"
(nur light/dark, geprueft in lib/theme.ts); der Server setzt data-theme
schon beim Ausliefern, nichts blitzt hell auf. "Automatisch" folgt der
Systemeinstellung. Gewaehlter und automatischer Dunkelblock in
globals.css werden per Test auf Gleichheit geprueft.

Logo, Biene und Select-Pfeil nutzen die Rollenfarben. In der Kopfzeile
steht "Honig entdecken" zwischen 1280 und 1535 px nicht mehr doppelt
neben "Shop" (sonst stiess die Navigation an), der Slogan erscheint erst
ab 1680 px.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-26 06:12:44 +02:00
1 parent b77516b9f5
commit 6950657c91
95 files changed
+514 -272

No files matched your search

+10 -10
View File
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
/**
* Verlinkte Karte fuer die Konto-Uebersicht (Bestellungen, Adressen, ...).
* Ersetzt den zuvor identischen, icon-losen "border + hover:border-forest"-
* Ersetzt den zuvor identischen, icon-losen "border + hover:border-heading"-
* Kartenstil, der auf jeder Konto-Seite gleich aussah.
*/
export function AccountNavCard({
@@ -27,7 +27,7 @@ export function AccountNavCard({
className={`group relative flex items-start gap-5 border p-6 transition-[transform,border-color,box-shadow] duration-500 ease-[var(--ease-arrive)] hover:-translate-y-1 ${
accent
? "border-honey/40 bg-honey/5 hover:border-honey hover:shadow-[0_22px_40px_-24px_rgba(184,138,68,0.45)]"
: "border-ink/10 bg-background hover:border-forest/60 hover:shadow-[0_22px_40px_-24px_rgba(36,53,43,0.35)]"
: "border-ink/10 bg-background hover:border-heading/60 hover:shadow-[0_22px_40px_-24px_rgba(36,53,43,0.35)]"
} ${className}`}
>
{/* Icon in einer Wabe, die sich beim Hover mit Honig fuellt. */}
@@ -39,20 +39,20 @@ export function AccountNavCard({
/>
<span
className={`relative transition-colors duration-500 group-hover:text-cream ${
accent ? "text-honey-dark" : "text-forest"
accent ? "text-honey-dark" : "text-heading"
}`}
>
{icon}
</span>
</span>
<span className="flex-1 pt-1">
<span className="block font-serif text-2xl leading-tight text-forest">{title}</span>
<span className="block font-serif text-2xl leading-tight text-heading">{title}</span>
<span className="block mt-1.5 text-sm text-ink/70">{description}</span>
</span>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="mt-2 h-4 w-4 flex-shrink-0 -translate-x-2 text-forest opacity-0 transition-[opacity,transform] duration-500 ease-[var(--ease-arrive)] group-hover:translate-x-0 group-hover:opacity-100"
className="mt-2 h-4 w-4 flex-shrink-0 -translate-x-2 text-heading opacity-0 transition-[opacity,transform] duration-500 ease-[var(--ease-arrive)] group-hover:translate-x-0 group-hover:opacity-100"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
@@ -68,9 +68,9 @@ const ORDER_STATUS_STYLES: Record<string, string> = {
BEZAHLT: "border-honey/40 text-honey-dark bg-honey/5",
IN_VORBEREITUNG: "border-honey/40 text-honey-dark bg-honey/5",
VERSANDBEREIT: "border-honey/40 text-honey-dark bg-honey/5",
VERSENDET: "border-forest/40 text-forest bg-forest/5",
ABHOLBEREIT: "border-forest/40 text-forest bg-forest/5",
ABGEHOLT: "border-forest/40 text-forest bg-forest/5",
VERSENDET: "border-heading/40 text-heading bg-forest/5",
ABHOLBEREIT: "border-heading/40 text-heading bg-forest/5",
ABGEHOLT: "border-heading/40 text-heading bg-forest/5",
STORNIERT: "border-red-200 text-red-700 bg-red-50",
};
@@ -182,7 +182,7 @@ export function OrderStatusTimeline({
</span>
) : null}
</div>
<p className={`mt-2 text-[11px] text-center leading-tight ${isDone ? "text-forest" : "text-ink/55"}`}>
<p className={`mt-2 text-[11px] text-center leading-tight ${isDone ? "text-heading" : "text-ink/55"}`}>
{step.label}
</p>
</div>
@@ -218,7 +218,7 @@ export function AccountEmptyState({
</svg>
<span className="relative">{icon}</span>
</span>
<p className="mt-5 font-serif text-2xl text-forest">{title}</p>
<p className="mt-5 font-serif text-2xl text-heading">{title}</p>
{description ? (
<p className="mt-1 text-sm text-ink/70 max-w-sm mx-auto">{description}</p>
) : null}
+2 -2
View File
@@ -32,7 +32,7 @@ export function AdminArchiveTabs({
const tabClass = (selected: boolean): string =>
`px-4 py-2 text-sm border-b-2 -mb-px transition-colors ${
selected ? "border-forest text-forest" : "border-transparent text-ink/60 hover:text-forest"
selected ? "border-heading text-heading" : "border-transparent text-ink/60 hover:text-heading"
}`;
return (
@@ -72,7 +72,7 @@ export function AdminArchiveToggle({
{archivedManually ? ` · ${labels.manualHint}` : null}
</span>
) : null}
<button type="submit" className="link-draw cursor-pointer text-ink/70 hover:text-forest">
<button type="submit" className="link-draw cursor-pointer text-ink/70 hover:text-heading">
{archivedAt ? labels.restore : labels.archive}
</button>
</form>
+1 -1
View File
@@ -148,7 +148,7 @@ export function AdminCommandPalette({
onMouseEnter={() => setActive(i)}
onClick={() => go(entry)}
className={`flex w-full items-center gap-3 px-5 py-2.5 text-left text-sm transition-colors ${
i === active ? "bg-honey/10 text-forest" : "text-ink/75"
i === active ? "bg-honey/10 text-heading" : "text-ink/75"
}`}
>
<HexMark
+1 -1
View File
@@ -102,7 +102,7 @@ export function AdminFieldset({
}) {
return (
<fieldset className={`space-y-4 ${last ? "" : "border-b border-ink/10 pb-8"} ${className}`}>
<legend className="mb-4 font-serif text-xl text-forest">{legend}</legend>
<legend className="mb-4 font-serif text-xl text-heading">{legend}</legend>
{children}
</fieldset>
);
+1 -1
View File
@@ -95,7 +95,7 @@ export function AdminImageField({ name, label, initialValue }: AdminImageFieldPr
type="button"
onClick={() => inputRef.current?.click()}
disabled={status === "uploading"}
className="border border-ink/20 bg-background px-3 py-2 text-xs hover:border-forest disabled:opacity-50"
className="border border-ink/20 bg-background px-3 py-2 text-xs hover:border-heading disabled:opacity-50"
>
{t.adminImageField.chooseFile}
</button>
+1 -1
View File
@@ -111,7 +111,7 @@ export function AdminImageListField({ name, label, initialValues }: AdminImageLi
type="button"
onClick={() => inputRef.current?.click()}
disabled={status === "uploading"}
className="border border-ink/20 bg-background px-3 py-2 text-xs hover:border-forest disabled:opacity-50"
className="border border-ink/20 bg-background px-3 py-2 text-xs hover:border-heading disabled:opacity-50"
>
{t.adminImageField.chooseFile}
</button>
+5 -5
View File
@@ -20,7 +20,7 @@ export function AdminPageHeader({
return (
<div className="mb-8">
<div className="flex flex-wrap items-center justify-between gap-4">
<h1 className="text-2xl text-forest">{title}</h1>
<h1 className="text-2xl text-heading">{title}</h1>
{action ? (
<LinkButton href={action.href} className="px-5 py-2.5">
{action.label}
@@ -74,7 +74,7 @@ export function AdminSearchForm({
{children}
<button
type="submit"
className="border border-forest px-4 py-2 text-sm text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
className="border border-heading px-4 py-2 text-sm text-heading transition-colors duration-300 hover:bg-forest hover:text-cream"
>
{submitLabel}
</button>
@@ -90,7 +90,7 @@ export function AdminSearchForm({
export type AdminBadgeTone = "positive" | "pending" | "neutral" | "danger";
const BADGE_TONES: Record<AdminBadgeTone, string> = {
positive: "border-forest/40 bg-forest/5 text-forest",
positive: "border-heading/40 bg-forest/5 text-heading",
pending: "border-honey/40 bg-honey/5 text-honey-dark",
neutral: "border-ink/20 text-ink/70",
danger: "border-red-200 bg-red-50 text-red-700",
@@ -155,14 +155,14 @@ export function AdminRowActions({ children }: { children: ReactNode }) {
export function AdminActionLink({ href, children }: { href: string; children: ReactNode }) {
return (
<Link href={href} className="link-draw text-forest">
<Link href={href} className="link-draw text-heading">
{children}
</Link>
);
}
const ACTION_TONES = {
default: "text-forest",
default: "text-heading",
muted: "text-ink/70",
danger: "text-red-700",
} as const;
+19 -5
View File
@@ -10,6 +10,8 @@ import { HexMark, HoneycombPattern } from "@/components/honeycomb";
import { AdminCommandPalette, type PaletteEntry } from "@/components/admin-command-palette";
import { useI18n } from "@/i18n/i18n-provider";
import type { Dictionary } from "@/i18n/dictionary-type";
import { ThemeToggle } from "@/components/theme-toggle";
import type { ThemeChoice } from "@/lib/theme";
/** Was an einer Seite gerade auf Bearbeitung wartet (Zaehler in der Seitenleiste). */
export type AdminBadges = Partial<Record<string, number>>;
@@ -118,7 +120,7 @@ function NavLinks({
{active ? (
<motion.span
layoutId="admin-nav-active"
className="absolute inset-y-0.5 left-2 right-2 -z-10 bg-cream/10"
className="absolute inset-y-0.5 left-2 right-2 -z-10 bg-surface/10"
transition={{ type: "spring", stiffness: 420, damping: 36 }}
/>
) : null}
@@ -131,7 +133,7 @@ function NavLinks({
<span className="flex-1">{item.label}</span>
{count > 0 ? (
<span
className="min-w-5 rounded-full bg-honey-dark px-1.5 text-center text-[11px] leading-5 text-cream tabular-nums"
className="min-w-5 rounded-full bg-honey-deep px-1.5 text-center text-[11px] leading-5 text-cream tabular-nums"
title={waitingLabel(count)}
>
<span aria-hidden="true">{count}</span>
@@ -175,6 +177,8 @@ function PaletteTrigger({ onClick, label, shortcut }: { onClick: () => void; lab
interface AdminShellProps {
adminName: string;
/** Gewaehlte Darstellung (Cookie), siehe lib/theme.ts. */
themeChoice?: ThemeChoice;
/** Logo fuer dunkle Flaechen, siehe lib/brand.ts. */
logoSrc?: string | null;
badges?: AdminBadges;
@@ -190,7 +194,14 @@ interface AdminShellProps {
* Shop-Kopf- und -Fusszeile blendet das Root-Layout hier aus
* (components/hide-on-admin.tsx).
*/
export function AdminShell({ adminName, logoSrc = null, badges = {}, logoutAction, children }: AdminShellProps) {
export function AdminShell({
adminName,
logoSrc = null,
themeChoice = "system",
badges = {},
logoutAction,
children,
}: AdminShellProps) {
const { t } = useI18n();
const groups = getAdminNavGroups(t);
const pathname = usePathname();
@@ -209,9 +220,10 @@ export function AdminShell({ adminName, logoSrc = null, badges = {}, logoutActio
<div className="px-5 py-4 border-t border-cream/10">
{/* Der Shop-Kopf ist im Admin ausgeblendet (HideOnAdmin) — ohne diesen
Link fuehrte kein Weg zurueck auf die Website. */}
<div className="mb-4 flex items-center justify-between gap-3">
<Link
href="/"
className="group mb-4 inline-flex items-center gap-2 py-1 text-sm text-cream/90 hover:text-cream"
className="group inline-flex items-center gap-2 py-1 text-sm text-cream/90 hover:text-cream"
>
<svg
width="14"
@@ -227,6 +239,8 @@ export function AdminShell({ adminName, logoSrc = null, badges = {}, logoutActio
</svg>
<span className="link-draw">{t.adminShell.backToWebsite}</span>
</Link>
<ThemeToggle initial={themeChoice} className="-mr-2 text-cream/70 hover:text-cream" />
</div>
<p className="text-xs text-cream/60 truncate">{t.adminShell.loggedInAs(adminName)}</p>
<form action={logoutAction} className="mt-2">
<button type="submit" className="text-xs text-cream/80 underline hover:text-cream">
@@ -237,7 +251,7 @@ export function AdminShell({ adminName, logoSrc = null, badges = {}, logoutActio
);
return (
<div className="min-h-screen bg-cream/40 lg:flex">
<div className="min-h-screen bg-surface/40 lg:flex">
<aside className="relative isolate hidden overflow-hidden lg:flex lg:flex-col lg:w-64 lg:shrink-0 lg:sticky lg:top-0 lg:h-screen bg-forest text-cream">
<HoneycombPattern className="-z-10 text-honey-light/[0.06]" size={18} fade="bottom" />
<div className="flex items-center h-16 px-5 border-b border-cream/10">
+1 -1
View File
@@ -94,7 +94,7 @@ export function AdminVideoField({ name, label, initialValue }: AdminVideoFieldPr
type="button"
onClick={() => inputRef.current?.click()}
disabled={status === "uploading"}
className="border border-ink/20 bg-background px-3 py-2 text-xs hover:border-forest disabled:opacity-50"
className="border border-ink/20 bg-background px-3 py-2 text-xs hover:border-heading disabled:opacity-50"
>
{t.adminVideoField.chooseFile}
</button>
+2 -2
View File
@@ -48,7 +48,7 @@ export async function ApiCoreSyncResult({ searchParams }: { searchParams: SyncSe
</p>
) : null}
{done ? (
<div className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3 space-y-1">
<div className="mb-6 text-sm text-heading bg-forest/5 border border-heading/20 px-4 py-3 space-y-1">
<p>
{t.resultHarvests(
count(searchParams.hNew),
@@ -92,7 +92,7 @@ export async function ApiCoreSyncPanel({ returnTo }: { returnTo: string }) {
return (
<div className="bg-background border border-ink/10 p-6">
<h2 className="text-lg text-forest mb-4">{t.heading}</h2>
<h2 className="text-lg text-heading mb-4">{t.heading}</h2>
<p className="text-sm text-ink/65 mb-4">{t.intro}</p>
<form action={syncApiCoreAction}>
<input type="hidden" name="returnTo" value={returnTo} />
+1 -1
View File
@@ -9,7 +9,7 @@ export function BackLink({ href, label }: { href: string; label: string }) {
return (
<Link
href={href}
className="-mt-1.5 mb-4.5 inline-flex items-center gap-1.5 py-1.5 text-sm text-ink/70 hover:text-forest"
className="-mt-1.5 mb-4.5 inline-flex items-center gap-1.5 py-1.5 text-sm text-ink/70 hover:text-heading"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75">
<path d="M15 6l-6 6 6 6" />
+4 -4
View File
@@ -19,8 +19,8 @@ export function BeeGlyph({ className = "h-8 w-10" }: { className?: string }) {
</defs>
{/* Fluegel — kippen um ihre Unterkante */}
<g className="motion-flutter origin-bottom animate-[wh-flutter_0.11s_ease-in-out_infinite_alternate] [transform-box:fill-box]">
<ellipse cx="16.5" cy="9.5" rx="5.2" ry="8" transform="rotate(-22 16.5 9.5)" fill="#FAF8F3" fillOpacity="0.9" stroke="#24352B" strokeWidth="1.1" />
<ellipse cx="23.5" cy="10" rx="4.4" ry="6.8" transform="rotate(14 23.5 10)" fill="#FAF8F3" fillOpacity="0.9" stroke="#24352B" strokeWidth="1.1" />
<ellipse cx="16.5" cy="9.5" rx="5.2" ry="8" transform="rotate(-22 16.5 9.5)" fill="#FAF8F3" fillOpacity="0.9" stroke="var(--color-heading)" strokeWidth="1.1" />
<ellipse cx="23.5" cy="10" rx="4.4" ry="6.8" transform="rotate(14 23.5 10)" fill="#FAF8F3" fillOpacity="0.9" stroke="var(--color-heading)" strokeWidth="1.1" />
</g>
{/* Stachel */}
<path d="M9.6 19.2 5.2 20l4.4.9z" fill="#24352B" />
@@ -31,8 +31,8 @@ export function BeeGlyph({ className = "h-8 w-10" }: { className?: string }) {
</g>
<ellipse cx="20" cy="20" rx="10.5" ry="6.8" fill="none" stroke="#24352B" strokeWidth="1.2" />
{/* Kopf und Fuehler */}
<circle cx="32" cy="18.8" r="4.3" fill="#24352B" />
<path d="M33.5 15c1-2.6 2.6-3.9 4.8-4.1M31.4 14.8c-.1-2.8.8-4.6 2.6-5.6" fill="none" stroke="#24352B" strokeWidth="1" strokeLinecap="round" />
<circle cx="32" cy="18.8" r="4.3" fill="var(--color-heading)" />
<path d="M33.5 15c1-2.6 2.6-3.9 4.8-4.1M31.4 14.8c-.1-2.8.8-4.6 2.6-5.6" fill="none" stroke="var(--color-heading)" strokeWidth="1" strokeLinecap="round" />
</svg>
);
}
+5 -5
View File
@@ -65,7 +65,7 @@ export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: s
if (status === "success") {
return (
<p className="text-forest">
<p className="text-heading">
{t.businessInquiryForm.successMessage}
</p>
);
@@ -95,14 +95,14 @@ export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: s
{t.unternehmenPage.packages.map((p, i) => (
<label
key={p.key}
className="group relative flex cursor-pointer flex-col border border-ink/20 bg-background p-4 transition-colors duration-300 hover:border-ink/40 has-[:checked]:border-forest has-[:checked]:bg-forest/[0.04] has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-honey-dark"
className="group relative flex cursor-pointer flex-col border border-ink/20 bg-background p-4 transition-colors duration-300 hover:border-ink/40 has-[:checked]:border-heading has-[:checked]:bg-forest/[0.04] has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-honey-dark"
>
<input type="radio" name="package" value={p.key} defaultChecked={i === 0} className="sr-only" />
<span className="flex items-center justify-between gap-3">
<span className="font-serif text-xl text-forest">{p.title}</span>
<span className="font-serif text-xl text-heading">{p.title}</span>
<span
aria-hidden="true"
className="h-3.5 w-3.5 flex-shrink-0 rounded-full border border-ink/30 transition-[border-width,border-color] duration-200 group-has-[:checked]:border-[4.5px] group-has-[:checked]:border-forest"
className="h-3.5 w-3.5 flex-shrink-0 rounded-full border border-ink/30 transition-[border-width,border-color] duration-200 group-has-[:checked]:border-[4.5px] group-has-[:checked]:border-heading"
/>
</span>
<span className="mt-2 text-ink/70 leading-relaxed">{p.text}</span>
@@ -151,7 +151,7 @@ function TextField({
name={name}
type={type}
required={required}
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest"
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-heading"
/>
</label>
);
+1 -1
View File
@@ -25,7 +25,7 @@ export function CopyCodeButton({ code, label, copiedLabel }: { code: string; lab
<button
type="button"
onClick={copy}
className="inline-flex h-10 items-center gap-2 border border-forest px-4 text-xs tracking-wide-caps text-forest transition-colors hover:bg-forest hover:text-cream"
className="inline-flex h-10 items-center gap-2 border border-heading px-4 text-xs tracking-wide-caps text-heading transition-colors hover:bg-forest hover:text-cream"
aria-live="polite"
>
<AnimatePresence mode="wait" initial={false}>
+1 -1
View File
@@ -11,7 +11,7 @@ import { FLASH_COOKIE, type Flash } from "@/lib/flash-core";
const AUTO_HIDE_MS: Record<Flash["kind"], number> = { success: 6000, info: 8000, error: 12000 };
const STYLES: Record<Flash["kind"], string> = {
success: "bg-forest text-cream border-forest",
success: "bg-forest text-cream border-heading",
info: "bg-background text-ink border-ink/20",
error: "bg-red-50 text-red-800 border-red-300",
};
+4 -4
View File
@@ -73,7 +73,7 @@ export async function OriginStory() {
return (
// Randlos geteilt: das Foto fuellt die linke Haelfte bis an den
// Fensterrand, der Text steht daneben — kein Rahmen, keine Karte.
<section className="grid bg-cream/60 md:grid-cols-2">
<section className="grid bg-surface/60 md:grid-cols-2">
<ScrollReveal className="h-full">
<ParallaxMedia className="h-full min-h-[24rem] md:min-h-[40rem]">
<ContentImage
@@ -129,7 +129,7 @@ export async function Philosophy() {
delay={i * 0.06}
className="border-b border-ink/15 py-8 sm:pr-8 lg:border-b-0 lg:border-l lg:px-8 lg:first:border-l-0 lg:first:pl-0"
>
<h3 className="font-serif text-3xl leading-tight text-forest text-balance">{item.title}</h3>
<h3 className="font-serif text-3xl leading-tight text-heading text-balance">{item.title}</h3>
<p className="mt-3 max-w-[34ch] text-sm leading-relaxed text-ink/70">{item.text}</p>
</ScrollReveal>
))}
@@ -151,7 +151,7 @@ export async function BeekeeperNote() {
const [content, company] = await Promise.all([getContentValues(["imkerei.image"], locale), getCompanyInfo()]);
const quote = t.imkereiPage.paragraphs[0];
return (
<section className="relative overflow-hidden bg-cream/60 py-24 md:py-32">
<section className="relative overflow-hidden bg-surface/60 py-24 md:py-32">
<HoneycombPattern className="text-honey/[0.08]" size={26} fade="right" />
<Container className="relative grid items-center gap-16 md:grid-cols-12 md:gap-12">
<ScrollReveal className="mx-auto w-64 sm:w-72 md:col-span-5 md:w-full md:max-w-sm">
@@ -195,7 +195,7 @@ export async function BeekeeperNote() {
</ScrollReveal>
<ScrollReveal delay={0.1} className="md:col-span-7">
<blockquote>
<p className="font-serif text-3xl leading-snug text-forest text-balance md:text-4xl lg:text-[2.75rem]">
<p className="font-serif text-3xl leading-snug text-heading text-balance md:text-4xl lg:text-[2.75rem]">
<span aria-hidden="true" className="mr-1 text-honey">„</span>
{quote}
<span aria-hidden="true" className="ml-0.5 text-honey">“</span>
+3 -3
View File
@@ -16,7 +16,7 @@ export async function ProcessTimeline() {
const content = await getContentValues(["home.processTimeline.steps"], locale);
const steps = parseListNumbered(content["home.processTimeline.steps"]);
return (
<section className="py-24 bg-cream/60">
<section className="py-24 bg-surface/60">
<Container>
<ScrollReveal>
<SectionHeading
@@ -86,7 +86,7 @@ export async function Sustainability() {
const content = await getContentValues(["home.sustainability.items"], locale);
const items = parseListTitled(content["home.sustainability.items"]);
return (
<section className="py-28 bg-cream/60">
<section className="py-28 bg-surface/60">
{/* Geteilt statt Viererreihe: die Ueberschrift laeuft mit, die Punkte
werden als ruhige Liste gelesen. */}
<Container className="grid gap-12 lg:grid-cols-12 lg:gap-16">
@@ -102,7 +102,7 @@ export async function Sustainability() {
<div className="group grid grid-cols-[auto_1fr] gap-x-5 py-7">
<HexMark className="mt-2 h-3 w-3 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90" />
<div>
<h3 className="text-2xl text-forest">{item.title}</h3>
<h3 className="text-2xl text-heading">{item.title}</h3>
<p className="mt-2 max-w-[55ch] text-ink/70 leading-relaxed">{item.text}</p>
</div>
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ export function LanguageSwitcher({ className = "" }: { className?: string }) {
aria-current={code === locale}
title={localeLabels[code]}
className={`uppercase tracking-wide transition-colors disabled:opacity-50 ${
code === locale ? "text-forest font-medium" : "text-ink/70 hover:text-forest"
code === locale ? "text-heading font-medium" : "text-ink/70 hover:text-heading"
}`}
>
{code}
+2 -2
View File
@@ -23,14 +23,14 @@ export function LegalPage({
return (
<div className="py-20">
<Container className="max-w-3xl">
<h1 className="text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] text-forest">{title}</h1>
<h1 className="text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] text-heading">{title}</h1>
{noticeText ? (
<p className="mt-2 text-xs text-honey-dark tracking-wide-caps">{noticeText}</p>
) : null}
<div className="mt-10 space-y-10">
{sections.map((section) => (
<section key={section.heading}>
<h2 className="text-2xl text-forest">{section.heading}</h2>
<h2 className="text-2xl text-heading">{section.heading}</h2>
<div className="mt-3 space-y-3 text-sm text-ink/70 leading-relaxed">
{section.paragraphs.map((p, i) => (
<p key={i}>{p}</p>
+1 -1
View File
@@ -32,7 +32,7 @@ export function Logomark({
}
const hexColor = variant === "inverted" ? "#F4EFE5" : "#B88A44";
const treeColor = variant === "inverted" ? "#F4EFE5" : "#24352B";
const treeColor = variant === "inverted" ? "#F4EFE5" : "var(--color-heading)";
return (
<svg viewBox="0 0 100 100" className={className} fill="none" aria-hidden="true">
+2 -2
View File
@@ -91,7 +91,7 @@ export function NewsletterSignup({ turnstileSiteKey }: { turnstileSiteKey?: stri
/>
</svg>
<motion.p
className="mt-4 font-serif text-2xl text-forest"
className="mt-4 font-serif text-2xl text-heading"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.45 }}
@@ -115,7 +115,7 @@ export function NewsletterSignup({ turnstileSiteKey }: { turnstileSiteKey?: stri
placeholder={t.home.newsletter.emailPlaceholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
className="min-w-0 flex-1 border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-forest"
className="min-w-0 flex-1 border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-heading"
/>
<Button type="submit" disabled={status === "loading"} className="flex-shrink-0 whitespace-nowrap">
{status === "loading" ? t.home.newsletter.subscribing : t.home.newsletter.subscribe}
+1 -1
View File
@@ -26,7 +26,7 @@ export function PageOutro({ links, ariaLabel }: { links: PageOutroLink[]; ariaLa
}`}
>
<span>
<span className="block font-serif text-4xl leading-tight text-forest md:text-5xl">
<span className="block font-serif text-4xl leading-tight text-heading md:text-5xl">
{link.label}
</span>
<span className="mt-3 block max-w-[40ch] text-sm leading-relaxed text-ink/65">{link.text}</span>
+2 -2
View File
@@ -102,13 +102,13 @@ export function PasswordField({
minLength={minLength}
autoComplete={autoComplete}
onChange={showStrength ? (e) => setValue(e.target.value) : undefined}
className="w-full border border-ink/20 bg-background px-4 py-3 pr-12 focus:outline-none focus:border-forest"
className="w-full border border-ink/20 bg-background px-4 py-3 pr-12 focus:outline-none focus:border-heading"
/>
<button
type="button"
onClick={() => setVisible((v) => !v)}
aria-label={visible ? "Passwort verbergen" : "Passwort anzeigen"}
className="absolute inset-y-0 right-0 flex items-center px-3 text-ink/50 hover:text-forest"
className="absolute inset-y-0 right-0 flex items-center px-3 text-ink/50 hover:text-heading"
>
{visible ? <EyeOffIcon /> : <EyeIcon />}
</button>
+1 -1
View File
@@ -3,7 +3,7 @@ import { Logomark } from "@/components/logomark";
const SURFACES = {
forest: "bg-forest text-cream",
cream: "bg-cream text-forest",
cream: "bg-surface text-heading",
honey: "bg-honey-pale/60 text-forest",
} as const;
+1 -1
View File
@@ -88,7 +88,7 @@ function ProcessStep({
</svg>
</motion.span>
<span className="font-serif text-sm text-honey-dark tabular-nums">{step.number}</span>
<h3 className="mt-1 text-xl text-forest">{step.title}</h3>
<h3 className="mt-1 text-xl text-heading">{step.title}</h3>
<p className="mt-2 text-sm text-ink/70 leading-relaxed">{step.text}</p>
</li>
);
+8 -8
View File
@@ -41,7 +41,7 @@ export function ProductTeaserLink({
return (
<Link
href={`/honig/${slug}`}
className="group flex items-center gap-5 border border-ink/10 p-4 transition-[border-color,transform,box-shadow] duration-500 ease-[var(--ease-arrive)] hover:-translate-y-0.5 hover:border-forest/50 hover:shadow-[0_18px_34px_-24px_rgba(36,53,43,0.45)]"
className="group flex items-center gap-5 border border-ink/10 p-4 transition-[border-color,transform,box-shadow] duration-500 ease-[var(--ease-arrive)] hover:-translate-y-0.5 hover:border-heading/50 hover:shadow-[0_18px_34px_-24px_rgba(36,53,43,0.45)]"
>
<span className="block overflow-hidden rounded-lg">
<ContentImage
@@ -53,8 +53,8 @@ export function ProductTeaserLink({
/>
</span>
<span className="flex-1">
<span className="block font-serif text-xl text-forest">{name}</span>
<span className="mt-1 inline-flex items-center gap-1.5 text-sm text-ink/70 transition-colors group-hover:text-forest">
<span className="block font-serif text-xl text-heading">{name}</span>
<span className="mt-1 inline-flex items-center gap-1.5 text-sm text-ink/70 transition-colors group-hover:text-heading">
{t.product.viewProduct.replace(/\s*→\s*$/, "")}
<svg
aria-hidden="true"
@@ -101,7 +101,7 @@ export function TasteScale({
<span
key={i}
className={`relative h-1.5 w-6 overflow-hidden rounded-full ${
tone === "inverted" ? "bg-cream/15" : "bg-ink/10"
tone === "inverted" ? "bg-surface/15" : "bg-ink/10"
}`}
>
{i < value ? (
@@ -295,14 +295,14 @@ export function ProductCard({
{t.product.outOfStock}
</span>
) : stock <= 5 ? (
<span className="absolute top-3 left-3 bg-honey-dark text-cream text-[10px] tracking-wide-caps px-2.5 py-1">
<span className="absolute top-3 left-3 bg-honey-deep text-cream text-[10px] tracking-wide-caps px-2.5 py-1">
{t.product.onlyLeft(stock)}
</span>
) : null}
</Link>
<div className="mt-5 flex flex-1 flex-col">
<Link href={`/honig/${product.slug}`} className="self-start">
<h3 className="link-draw text-2xl leading-tight text-forest">{product.name}</h3>
<h3 className="link-draw text-2xl leading-tight text-heading">{product.name}</h3>
</Link>
<p className="text-sm text-ink/70 mt-1">
{product.tasteTags.join(" · ")}
@@ -319,7 +319,7 @@ export function ProductCard({
</p>
<div className="mt-auto flex items-baseline justify-between pt-3">
<span className="text-sm text-ink/65 tabular-nums">{product.weightGrams} g</span>
<span className="font-serif text-2xl text-forest tabular-nums">
<span className="font-serif text-2xl text-heading tabular-nums">
{formatPrice(product.priceCents, product.currency, locale)}
</span>
</div>
@@ -331,7 +331,7 @@ export function ProductCard({
) : null}
<Link
href={`/honig/${product.slug}`}
className="group/more mt-3 inline-flex items-center gap-2 self-start py-1 text-sm tracking-wide-caps text-forest"
className="group/more mt-3 inline-flex items-center gap-2 self-start py-1 text-sm tracking-wide-caps text-heading"
>
<span className="link-draw pb-0.5">
{product.category === "HONIG" ? t.product.toHoney : t.product.toProduct}
+1 -1
View File
@@ -100,7 +100,7 @@ export function ProductGallery({ images, productName, description }: ProductGall
aria-label={`${isImageSlide(slide) ? `Produktbild ${index + 1}` : slide.label} – Bild ${index + 1} von ${slides.length} anzeigen`}
aria-current={index === activeIndex}
className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-colors ${
index === activeIndex ? "border-forest" : "border-transparent hover:border-ink/20"
index === activeIndex ? "border-heading" : "border-transparent hover:border-ink/20"
}`}
>
{isImageSlide(slide) ? (
+1 -1
View File
@@ -22,7 +22,7 @@ export function QuantityStepper({
size?: "sm" | "md";
}) {
const buttonSize = size === "sm" ? "w-8 h-8" : "w-10 h-10";
const buttonClass = `${buttonSize} flex items-center justify-center text-ink/70 transition-[color,background-color,transform] duration-200 hover:bg-honey/10 hover:text-forest active:scale-90 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-ink/70 disabled:active:scale-100`;
const buttonClass = `${buttonSize} flex items-center justify-center text-ink/70 transition-[color,background-color,transform] duration-200 hover:bg-honey/10 hover:text-heading active:scale-90 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-ink/70 disabled:active:scale-100`;
return (
<div className="inline-flex items-stretch border border-ink/20 bg-background">
+1 -1
View File
@@ -43,7 +43,7 @@ export function RecentlyViewedSection({ excludeProductId }: { excludeProductId?:
return (
<div className="mt-20">
<h2 className="text-2xl text-forest mb-8">{t.productPage.recentlyViewedHeading}</h2>
<h2 className="text-2xl text-heading mb-8">{t.productPage.recentlyViewedHeading}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
+4 -4
View File
@@ -24,7 +24,7 @@ const STATUS_FILL: Record<DhlTrackingStatus, string> = {
const STATUS_TEXT: Record<DhlTrackingStatus, string> = {
"pre-transit": "text-ink/70",
transit: "text-honey-dark",
delivered: "text-forest",
delivered: "text-heading",
failure: "text-red-700",
unknown: "text-ink/70",
};
@@ -50,7 +50,7 @@ export function ShipmentTrackingHistory({
if (result.kind !== "ok") {
return (
<section className="mt-5 border-t border-ink/10 pt-5">
<h3 className="text-sm text-forest">{labels.heading}</h3>
<h3 className="text-sm text-heading">{labels.heading}</h3>
<p className="mt-2 text-xs text-ink/65 leading-relaxed">
{result.kind === "not-found" ? labels.notYetScanned : labels.unavailable}
</p>
@@ -62,7 +62,7 @@ export function ShipmentTrackingHistory({
return (
<section className="mt-5 border-t border-ink/10 pt-5" aria-label={labels.ariaLabel}>
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h3 className="text-sm text-forest">{labels.heading}</h3>
<h3 className="text-sm text-heading">{labels.heading}</h3>
<span className={`text-xs ${STATUS_TEXT[info.status]}`}>{labels.statuses[info.status]}</span>
</div>
@@ -99,7 +99,7 @@ export function ShipmentTrackingHistory({
strokeWidth="2"
/>
</svg>
<p className={`text-sm ${i === 0 ? "font-serif text-lg text-forest" : "text-ink/70"}`}>{event.description}</p>
<p className={`text-sm ${i === 0 ? "font-serif text-lg text-heading" : "text-ink/70"}`}>{event.description}</p>
<p className="mt-0.5 text-xs text-ink/55">
<time dateTime={event.timestamp.toISOString()}>{formatDateTime(event.timestamp, locale)}</time>
{event.location ? ` · ${event.location}` : null}
+24 -16
View File
@@ -17,6 +17,8 @@ import { useI18n } from "@/i18n/i18n-provider";
import { CartFlight, onCartLanded } from "@/components/cart-flight";
import { HexMark } from "@/components/honeycomb";
import type { Dictionary } from "@/i18n/dictionary-type";
import { ThemeToggle } from "@/components/theme-toggle";
import type { ThemeChoice } from "@/lib/theme";
const FOCUSABLE_SELECTOR =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
@@ -81,10 +83,10 @@ function Logo({ t, companyName, logoSrc }: { t: Dictionary; companyName: string;
{/* Kopfzeile ist bei 1280-1440 px knapp: breite Logos hier enger begrenzt. */}
<Logomark className="h-9 w-9 max-w-14 flex-shrink-0 sm:max-w-28" src={logoSrc} />
<span className="flex flex-col leading-none">
<span className="text-2xl tracking-[0.2em] text-forest font-serif">
<span className="text-2xl tracking-[0.2em] text-heading font-serif">
{companyName}
</span>
<span className="hidden 2xl:block text-[10px] tracking-[0.3em] uppercase text-honey-dark mt-1 whitespace-nowrap">
<span className="hidden min-[1680px]:block text-[10px] tracking-[0.3em] uppercase text-honey-dark mt-1 whitespace-nowrap">
{t.footer.tagline}
</span>
</span>
@@ -166,7 +168,7 @@ function SearchButton() {
autoFocus
autoComplete="off"
placeholder={t.nav.searchPlaceholder}
className="w-full border-b border-forest bg-transparent px-2 py-1.5 text-sm focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
className="w-full border-b border-heading bg-transparent px-2 py-1.5 text-sm focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
/>
{visibleSuggestions.length > 0 ? (
<motion.ul
@@ -214,7 +216,7 @@ function SearchButton() {
type="button"
onClick={() => (open ? closeSearch() : setOpen(true))}
aria-label={t.nav.openSearch}
className="flex items-center justify-center w-10 h-10 text-forest flex-shrink-0"
className="flex items-center justify-center w-10 h-10 text-heading flex-shrink-0"
>
<AnimatePresence mode="wait" initial={false}>
<motion.svg
@@ -253,7 +255,7 @@ function HighlightMatch({ text, query }: { text: string; query: string }) {
return (
<>
{text.slice(0, index)}
<mark className="bg-honey-pale/70 text-forest">{text.slice(index, index + needle.length)}</mark>
<mark className="bg-honey-pale/70 text-heading">{text.slice(index, index + needle.length)}</mark>
{text.slice(index + needle.length)}
</>
);
@@ -265,7 +267,7 @@ function AccountIconButton() {
<Link
href="/konto"
aria-label={t.nav.account}
className="flex items-center justify-center w-10 h-10 text-forest"
className="flex items-center justify-center w-10 h-10 text-heading"
>
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="8" r="4" />
@@ -301,7 +303,7 @@ function CartIconButton() {
onClick={openCart}
aria-label={t.nav.openCart(count)}
data-cart-target=""
className="relative flex items-center justify-center w-10 h-10 text-forest transition-colors hover:text-honey-dark"
className="relative flex items-center justify-center w-10 h-10 text-heading transition-colors hover:text-honey-dark"
>
<motion.svg
animate={basketControls}
@@ -326,7 +328,7 @@ function CartIconButton() {
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.4, opacity: 0 }}
transition={{ type: "spring", stiffness: 520, damping: 18 }}
className="absolute -top-1 -right-1 bg-honey-dark text-cream text-[10px] min-w-4 h-4 px-1 rounded-full flex items-center justify-center tabular-nums"
className="absolute -top-1 -right-1 bg-honey-deep text-cream text-[10px] min-w-4 h-4 px-1 rounded-full flex items-center justify-center tabular-nums"
>
{count}
</motion.span>
@@ -380,10 +382,12 @@ export function Header({
companyName,
logoSrc = null,
logoInvertedSrc = null,
themeChoice = "system",
}: {
companyName: string;
logoSrc?: string | null;
logoInvertedSrc?: string | null;
themeChoice?: ThemeChoice;
}) {
const { t } = useI18n();
const pathname = usePathname();
@@ -430,7 +434,7 @@ export function Header({
href={item.href}
aria-current={active ? "page" : undefined}
className={`link-draw pb-1 text-sm whitespace-nowrap transition-colors ${
active ? "text-forest" : "text-ink/80 hover:text-forest"
active ? "text-heading" : "text-ink/80 hover:text-heading"
}`}
>
{t.nav[item.key]}
@@ -440,13 +444,16 @@ export function Header({
</nav>
<div className="flex items-center gap-2">
<div className="hidden md:block">
{/* Zwischen xl und 2xl steht "Shop" schon in der Navigation - dort waere der
Knopf eine Dopplung und nahm der knappen Kopfzeile den Platz. */}
<div className="hidden md:block xl:hidden 2xl:block">
<LinkButton href="/shop" variant="primary" className="whitespace-nowrap">
{t.nav.discoverHoney}
</LinkButton>
</div>
<div className="hidden xl:flex items-center gap-1">
<LanguageSwitcher />
<ThemeToggle initial={themeChoice} className="text-ink/70 hover:text-heading" />
<SearchButton />
<AccountIconButton />
</div>
@@ -455,7 +462,7 @@ export function Header({
<button
ref={menuButtonRef}
aria-label={t.nav.openMenu}
className="xl:hidden flex items-center justify-center w-10 h-10 text-forest"
className="xl:hidden flex items-center justify-center w-10 h-10 text-heading"
onClick={() => setMobileOpen(true)}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
@@ -494,6 +501,7 @@ export function Header({
</span>
<div className="flex items-center gap-4">
<LanguageSwitcher className="!text-cream/70" />
<ThemeToggle initial={themeChoice} className="text-cream/70 hover:text-cream" />
<button
aria-label={t.nav.closeMenu}
className="w-10 h-10 flex items-center justify-center"
@@ -625,7 +633,7 @@ export function CartDrawer() {
className="fixed right-0 top-0 h-full w-full sm:w-[420px] bg-background z-50 flex flex-col shadow-[-24px_0_60px_-30px_rgba(36,53,43,0.45)] outline-none"
>
<div className="flex items-center justify-between px-6 h-20 border-b border-ink/10">
<h2 id="cart-drawer-title" className="flex items-baseline gap-2 text-2xl font-serif text-forest">
<h2 id="cart-drawer-title" className="flex items-baseline gap-2 text-2xl font-serif text-heading">
{t.cart.title}
{items.length > 0 ? (
<span className="font-sans text-xs text-ink/55 tabular-nums">({cartItemCount(items)})</span>
@@ -634,7 +642,7 @@ export function CartDrawer() {
<button
aria-label={t.cart.close}
onClick={closeCart}
className="flex w-9 h-9 items-center justify-center text-ink/70 transition-[color,transform] duration-300 hover:rotate-90 hover:text-forest"
className="flex w-9 h-9 items-center justify-center text-ink/70 transition-[color,transform] duration-300 hover:rotate-90 hover:text-heading"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M6 6l12 12M18 6L6 18" />
@@ -655,7 +663,7 @@ export function CartDrawer() {
strokeDasharray="6 5"
/>
</svg>
<p className="mt-6 font-serif text-2xl text-forest">{t.cart.empty}</p>
<p className="mt-6 font-serif text-2xl text-heading">{t.cart.empty}</p>
<LinkButton href="/shop" variant="secondary" onClick={closeCart} className="mt-8">
{t.nav.discoverHoney}
</LinkButton>
@@ -676,7 +684,7 @@ export function CartDrawer() {
>
<CartItemThumbnail image={item.image} className="w-16 h-16 flex-shrink-0 rounded-lg" />
<div className="flex-1">
<p className="font-serif text-lg leading-tight text-forest">{item.name}</p>
<p className="font-serif text-lg leading-tight text-heading">{item.name}</p>
<p className="text-xs text-ink/65">{item.weightGrams} g</p>
{/* Live-Bestand trifft erst NACH dem ersten Rendern ein (siehe
use-cart-stock.ts) — der Wrapper bleibt beim Uebergang von
@@ -726,7 +734,7 @@ export function CartDrawer() {
key={total}
initial={{ opacity: 0.4, y: -4 }}
animate={{ opacity: 1, y: 0 }}
className="font-serif text-2xl text-forest tabular-nums"
className="font-serif text-2xl text-heading tabular-nums"
>
{formatPrice(total, "EUR", locale)}
</motion.span>
+1 -1
View File
@@ -38,7 +38,7 @@ export function StickyAddToCartBar({
<div className="flex items-center gap-4">
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-ink">{product.name}</p>
<p className="text-sm text-forest tabular-nums">{formatPrice(product.priceCents, product.currency, locale)}</p>
<p className="text-sm text-heading tabular-nums">{formatPrice(product.priceCents, product.currency, locale)}</p>
</div>
{inStock ? (
<Button
+74
View File
@@ -0,0 +1,74 @@
"use client";
import { useSyncExternalStore } from "react";
import { useI18n } from "@/i18n/i18n-provider";
import {
THEME_COOKIE,
THEME_COOKIE_MAX_AGE,
nextThemeChoice,
parseThemeChoice,
type ThemeChoice,
} from "@/lib/theme";
const THEME_EVENT = "wh-theme-change";
function readChoice(): ThemeChoice {
return parseThemeChoice(document.documentElement.dataset.theme);
}
function subscribe(onChange: () => void): () => void {
window.addEventListener(THEME_EVENT, onChange);
return () => window.removeEventListener(THEME_EVENT, onChange);
}
function applyChoice(choice: ThemeChoice): void {
const root = document.documentElement;
const secure = window.location.protocol === "https:" ? "; secure" : "";
if (choice === "system") {
delete root.dataset.theme;
document.cookie = `${THEME_COOKIE}=; path=/; max-age=0; samesite=lax${secure}`;
} else {
root.dataset.theme = choice;
document.cookie = `${THEME_COOKIE}=${choice}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; samesite=lax${secure}`;
}
// Alle Umschalter (Kopfzeile, Mobilmenue, Admin-Leiste) bleiben synchron.
window.dispatchEvent(new Event(THEME_EVENT));
}
/**
* Umschalter Automatisch -> Hell -> Dunkel. Das Symbol zeigt die aktuelle
* Wahl; Name und Tooltip sagen, was ein Klick bewirkt. "initial" kommt vom
* Server (Cookie), damit der erste Render mit dem Dokument uebereinstimmt.
*/
export function ThemeToggle({ initial, className = "" }: { initial: ThemeChoice; className?: string }) {
const { t } = useI18n();
const choice = useSyncExternalStore(subscribe, readChoice, () => initial);
const next = nextThemeChoice(choice);
const label = t.theme.toggleLabel(t.theme[choice], t.theme[next]);
return (
<button
type="button"
onClick={() => applyChoice(next)}
aria-label={label}
title={label}
className={`inline-flex h-10 w-10 items-center justify-center transition-colors duration-300 ${className}`}
>
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
{choice === "light" ? (
<>
<circle cx="12" cy="12" r="4" />
<path d="M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4" strokeLinecap="round" />
</>
) : choice === "dark" ? (
<path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z" strokeLinejoin="round" />
) : (
<>
<circle cx="12" cy="12" r="8" />
<path d="M12 4a8 8 0 0 1 0 16Z" fill="currentColor" stroke="none" />
</>
)}
</svg>
</button>
);
}
+1 -1
View File
@@ -61,7 +61,7 @@ export function TrustBadges({ className = "" }: { className?: string }) {
return (
<li key={badge.label} className="flex items-center gap-2 text-xs text-ink/70">
{badge.href ? (
<Link href={badge.href} className="flex items-center gap-2 hover:text-forest">
<Link href={badge.href} className="flex items-center gap-2 hover:text-heading">
{content}
</Link>
) : (
+4 -4
View File
@@ -28,14 +28,14 @@ const fillBase =
"bg-no-repeat bg-bottom bg-[length:100%_0%] hover:bg-[length:100%_100%] focus-visible:bg-[length:100%_100%] motion-reduce:transition-colors";
const buttonStyles: Record<ButtonVariant, string> = {
primary: `${fillBase} bg-forest bg-[linear-gradient(var(--color-honey-dark),var(--color-honey-dark))] text-cream border border-forest hover:border-honey-dark`,
secondary: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-forest),var(--color-forest))] text-forest border border-forest hover:text-cream focus-visible:text-cream`,
primary: `${fillBase} bg-forest bg-[linear-gradient(var(--color-honey-deep),var(--color-honey-deep))] text-cream border border-heading hover:border-honey-dark`,
secondary: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-forest),var(--color-forest))] text-heading border border-heading hover:text-cream focus-visible:text-cream`,
ghost: "bg-transparent text-ink border border-ink/20 hover:border-ink hover:bg-ink/[0.03]",
// Fuer dunkle Flaechen (Hero, Waldgruen-Abschnitte, Menue): Creme-Rahmen,
// fuellt sich mit Creme, Text wechselt auf Waldgruen.
inverted: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-cream hover:text-forest focus-visible:text-forest`,
// Hauptaktion auf dunklen Flaechen, wo Waldgruen im Grund verschwinden wuerde.
accent: `${fillBase} bg-honey-dark bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-honey-dark hover:text-forest hover:border-cream focus-visible:text-forest`,
accent: `${fillBase} bg-honey-deep bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-honey-dark hover:text-forest hover:border-cream focus-visible:text-forest`,
};
const buttonBase =
@@ -108,7 +108,7 @@ export function SectionHeading({
Ueberschrift traegt den Abschnitt allein. */}
<Heading
className={`text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] ${
tone === "inverted" ? "text-cream" : "text-forest"
tone === "inverted" ? "text-cream" : "text-heading"
}`}
>
{title}