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

+2 -2
View File
@@ -64,7 +64,7 @@ export default async function AdminInquiriesPage(props: PageProps<"/admin/anfrag
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-start justify-between gap-4">
<div> <div>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<p className="text-forest">{inquiry.companyName}</p> <p className="text-heading">{inquiry.companyName}</p>
<AdminBadge tone={STATUS_TONES[inquiry.status] ?? "neutral"} pulse={inquiry.status === "NEU"}> <AdminBadge tone={STATUS_TONES[inquiry.status] ?? "neutral"} pulse={inquiry.status === "NEU"}>
{statusLabels[inquiry.status] ?? inquiry.status} {statusLabels[inquiry.status] ?? inquiry.status}
</AdminBadge> </AdminBadge>
@@ -91,7 +91,7 @@ export default async function AdminInquiriesPage(props: PageProps<"/admin/anfrag
</select> </select>
<button <button
type="submit" type="submit"
className="border border-forest px-3 py-1.5 text-xs text-forest transition-colors duration-300 hover:bg-forest hover:text-cream" className="border border-heading px-3 py-1.5 text-xs text-heading transition-colors duration-300 hover:bg-forest hover:text-cream"
> >
{t.adminInquiriesPage.save} {t.adminInquiriesPage.save}
</button> </button>
+1 -1
View File
@@ -35,7 +35,7 @@ export default async function AdminAuditLogPage() {
<tr key={entry.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={entry.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 whitespace-nowrap tabular-nums text-ink/70">{formatDateTime(entry.createdAt)}</td> <td className="p-4 whitespace-nowrap tabular-nums text-ink/70">{formatDateTime(entry.createdAt)}</td>
<td className="p-4 whitespace-nowrap">{entry.adminEmail}</td> <td className="p-4 whitespace-nowrap">{entry.adminEmail}</td>
<td className="p-4 whitespace-nowrap font-mono text-xs text-forest">{entry.action}</td> <td className="p-4 whitespace-nowrap font-mono text-xs text-heading">{entry.action}</td>
<td className="p-4 whitespace-nowrap text-ink/70"> <td className="p-4 whitespace-nowrap text-ink/70">
{entry.entityType} {entry.entityType}
{entry.entityId ? <span className="text-ink/65"> #{entry.entityId.slice(-6)}</span> : null} {entry.entityId ? <span className="text-ink/65"> #{entry.entityId.slice(-6)}</span> : null}
+2 -2
View File
@@ -34,7 +34,7 @@ export default async function AdminBenutzerEditPage(props: PageProps<"/admin/ben
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/benutzer" label={tu.backToUsers} /> <BackLink href="/admin/benutzer" label={tu.backToUsers} />
<h1 className="text-2xl text-forest mb-2"> <h1 className="text-2xl text-heading mb-2">
{customer.firstName} {customer.lastName} {customer.firstName} {customer.lastName}
</h1> </h1>
<p className="text-sm text-ink/65 mb-8">{tu.createdOn(formatDate(customer.createdAt))}</p> <p className="text-sm text-ink/65 mb-8">{tu.createdOn(formatDate(customer.createdAt))}</p>
@@ -61,7 +61,7 @@ export default async function AdminBenutzerEditPage(props: PageProps<"/admin/ben
</AdminFormPanel> </AdminFormPanel>
<section className="mt-8 space-y-6 border border-ink/10 bg-background p-6 sm:p-8"> <section className="mt-8 space-y-6 border border-ink/10 bg-background p-6 sm:p-8">
<h2 className="font-serif text-xl text-forest">{tu.securityHeading}</h2> <h2 className="font-serif text-xl text-heading">{tu.securityHeading}</h2>
<dl className="divide-y divide-ink/10 text-sm"> <dl className="divide-y divide-ink/10 text-sm">
<div className="flex items-center justify-between gap-4 py-3"> <div className="flex items-center justify-between gap-4 py-3">
<dt className="text-ink/70">{tu.twoFactorLabel}</dt> <dt className="text-ink/70">{tu.twoFactorLabel}</dt>
+1 -1
View File
@@ -12,7 +12,7 @@ export default async function AdminBenutzerNewPage() {
return ( return (
<div className="max-w-xl"> <div className="max-w-xl">
<BackLink href="/admin/benutzer" label={tu.backToUsers} /> <BackLink href="/admin/benutzer" label={tu.backToUsers} />
<h1 className="text-2xl text-forest mb-2">{tu.title}</h1> <h1 className="text-2xl text-heading mb-2">{tu.title}</h1>
<p className="text-sm text-ink/70 mb-8">{tu.intro}</p> <p className="text-sm text-ink/70 mb-8">{tu.intro}</p>
<AdminFormPanel action={createCustomerAction} actions={<Button type="submit">{tu.createAndSend}</Button>}> <AdminFormPanel action={createCustomerAction} actions={<Button type="submit">{tu.createAndSend}</Button>}>
+1 -1
View File
@@ -76,7 +76,7 @@ export default async function AdminBenutzerPage(props: PageProps<"/admin/benutze
return ( return (
<tr key={customer.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={customer.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest"> <td className="p-4 text-heading">
{customer.firstName} {customer.lastName} {customer.firstName} {customer.lastName}
{isSelf ? <span className="text-ink/65">{t.adminUsersPage.youSuffix}</span> : null} {isSelf ? <span className="text-ink/65">{t.adminUsersPage.youSuffix}</span> : null}
</td> </td>
+5 -5
View File
@@ -52,7 +52,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
<div className="flex flex-wrap items-start justify-between gap-4 mb-8"> <div className="flex flex-wrap items-start justify-between gap-4 mb-8">
<div> <div>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<h1 className="text-2xl text-forest">{order.orderNumber}</h1> <h1 className="text-2xl text-heading">{order.orderNumber}</h1>
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} /> <OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
</div> </div>
<p className="text-sm text-ink/65 mt-1">{formatDate(order.createdAt)}</p> <p className="text-sm text-ink/65 mt-1">{formatDate(order.createdAt)}</p>
@@ -161,7 +161,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
) : dhlConfigured ? ( ) : dhlConfigured ? (
<form action={createDhlLabelAction}> <form action={createDhlLabelAction}>
<input type="hidden" name="orderId" value={order.id} /> <input type="hidden" name="orderId" value={order.id} />
<button type="submit" className="bg-honey-dark text-cream px-4 py-2 text-sm"> <button type="submit" className="bg-honey-deep text-cream px-4 py-2 text-sm">
{t.adminOrderDetailPage.dhlCreateButton} {t.adminOrderDetailPage.dhlCreateButton}
</button> </button>
</form> </form>
@@ -215,7 +215,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
{order.items.map((item) => ( {order.items.map((item) => (
<tr key={item.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={item.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="py-2"> <td className="py-2">
<Link href={`/admin/produkte/${item.productId}`} className="text-forest underline"> <Link href={`/admin/produkte/${item.productId}`} className="text-heading underline">
{item.productName} {item.productName}
</Link>{" "} </Link>{" "}
<span className="text-ink/65">({item.weightGrams} g)</span> <span className="text-ink/65">({item.weightGrams} g)</span>
@@ -249,7 +249,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
<span>−{formatPrice(order.discountCents, order.currency)}</span> <span>−{formatPrice(order.discountCents, order.currency)}</span>
</div> </div>
) : null} ) : null}
<div className="flex justify-between text-forest pt-1 border-t border-ink/10"> <div className="flex justify-between text-heading pt-1 border-t border-ink/10">
<span>{t.adminOrderDetailPage.totalLabel}</span> <span>{t.adminOrderDetailPage.totalLabel}</span>
<span>{formatPrice(order.totalCents, order.currency)}</span> <span>{formatPrice(order.totalCents, order.currency)}</span>
</div> </div>
@@ -282,7 +282,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
<span>{order.invoice.invoiceNumber}</span> <span>{order.invoice.invoiceNumber}</span>
<a <a
href={`/rechnung/${order.invoice.invoiceNumber}/pdf`} href={`/rechnung/${order.invoice.invoiceNumber}/pdf`}
className="link-draw text-sm text-forest" className="link-draw text-sm text-heading"
> >
{t.adminOrderDetailPage.downloadPdf} {t.adminOrderDetailPage.downloadPdf}
</a> </a>
+2 -2
View File
@@ -112,7 +112,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
<tbody className="divide-y divide-ink/10"> <tbody className="divide-y divide-ink/10">
{orders.map((order) => ( {orders.map((order) => (
<tr key={order.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={order.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest whitespace-nowrap"> <td className="p-4 text-heading whitespace-nowrap">
<Link href={`/admin/bestellungen/${order.id}`} className="link-draw"> <Link href={`/admin/bestellungen/${order.id}`} className="link-draw">
{order.orderNumber} {order.orderNumber}
</Link> </Link>
@@ -155,7 +155,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
</select> </select>
<button <button
type="submit" type="submit"
className="border border-forest px-3 py-1.5 text-xs text-forest transition-colors duration-300 hover:bg-forest hover:text-cream" className="border border-heading px-3 py-1.5 text-xs text-heading transition-colors duration-300 hover:bg-forest hover:text-cream"
> >
{t.adminOrdersPage.save} {t.adminOrdersPage.save}
</button> </button>
+1 -1
View File
@@ -71,7 +71,7 @@ export default async function AdminBewertungenPage(props: PageProps<"/admin/bewe
{reviews.map((review) => ( {reviews.map((review) => (
<tr key={review.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={review.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4"> <td className="p-4">
<Link href={`/honig/${review.product.slug}`} className="link-draw text-forest"> <Link href={`/honig/${review.product.slug}`} className="link-draw text-heading">
{review.product.name} {review.product.name}
</Link> </Link>
</td> </td>
+1 -1
View File
@@ -20,7 +20,7 @@ export default async function AdminBatchEditPage(props: PageProps<"/admin/charge
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/chargen" label={tb.backToBatches} /> <BackLink href="/admin/chargen" label={tb.backToBatches} />
<h1 className="text-2xl text-forest mb-2">{tb.title}</h1> <h1 className="text-2xl text-heading mb-2">{tb.title}</h1>
<p className="text-sm text-ink/65 mb-8"> <p className="text-sm text-ink/65 mb-8">
{batch.code} · {batch.harvest.type} {batch.harvest.year} {batch.code} · {batch.harvest.type} {batch.harvest.year}
</p> </p>
+2 -2
View File
@@ -46,7 +46,7 @@ export default async function AdminBatchesPage() {
<tbody className="divide-y divide-ink/10"> <tbody className="divide-y divide-ink/10">
{batches.map((batch) => ( {batches.map((batch) => (
<tr key={batch.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={batch.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest">{batch.code}</td> <td className="p-4 text-heading">{batch.code}</td>
<td className="p-4"> <td className="p-4">
{batch.harvest.type} {batch.harvest.year} {batch.harvest.type} {batch.harvest.year}
</td> </td>
@@ -81,7 +81,7 @@ export default async function AdminBatchesPage() {
</div> </div>
<div className="mt-10 bg-background border border-ink/10 p-6 max-w-xl"> <div className="mt-10 bg-background border border-ink/10 p-6 max-w-xl">
<h2 className="font-serif text-xl text-forest mb-4">{t.adminBatchesPage.newBatchHeading}</h2> <h2 className="font-serif text-xl text-heading mb-4">{t.adminBatchesPage.newBatchHeading}</h2>
<form action={createBatchAction} className="space-y-4"> <form action={createBatchAction} className="space-y-4">
<div className="grid grid-cols-2 items-end gap-4"> <div className="grid grid-cols-2 items-end gap-4">
<AdminField <AdminField
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernt
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/ernten" label={th.backToHarvests} /> <BackLink href="/admin/ernten" label={th.backToHarvests} />
<h1 className="text-2xl text-forest mb-8">{th.title}</h1> <h1 className="text-2xl text-heading mb-8">{th.title}</h1>
{harvest.isPublished ? null : ( {harvest.isPublished ? null : (
<div className="mb-6 flex flex-col justify-between gap-3 border border-honey/40 bg-honey/5 px-4 py-3 sm:flex-row sm:items-center"> <div className="mb-6 flex flex-col justify-between gap-3 border border-honey/40 bg-honey/5 px-4 py-3 sm:flex-row sm:items-center">
+2 -2
View File
@@ -47,7 +47,7 @@ export default async function AdminHarvestsPage() {
<tbody className="divide-y divide-ink/10"> <tbody className="divide-y divide-ink/10">
{harvests.map((harvest) => ( {harvests.map((harvest) => (
<tr key={harvest.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={harvest.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest">{harvest.type}</td> <td className="p-4 text-heading">{harvest.type}</td>
<td className="p-4 tabular-nums">{harvest.year}</td> <td className="p-4 tabular-nums">{harvest.year}</td>
<td className="p-4 whitespace-nowrap tabular-nums">{formatDate(harvest.harvestDate)}</td> <td className="p-4 whitespace-nowrap tabular-nums">{formatDate(harvest.harvestDate)}</td>
<td className="p-4">{harvest.location}</td> <td className="p-4">{harvest.location}</td>
@@ -93,7 +93,7 @@ export default async function AdminHarvestsPage() {
<div className="mt-10 max-w-xl"> <div className="mt-10 max-w-xl">
<div className="bg-background border border-ink/10 p-6"> <div className="bg-background border border-ink/10 p-6">
<h2 className="font-serif text-xl text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2> <h2 className="font-serif text-xl text-heading mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
<form action={createHarvestAction} className="space-y-4"> <form action={createHarvestAction} className="space-y-4">
<div className="grid grid-cols-2 items-end gap-4"> <div className="grid grid-cols-2 items-end gap-4">
<AdminField name="type" label={t.adminHarvestsPage.typeLabel} required /> <AdminField name="type" label={t.adminHarvestsPage.typeLabel} required />
+2 -2
View File
@@ -21,13 +21,13 @@ export default function AdminError({
return ( return (
<div className="py-20 text-center"> <div className="py-20 text-center">
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.errorPage.eyebrow}</p> <p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.errorPage.eyebrow}</p>
<h1 className="text-2xl text-forest">{t.errorPage.title}</h1> <h1 className="text-2xl text-heading">{t.errorPage.title}</h1>
<p className="mt-3 text-ink/70">{t.errorPage.description}</p> <p className="mt-3 text-ink/70">{t.errorPage.description}</p>
<div className="mt-6 flex items-center justify-center gap-4"> <div className="mt-6 flex items-center justify-center gap-4">
<Button type="button" onClick={() => reset()}> <Button type="button" onClick={() => reset()}>
{t.errorPage.retry} {t.errorPage.retry}
</Button> </Button>
<Link href="/admin" className="text-sm text-forest underline"> <Link href="/admin" className="text-sm text-heading underline">
{t.adminVouchersPage.backToDashboard} {t.adminVouchersPage.backToDashboard}
</Link> </Link>
</div> </div>
+1 -1
View File
@@ -28,7 +28,7 @@ export default async function AdminGutscheinEditPage(props: PageProps<"/admin/gu
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/gutscheine" label={tv.backToVouchers} /> <BackLink href="/admin/gutscheine" label={tv.backToVouchers} />
<h1 className="text-2xl text-forest mb-2">{tv.title}</h1> <h1 className="text-2xl text-heading mb-2">{tv.title}</h1>
<p className="text-sm text-ink/65 mb-8">{tv.redeemedSoFar(code.usedCount, code.maxUses)}</p> <p className="text-sm text-ink/65 mb-8">{tv.redeemedSoFar(code.usedCount, code.maxUses)}</p>
<AdminFormPanel action={updateDiscountCodeAction} actions={<Button type="submit">{tv.save}</Button>}> <AdminFormPanel action={updateDiscountCodeAction} actions={<Button type="submit">{tv.save}</Button>}>
+1 -1
View File
@@ -18,7 +18,7 @@ export default async function AdminGutscheinNewPage() {
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/gutscheine" label={tv.backToVouchers} /> <BackLink href="/admin/gutscheine" label={tv.backToVouchers} />
<h1 className="text-2xl text-forest mb-8">{tv.title}</h1> <h1 className="text-2xl text-heading mb-8">{tv.title}</h1>
<AdminFormPanel action={createDiscountCodeAction} actions={<Button type="submit">{tv.create}</Button>}> <AdminFormPanel action={createDiscountCodeAction} actions={<Button type="submit">{tv.create}</Button>}>
<AdminFieldset legend={tv.sectionDiscount}> <AdminFieldset legend={tv.sectionDiscount}>
+1 -1
View File
@@ -42,7 +42,7 @@ export default async function AdminGutscheinePage() {
<tbody className="divide-y divide-ink/10"> <tbody className="divide-y divide-ink/10">
{codes.map((code) => ( {codes.map((code) => (
<tr key={code.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={code.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest font-mono">{code.code}</td> <td className="p-4 text-heading font-mono">{code.code}</td>
<td className="p-4"> <td className="p-4">
{code.type === "PERCENT" ? `${code.value} %` : formatPrice(code.value)} {code.type === "PERCENT" ? `${code.value} %` : formatPrice(code.value)}
</td> </td>
+11 -11
View File
@@ -72,7 +72,7 @@ function FieldEditor({
<select <select
name={def.key} name={def.key}
defaultValue={current} defaultValue={current}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
> >
{(def.options ?? []).map((option) => ( {(def.options ?? []).map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -92,7 +92,7 @@ function FieldEditor({
rows={rows} rows={rows}
defaultValue={row?.valueDe ?? ""} defaultValue={row?.valueDe ?? ""}
placeholder={def.getDefault("de")} placeholder={def.getDefault("de")}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
/> />
) : ( ) : (
<input <input
@@ -100,7 +100,7 @@ function FieldEditor({
name={def.key} name={def.key}
defaultValue={row?.valueDe ?? ""} defaultValue={row?.valueDe ?? ""}
placeholder={def.getDefault("de")} placeholder={def.getDefault("de")}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
/> />
)} )}
</label> </label>
@@ -120,7 +120,7 @@ function FieldEditor({
rows={rows} rows={rows}
defaultValue={row?.valueDe ?? ""} defaultValue={row?.valueDe ?? ""}
placeholder={def.getDefault("de")} placeholder={def.getDefault("de")}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
/> />
) : ( ) : (
<input <input
@@ -128,7 +128,7 @@ function FieldEditor({
name={`de::${def.key}`} name={`de::${def.key}`}
defaultValue={row?.valueDe ?? ""} defaultValue={row?.valueDe ?? ""}
placeholder={def.getDefault("de")} placeholder={def.getDefault("de")}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
/> />
)} )}
</label> </label>
@@ -140,7 +140,7 @@ function FieldEditor({
rows={rows} rows={rows}
defaultValue={row?.valueEn ?? ""} defaultValue={row?.valueEn ?? ""}
placeholder={def.getDefault("en")} placeholder={def.getDefault("en")}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
/> />
) : ( ) : (
<input <input
@@ -148,7 +148,7 @@ function FieldEditor({
name={`en::${def.key}`} name={`en::${def.key}`}
defaultValue={row?.valueEn ?? ""} defaultValue={row?.valueEn ?? ""}
placeholder={def.getDefault("en")} placeholder={def.getDefault("en")}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
/> />
)} )}
</label> </label>
@@ -185,7 +185,7 @@ function GroupForm({
className="mb-10 bg-background border border-ink/10 p-6 space-y-6" className="mb-10 bg-background border border-ink/10 p-6 space-y-6"
> >
<input type="hidden" name="group" value={groupKeyValue} /> <input type="hidden" name="group" value={groupKeyValue} />
<h3 className="text-lg text-forest">{groupHeading}</h3> <h3 className="text-lg text-heading">{groupHeading}</h3>
{fields.map((def) => ( {fields.map((def) => (
<FieldEditor key={def.key} def={def} row={current.get(def.key)} locale={locale} t={t} /> <FieldEditor key={def.key} def={def} row={current.get(def.key)} locale={locale} t={t} />
@@ -216,11 +216,11 @@ export default async function AdminInhaltePage() {
return ( return (
<div> <div>
<BackLink href="/admin" label={t.adminContentPage.backToDashboard} /> <BackLink href="/admin" label={t.adminContentPage.backToDashboard} />
<h1 className="text-2xl text-forest mb-2">{t.adminContentPage.title}</h1> <h1 className="text-2xl text-heading mb-2">{t.adminContentPage.title}</h1>
<p className="text-sm text-ink/65 mb-8 max-w-2xl">{t.adminContentPage.intro}</p> <p className="text-sm text-ink/65 mb-8 max-w-2xl">{t.adminContentPage.intro}</p>
<section className="mb-12"> <section className="mb-12">
<h2 className="text-xl text-forest mb-1">{t.adminContentPage.sectionImages}</h2> <h2 className="text-xl text-heading mb-1">{t.adminContentPage.sectionImages}</h2>
<p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionImagesIntro}</p> <p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionImagesIntro}</p>
{groups.map((group) => ( {groups.map((group) => (
<GroupForm <GroupForm
@@ -236,7 +236,7 @@ export default async function AdminInhaltePage() {
</section> </section>
<section> <section>
<h2 className="text-xl text-forest mb-1">{t.adminContentPage.sectionText}</h2> <h2 className="text-xl text-heading mb-1">{t.adminContentPage.sectionText}</h2>
<p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionTextIntro}</p> <p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionTextIntro}</p>
{groups.map((group) => ( {groups.map((group) => (
<GroupForm <GroupForm
+4
View File
@@ -8,6 +8,8 @@ import { AdminShell } from "@/components/admin-shell";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { getBrandAssets } from "@/lib/brand"; import { getBrandAssets } from "@/lib/brand";
import { cookies } from "next/headers";
import { THEME_COOKIE, parseThemeChoice } from "@/lib/theme";
// Admin-Daten (Bestellungen, Bestand) muessen immer aktuell sein; zudem // Admin-Daten (Bestellungen, Bestand) muessen immer aktuell sein; zudem
// entkoppelt das den Docker-Build-Schritt von einer DB-Verbindung. // entkoppelt das den Docker-Build-Schritt von einer DB-Verbindung.
@@ -49,6 +51,7 @@ export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
// abgeholte Bestellungen (keine offenen Zahlungen), neue Retouren, // abgeholte Bestellungen (keine offenen Zahlungen), neue Retouren,
// unfreigegebene Bewertungen, neue Firmenanfragen. Reine COUNT-Abfragen. // unfreigegebene Bewertungen, neue Firmenanfragen. Reine COUNT-Abfragen.
const brand = await getBrandAssets(); const brand = await getBrandAssets();
const themeChoice = parseThemeChoice((await cookies()).get(THEME_COOKIE)?.value);
const [openOrders, openReturns, pendingReviews, newInquiries] = await Promise.all([ const [openOrders, openReturns, pendingReviews, newInquiries] = await Promise.all([
prisma.order.count({ prisma.order.count({
where: { status: { in: ["BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "ABHOLBEREIT"] }, archivedAt: null }, where: { status: { in: ["BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "ABHOLBEREIT"] }, archivedAt: null },
@@ -62,6 +65,7 @@ export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
<AdminShell <AdminShell
adminName={`${admin.firstName} ${admin.lastName}`} adminName={`${admin.firstName} ${admin.lastName}`}
logoSrc={brand.logoInverted} logoSrc={brand.logoInverted}
themeChoice={themeChoice}
badges={{ badges={{
"/admin/bestellungen": openOrders, "/admin/bestellungen": openOrders,
"/admin/retouren": openReturns, "/admin/retouren": openReturns,
+16 -16
View File
@@ -303,7 +303,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
{/* Begruessung statt nackter Ueberschrift: der Tag beginnt hier. */} {/* Begruessung statt nackter Ueberschrift: der Tag beginnt hier. */}
<div className="mb-10"> <div className="mb-10">
<p className="text-sm text-ink/60">{todayLine}</p> <p className="text-sm text-ink/60">{todayLine}</p>
<h1 className="mt-1 text-4xl md:text-5xl leading-tight text-forest"> <h1 className="mt-1 text-4xl md:text-5xl leading-tight text-heading">
{t.adminDashboardPage.greeting(dayPart, admin?.firstName ?? "")} {t.adminDashboardPage.greeting(dayPart, admin?.firstName ?? "")}
</h1> </h1>
</div> </div>
@@ -396,23 +396,23 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
{/* Was jetzt ansteht — direkt anklickbar. Wartet nichts, gibt es eine {/* Was jetzt ansteht — direkt anklickbar. Wartet nichts, gibt es eine
kleine Belohnung statt einer leeren Liste. */} kleine Belohnung statt einer leeren Liste. */}
<section className="border border-ink/10 bg-background p-6 lg:col-span-2"> <section className="border border-ink/10 bg-background p-6 lg:col-span-2">
<h2 className="text-lg text-forest">{t.adminDashboardPage.toDoHeading}</h2> <h2 className="text-lg text-heading">{t.adminDashboardPage.toDoHeading}</h2>
{toDo.length > 0 ? ( {toDo.length > 0 ? (
<ul className="mt-4 divide-y divide-ink/10"> <ul className="mt-4 divide-y divide-ink/10">
{toDo.map((item) => ( {toDo.map((item) => (
<li key={item.href}> <li key={item.href}>
<Link <Link
href={item.href} href={item.href}
className="group flex items-center gap-3 py-3 text-sm text-ink/80 transition-colors hover:text-forest" className="group flex items-center gap-3 py-3 text-sm text-ink/80 transition-colors hover:text-heading"
> >
<span className="flex h-7 min-w-7 items-center justify-center rounded-full bg-honey-dark px-2 text-xs font-semibold text-cream tabular-nums"> <span className="flex h-7 min-w-7 items-center justify-center rounded-full bg-honey-deep px-2 text-xs font-semibold text-cream tabular-nums">
{item.count} {item.count}
</span> </span>
<span className="flex-1">{item.label}</span> <span className="flex-1">{item.label}</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="h-4 w-4 text-ink/40 transition-[color,transform] duration-300 group-hover:translate-x-1 group-hover:text-forest" className="h-4 w-4 text-ink/40 transition-[color,transform] duration-300 group-hover:translate-x-1 group-hover:text-heading"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.6" strokeWidth="1.6"
@@ -430,7 +430,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
<path d="M7.5 13.2l3 3 6-6.4" stroke="var(--color-cream)" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /> <path d="M7.5 13.2l3 3 6-6.4" stroke="var(--color-cream)" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
<div> <div>
<p className="font-serif text-xl text-forest">{t.adminDashboardPage.allDoneTitle}</p> <p className="font-serif text-xl text-heading">{t.adminDashboardPage.allDoneTitle}</p>
<p className="mt-1 text-sm text-ink/65">{t.adminDashboardPage.allDoneText}</p> <p className="mt-1 text-sm text-ink/65">{t.adminDashboardPage.allDoneText}</p>
</div> </div>
</div> </div>
@@ -448,11 +448,11 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
key={stat.key} key={stat.key}
className="group border border-ink/10 bg-background p-5 transition-[border-color,transform] duration-300 hover:-translate-y-0.5 hover:border-honey/50" className="group border border-ink/10 bg-background p-5 transition-[border-color,transform] duration-300 hover:-translate-y-0.5 hover:border-honey/50"
> >
<span className="flex h-9 w-9 items-center justify-center border border-ink/10 text-forest transition-colors group-hover:border-honey/50 group-hover:text-honey-dark"> <span className="flex h-9 w-9 items-center justify-center border border-ink/10 text-heading transition-colors group-hover:border-honey/50 group-hover:text-honey-dark">
<Icon /> <Icon />
</span> </span>
<p className="mt-4 text-xs text-ink/60">{statLabels[stat.key]}</p> <p className="mt-4 text-xs text-ink/60">{statLabels[stat.key]}</p>
<p className="mt-1 text-2xl font-semibold text-forest">{stat.value}</p> <p className="mt-1 text-2xl font-semibold text-heading">{stat.value}</p>
</div> </div>
); );
})} })}
@@ -460,7 +460,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
<div className="mt-10 grid lg:grid-cols-2 gap-6"> <div className="mt-10 grid lg:grid-cols-2 gap-6">
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20"> <div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
<h2 className="text-lg text-forest mb-4">{t.adminDashboardPage.topProductsHeading}</h2> <h2 className="text-lg text-heading mb-4">{t.adminDashboardPage.topProductsHeading}</h2>
{topProducts.length === 0 ? ( {topProducts.length === 0 ? (
<p className="text-sm text-ink/65">{t.adminDashboardPage.noSalesYet}</p> <p className="text-sm text-ink/65">{t.adminDashboardPage.noSalesYet}</p>
) : ( ) : (
@@ -469,13 +469,13 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
<li key={product.name} className="flex items-center justify-between gap-4"> <li key={product.name} className="flex items-center justify-between gap-4">
<span className="flex items-center gap-3 text-ink/80"> <span className="flex items-center gap-3 text-ink/80">
<span className="relative flex h-6 w-6 flex-shrink-0 items-center justify-center text-[10px] font-semibold text-cream"> <span className="relative flex h-6 w-6 flex-shrink-0 items-center justify-center text-[10px] font-semibold text-cream">
<HexMark className={`absolute inset-0 h-6 w-6 ${i === 0 ? "text-[var(--color-chart-honey)]" : "text-forest/70"}`} /> <HexMark className={`absolute inset-0 h-6 w-6 ${i === 0 ? "text-[var(--color-chart-honey)]" : "text-heading/70"}`} />
<span className="relative">{i + 1}</span> <span className="relative">{i + 1}</span>
</span> </span>
{product.name}{" "} {product.name}{" "}
<span className="text-ink/65">{t.adminDashboardPage.unitsSuffix(product.quantity)}</span> <span className="text-ink/65">{t.adminDashboardPage.unitsSuffix(product.quantity)}</span>
</span> </span>
<span className="text-forest whitespace-nowrap">{formatPrice(product.revenueCents)}</span> <span className="text-heading whitespace-nowrap">{formatPrice(product.revenueCents)}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -483,7 +483,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
</div> </div>
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20"> <div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
<h2 className="text-lg text-forest mb-4"> <h2 className="text-lg text-heading mb-4">
{t.adminDashboardPage.revenueByCategoryHeading} {t.adminDashboardPage.revenueByCategoryHeading}
</h2> </h2>
{revenueByCategory.length === 0 ? ( {revenueByCategory.length === 0 ? (
@@ -496,7 +496,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
<span className="text-ink/70"> <span className="text-ink/70">
{t.common.categories[category as keyof typeof t.common.categories] ?? category} {t.common.categories[category as keyof typeof t.common.categories] ?? category}
</span> </span>
<span className="text-forest">{formatPrice(totals.revenueCents)}</span> <span className="text-heading">{formatPrice(totals.revenueCents)}</span>
</div> </div>
{/* Meter: Spur ist eine hellere Stufe desselben Tons. */} {/* Meter: Spur ist eine hellere Stufe desselben Tons. */}
<div className="h-2 overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--color-chart-honey)_15%,transparent)]"> <div className="h-2 overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--color-chart-honey)_15%,transparent)]">
@@ -514,7 +514,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
<div className="mt-6 grid lg:grid-cols-2 gap-6"> <div className="mt-6 grid lg:grid-cols-2 gap-6">
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20"> <div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
<h2 className="text-lg text-forest mb-4"> <h2 className="text-lg text-heading mb-4">
{t.adminDashboardPage.revenueTrendHeading(REVENUE_TREND_DAYS)} {t.adminDashboardPage.revenueTrendHeading(REVENUE_TREND_DAYS)}
</h2> </h2>
{/* Balken nach dataviz-Skill: duenn, 4px gerundete Kappe an der {/* Balken nach dataviz-Skill: duenn, 4px gerundete Kappe an der
@@ -553,7 +553,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
</div> </div>
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20"> <div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
<h2 className="text-lg text-forest mb-4">{t.adminDashboardPage.topVouchersHeading}</h2> <h2 className="text-lg text-heading mb-4">{t.adminDashboardPage.topVouchersHeading}</h2>
{topDiscountCodes.length === 0 ? ( {topDiscountCodes.length === 0 ? (
<p className="text-sm text-ink/65">{t.adminDashboardPage.noVouchersYet}</p> <p className="text-sm text-ink/65">{t.adminDashboardPage.noVouchersYet}</p>
) : ( ) : (
@@ -566,7 +566,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
({code.type === "PERCENT" ? `${code.value} %` : formatPrice(code.value)}) ({code.type === "PERCENT" ? `${code.value} %` : formatPrice(code.value)})
</span> </span>
</span> </span>
<span className="text-forest whitespace-nowrap"> <span className="text-heading whitespace-nowrap">
{t.adminDashboardPage.usedCountLabel(code.usedCount, code.maxUses)} {t.adminDashboardPage.usedCountLabel(code.usedCount, code.maxUses)}
</span> </span>
</li> </li>
+1 -1
View File
@@ -34,7 +34,7 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/produkte" label={te.backToProducts} /> <BackLink href="/admin/produkte" label={te.backToProducts} />
<h1 className="text-2xl text-forest mb-2">{te.title}</h1> <h1 className="text-2xl text-heading mb-2">{te.title}</h1>
<p className="text-sm text-ink/65 mb-8"> <p className="text-sm text-ink/65 mb-8">
{t.common.categories[product.category]} {t.common.categories[product.category]}
{product.harvest && product.batch {product.harvest && product.batch
+1 -1
View File
@@ -26,7 +26,7 @@ export default async function AdminProductNewPage() {
return ( return (
<div className="max-w-2xl"> <div className="max-w-2xl">
<BackLink href="/admin/produkte" label={tn.backToProducts} /> <BackLink href="/admin/produkte" label={tn.backToProducts} />
<h1 className="text-2xl text-forest mb-8">{tn.title}</h1> <h1 className="text-2xl text-heading mb-8">{tn.title}</h1>
<AdminFormPanel <AdminFormPanel
action={createProductAction} action={createProductAction}
className="group/product" className="group/product"
+2 -2
View File
@@ -99,7 +99,7 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
<div key={request.id} className="bg-background border border-ink/10 p-6"> <div key={request.id} className="bg-background border border-ink/10 p-6">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-start justify-between gap-4">
<div> <div>
<p className="text-forest">{request.order.orderNumber}</p> <p className="text-heading">{request.order.orderNumber}</p>
<p className="text-sm text-ink/70"> <p className="text-sm text-ink/70">
{request.order.firstName} {request.order.lastName} · {request.order.email} {request.order.firstName} {request.order.lastName} · {request.order.email}
</p> </p>
@@ -136,7 +136,7 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
</li> </li>
))} ))}
</ul> </ul>
<p className="mt-2 text-forest tabular-nums">{formatPrice(request.order.totalCents, request.order.currency)}</p> <p className="mt-2 text-heading tabular-nums">{formatPrice(request.order.totalCents, request.order.currency)}</p>
<p className="mt-4 text-sm text-ink/70 border-t border-ink/10 pt-4">{request.reason}</p> <p className="mt-4 text-sm text-ink/70 border-t border-ink/10 pt-4">{request.reason}</p>
+1 -1
View File
@@ -17,7 +17,7 @@ export default async function AdminVersandEditPage(props: PageProps<"/admin/vers
return ( return (
<div className="max-w-xl"> <div className="max-w-xl">
<BackLink href="/admin/versand" label={ts.backToShipping} /> <BackLink href="/admin/versand" label={ts.backToShipping} />
<h1 className="text-2xl text-forest mb-8">{ts.title}</h1> <h1 className="text-2xl text-heading mb-8">{ts.title}</h1>
<AdminFormPanel action={updateShippingTierAction} actions={<Button type="submit">{ts.save}</Button>}> <AdminFormPanel action={updateShippingTierAction} actions={<Button type="submit">{ts.save}</Button>}>
<input type="hidden" name="id" value={tier.id} /> <input type="hidden" name="id" value={tier.id} />
+1 -1
View File
@@ -12,7 +12,7 @@ export default async function AdminVersandNewPage() {
return ( return (
<div className="max-w-xl"> <div className="max-w-xl">
<BackLink href="/admin/versand" label={ts.backToShipping} /> <BackLink href="/admin/versand" label={ts.backToShipping} />
<h1 className="text-2xl text-forest mb-8">{ts.title}</h1> <h1 className="text-2xl text-heading mb-8">{ts.title}</h1>
<AdminFormPanel action={createShippingTierAction} actions={<Button type="submit">{ts.create}</Button>}> <AdminFormPanel action={createShippingTierAction} actions={<Button type="submit">{ts.create}</Button>}>
<div className="space-y-4"> <div className="space-y-4">
+1 -1
View File
@@ -35,7 +35,7 @@ export default async function AdminVersandPage() {
<tbody className="divide-y divide-ink/10"> <tbody className="divide-y divide-ink/10">
{tiers.map((tier) => ( {tiers.map((tier) => (
<tr key={tier.id} className="transition-colors hover:bg-ink/[0.03]"> <tr key={tier.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest"> <td className="p-4 text-heading">
{tier.maxQuantity === null {tier.maxQuantity === null
? t.adminShippingPage.quantityFrom(tier.minQuantity) ? t.adminShippingPage.quantityFrom(tier.minQuantity)
: t.adminShippingPage.quantityRange(tier.minQuantity, tier.maxQuantity)} : t.adminShippingPage.quantityRange(tier.minQuantity, tier.maxQuantity)}
+7 -7
View File
@@ -62,7 +62,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
defaultValue={typeof orderNumber === "string" ? orderNumber : ""} defaultValue={typeof orderNumber === "string" ? orderNumber : ""}
placeholder={t.trackOrderPage.orderNumberPlaceholder} placeholder={t.trackOrderPage.orderNumberPlaceholder}
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> </label>
<label className="block text-sm"> <label className="block text-sm">
@@ -73,7 +73,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
defaultValue={typeof email === "string" ? email : ""} defaultValue={typeof email === "string" ? email : ""}
required required
autoComplete="email" autoComplete="email"
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> </label>
<Button type="submit">{t.trackOrderPage.submit}</Button> <Button type="submit">{t.trackOrderPage.submit}</Button>
@@ -96,7 +96,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
<div className="mt-10 border border-ink/10 bg-background p-6 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)] sm:p-8"> <div className="mt-10 border border-ink/10 bg-background p-6 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)] sm:p-8">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<p className="text-forest">{order.orderNumber}</p> <p className="text-heading">{order.orderNumber}</p>
<p className="text-xs text-ink/65">{formatDate(order.createdAt, locale)}</p> <p className="text-xs text-ink/65">{formatDate(order.createdAt, locale)}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
@@ -122,7 +122,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
{order.items.map((item) => ( {order.items.map((item) => (
<li key={item.id}> <li key={item.id}>
{item.quantity} ×{" "} {item.quantity} ×{" "}
<Link href={`/honig/${item.product.slug}#bewertungen`} className="hover:text-forest hover:underline"> <Link href={`/honig/${item.product.slug}#bewertungen`} className="hover:text-heading hover:underline">
{item.productName} {item.productName}
</Link> </Link>
</li> </li>
@@ -157,7 +157,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
<> <>
{" "} {" "}
·{" "} ·{" "}
<a href={url} target="_blank" rel="noopener noreferrer" className="text-forest underline"> <a href={url} target="_blank" rel="noopener noreferrer" className="text-heading underline">
{t.shipmentInfo.trackPackage} {t.shipmentInfo.trackPackage}
</a> </a>
</> </>
@@ -181,9 +181,9 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
/> />
<div className="mt-5 flex items-center justify-between gap-4"> <div className="mt-5 flex items-center justify-between gap-4">
<p className="font-serif text-2xl text-forest tabular-nums">{formatPrice(order.totalCents, order.currency, locale)}</p> <p className="font-serif text-2xl text-heading tabular-nums">{formatPrice(order.totalCents, order.currency, locale)}</p>
{order.invoice ? ( {order.invoice ? (
<Link href={`/rechnung/${order.invoice.invoiceNumber}`} className="text-sm text-forest underline"> <Link href={`/rechnung/${order.invoice.invoiceNumber}`} className="text-sm text-heading underline">
{t.trackOrderPage.viewInvoice} {t.trackOrderPage.viewInvoice}
</Link> </Link>
) : null} ) : null}
+2 -2
View File
@@ -80,7 +80,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
</span> </span>
<span className="tracking-wide-caps text-xs text-honey-dark">{t.chargePage.verifiedBatch}</span> <span className="tracking-wide-caps text-xs text-honey-dark">{t.chargePage.verifiedBatch}</span>
</div> </div>
<h1 className="mt-6 text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-forest"> <h1 className="mt-6 text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-heading">
{batch.harvest.type} {batch.harvest.year} {batch.harvest.type} {batch.harvest.year}
</h1> </h1>
<p className="mt-4 text-sm text-ink/70">{t.chargePage.provenanceNote(batch.code)}</p> <p className="mt-4 text-sm text-ink/70">{t.chargePage.provenanceNote(batch.code)}</p>
@@ -132,7 +132,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
) : null} ) : null}
</div> </div>
<p className="mt-3 text-xs tracking-wide-caps text-ink/70">{step.label}</p> <p className="mt-3 text-xs tracking-wide-caps text-ink/70">{step.label}</p>
<p className="mt-1 font-serif text-xl text-forest tabular-nums">{step.value}</p> <p className="mt-1 font-serif text-xl text-heading tabular-nums">{step.value}</p>
</li> </li>
))} ))}
</ol> </ol>
+18 -18
View File
@@ -378,14 +378,14 @@ export function CheckoutForm({
aria-pressed={selected} aria-pressed={selected}
className={`group relative flex items-center gap-4 border px-5 py-4 text-left transition-[border-color,background-color,box-shadow] duration-300 ${ className={`group relative flex items-center gap-4 border px-5 py-4 text-left transition-[border-color,background-color,box-shadow] duration-300 ${
selected selected
? "border-forest bg-forest/[0.04] text-forest shadow-[0_14px_30px_-22px_rgba(36,53,43,0.6)]" ? "border-heading bg-forest/[0.04] text-heading shadow-[0_14px_30px_-22px_rgba(36,53,43,0.6)]"
: "border-ink/15 text-ink/70 hover:border-forest/50" : "border-ink/15 text-ink/70 hover:border-heading/50"
}`} }`}
> >
<span <span
aria-hidden="true" aria-hidden="true"
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center border transition-colors ${ className={`flex h-10 w-10 flex-shrink-0 items-center justify-center border transition-colors ${
selected ? "border-forest bg-forest text-cream" : "border-ink/15 text-forest" selected ? "border-heading bg-forest text-cream" : "border-ink/15 text-heading"
}`} }`}
> >
{option === "VERSAND" ? ( {option === "VERSAND" ? (
@@ -431,8 +431,8 @@ export function CheckoutForm({
aria-pressed={deliveryTarget === option} aria-pressed={deliveryTarget === option}
className={`flex-1 border px-4 py-3 text-sm transition-colors ${ className={`flex-1 border px-4 py-3 text-sm transition-colors ${
deliveryTarget === option deliveryTarget === option
? "border-forest bg-forest text-cream" ? "border-heading bg-forest text-cream"
: "border-ink/20 hover:border-forest" : "border-ink/20 hover:border-heading"
}`} }`}
> >
{option === "ADRESSE" {option === "ADRESSE"
@@ -482,7 +482,7 @@ export function CheckoutForm({
value={country} value={country}
onChange={(event) => setCountry(event.target.value)} onChange={(event) => setCountry(event.target.value)}
autoComplete="country" autoComplete="country"
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
> >
{countryCodes.map((code) => ( {countryCodes.map((code) => (
<option key={code} value={code}> <option key={code} value={code}>
@@ -502,12 +502,12 @@ export function CheckoutForm({
<p className="text-xs text-ink/70">{t.checkoutPage.packstationIntro}</p> <p className="text-xs text-ink/70">{t.checkoutPage.packstationIntro}</p>
{selectedPackstation ? ( {selectedPackstation ? (
<div className="flex items-start justify-between gap-4 border border-forest/40 bg-forest/5 p-3"> <div className="flex items-start justify-between gap-4 border border-heading/40 bg-forest/5 p-3">
<div className="text-sm"> <div className="text-sm">
<p className="text-xs tracking-wide-caps text-ink/70"> <p className="text-xs tracking-wide-caps text-ink/70">
{t.checkoutPage.packstationSelectedLabel} {t.checkoutPage.packstationSelectedLabel}
</p> </p>
<p className="mt-1 text-forest">{selectedPackstation.name}</p> <p className="mt-1 text-heading">{selectedPackstation.name}</p>
<p className="text-ink/70"> <p className="text-ink/70">
{selectedPackstation.street ? `${selectedPackstation.street}, ` : ""} {selectedPackstation.street ? `${selectedPackstation.street}, ` : ""}
{selectedPackstation.zip} {selectedPackstation.city} {selectedPackstation.zip} {selectedPackstation.city}
@@ -539,7 +539,7 @@ export function CheckoutForm({
type="button" type="button"
onClick={searchPackstations} onClick={searchPackstations}
disabled={packstationSearching || packstationZip.trim().length !== 5} disabled={packstationSearching || packstationZip.trim().length !== 5}
className="border border-ink/20 bg-background px-4 py-2 text-sm hover:border-forest disabled:opacity-50" className="border border-ink/20 bg-background px-4 py-2 text-sm hover:border-heading disabled:opacity-50"
> >
{packstationSearching {packstationSearching
? t.checkoutPage.packstationSearching ? t.checkoutPage.packstationSearching
@@ -562,9 +562,9 @@ export function CheckoutForm({
<button <button
type="button" type="button"
onClick={() => setSelectedPackstation(packstation)} onClick={() => setSelectedPackstation(packstation)}
className="w-full border border-ink/20 px-3 py-2 text-left text-sm hover:border-forest" className="w-full border border-ink/20 px-3 py-2 text-left text-sm hover:border-heading"
> >
<span className="text-forest">{packstation.name}</span> <span className="text-heading">{packstation.name}</span>
<span className="block text-xs text-ink/70"> <span className="block text-xs text-ink/70">
{packstation.street ? `${packstation.street}, ` : ""} {packstation.street ? `${packstation.street}, ` : ""}
{packstation.zip} {packstation.city} {packstation.zip} {packstation.city}
@@ -618,7 +618,7 @@ export function CheckoutForm({
maxLength={500} maxLength={500}
rows={3} rows={3}
placeholder={t.checkoutPage.giftMessagePlaceholder} placeholder={t.checkoutPage.giftMessagePlaceholder}
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest resize-none" className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-heading resize-none"
/> />
</label> </label>
) : null} ) : null}
@@ -644,13 +644,13 @@ export function CheckoutForm({
</form> </form>
<aside hidden={payment !== null} className="border border-ink/10 bg-background p-8 h-fit lg:sticky lg:top-28 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)]"> <aside hidden={payment !== null} className="border border-ink/10 bg-background p-8 h-fit lg:sticky lg:top-28 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)]">
<h2 className="text-2xl text-forest mb-6">{t.checkoutPage.yourOrderHeading}</h2> <h2 className="text-2xl text-heading mb-6">{t.checkoutPage.yourOrderHeading}</h2>
<ul className="space-y-3 text-sm"> <ul className="space-y-3 text-sm">
{items.map((item) => ( {items.map((item) => (
<li key={item.productId} className="flex items-baseline justify-between gap-4"> <li key={item.productId} className="flex items-baseline justify-between gap-4">
<span> <span>
<span className="text-ink/55 tabular-nums">{item.quantity} ×</span>{" "} <span className="text-ink/55 tabular-nums">{item.quantity} ×</span>{" "}
<span className="font-serif text-lg text-forest">{item.name}</span> <span className="font-serif text-lg text-heading">{item.name}</span>
</span> </span>
<span className="tabular-nums">{formatPrice(item.priceCents * item.quantity, "EUR", locale)}</span> <span className="tabular-nums">{formatPrice(item.priceCents * item.quantity, "EUR", locale)}</span>
</li> </li>
@@ -670,7 +670,7 @@ export function CheckoutForm({
<span>{shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}</span> <span>{shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}</span>
</div> </div>
{activeBulkDiscount ? ( {activeBulkDiscount ? (
<div className="flex justify-between text-forest"> <div className="flex justify-between text-heading">
<span> <span>
{t.cartPage.discountLabel} ({activeBulkDiscount.percent}%) {t.cartPage.discountLabel} ({activeBulkDiscount.percent}%)
</span> </span>
@@ -678,7 +678,7 @@ export function CheckoutForm({
</div> </div>
) : null} ) : null}
</div> </div>
<div className="mt-4 pt-4 border-t border-ink/10 flex items-baseline justify-between text-forest"> <div className="mt-4 pt-4 border-t border-ink/10 flex items-baseline justify-between text-heading">
<span className="text-base">{t.cartPage.total}</span> <span className="text-base">{t.cartPage.total}</span>
<span className="font-serif text-3xl tabular-nums">{formatPrice(total, "EUR", locale)}</span> <span className="font-serif text-3xl tabular-nums">{formatPrice(total, "EUR", locale)}</span>
</div> </div>
@@ -721,7 +721,7 @@ function Field({
placeholder={placeholder} placeholder={placeholder}
autoComplete={autoComplete} autoComplete={autoComplete}
required={required} 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> </label>
); );
@@ -756,7 +756,7 @@ function CheckoutSteps({ current, labels, ariaLabel }: { current: number; labels
</text> </text>
)} )}
</svg> </svg>
<span className={`tracking-wide-caps text-xs ${active ? "text-forest" : done ? "text-forest/70" : "text-ink/50"}`}> <span className={`tracking-wide-caps text-xs ${active ? "text-heading" : done ? "text-heading/70" : "text-ink/50"}`}>
{label} {label}
</span> </span>
</span> </span>
+1 -1
View File
@@ -114,7 +114,7 @@ export function EmbeddedPayment({
id="payment-heading" id="payment-heading"
ref={headingRef} ref={headingRef}
tabIndex={-1} tabIndex={-1}
className="scroll-mt-24 text-2xl text-forest outline-none" className="scroll-mt-24 text-2xl text-heading outline-none"
> >
{t.checkoutPage.paymentHeading} {t.checkoutPage.paymentHeading}
</h2> </h2>
+1 -1
View File
@@ -93,7 +93,7 @@ export default async function CheckoutSuccessPage(props: PageProps<"/checkout/su
/> />
{orderNumber ? ( {orderNumber ? (
<p className="mt-4 text-ink/70"> <p className="mt-4 text-ink/70">
{t.checkoutSuccessPage.orderNumberLabel}: <span className="text-forest">{orderNumber}</span> {t.checkoutSuccessPage.orderNumberLabel}: <span className="text-heading">{orderNumber}</span>
</p> </p>
) : null} ) : null}
<p className="mt-6 text-ink/70"> <p className="mt-6 text-ink/70">
+5 -5
View File
@@ -60,10 +60,10 @@ export default async function HarvestDetailPage(
<p className="tracking-wide-caps text-xs text-honey-dark mb-3"> <p className="tracking-wide-caps text-xs text-honey-dark mb-3">
{harvest.year} · {formatDate(harvest.harvestDate, locale)} {harvest.year} · {formatDate(harvest.harvestDate, locale)}
</p> </p>
<h1 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-forest">{harvest.type}</h1> <h1 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-heading">{harvest.type}</h1>
<p className="mt-8 max-w-[60ch] text-lg text-ink/75 leading-relaxed">{harvest.description}</p> <p className="mt-8 max-w-[60ch] text-lg text-ink/75 leading-relaxed">{harvest.description}</p>
<h2 className="mt-12 text-2xl text-forest">{t.erntenDetailPage.batchesHeading}</h2> <h2 className="mt-12 text-2xl text-heading">{t.erntenDetailPage.batchesHeading}</h2>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{harvest.batches.map((batch) => { {harvest.batches.map((batch) => {
const fill = batch.glassCount > 0 ? Math.min(1, batch.remainingGlasses / batch.glassCount) : 0; const fill = batch.glassCount > 0 ? Math.min(1, batch.remainingGlasses / batch.glassCount) : 0;
@@ -71,10 +71,10 @@ export default async function HarvestDetailPage(
<Link <Link
key={batch.id} key={batch.id}
href={`/charge/${batch.code}`} href={`/charge/${batch.code}`}
className="group block border border-ink/10 px-5 py-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 block border border-ink/10 px-5 py-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="flex items-center justify-between gap-4 text-sm"> <span className="flex items-center justify-between gap-4 text-sm">
<span className="font-serif text-xl text-forest tabular-nums">{batch.code}</span> <span className="font-serif text-xl text-heading tabular-nums">{batch.code}</span>
<span className="text-ink/65 tabular-nums"> <span className="text-ink/65 tabular-nums">
{t.erntenDetailPage.glassesAvailable(batch.remainingGlasses, batch.glassCount)} {t.erntenDetailPage.glassesAvailable(batch.remainingGlasses, batch.glassCount)}
</span> </span>
@@ -91,7 +91,7 @@ export default async function HarvestDetailPage(
})} })}
</div> </div>
<h2 className="mt-12 text-2xl text-forest">{t.erntenDetailPage.productsHeading}</h2> <h2 className="mt-12 text-2xl text-heading">{t.erntenDetailPage.productsHeading}</h2>
<div className="mt-4 grid gap-3"> <div className="mt-4 grid gap-3">
{harvest.products.map((product) => ( {harvest.products.map((product) => (
<ProductTeaserLink <ProductTeaserLink
+2 -2
View File
@@ -69,7 +69,7 @@ export default async function ErntenPage() {
</span> </span>
<div className="mt-5 flex items-start justify-between gap-4"> <div className="mt-5 flex items-start justify-between gap-4">
<div> <div>
<h3 className="link-draw inline pb-0.5 font-serif text-2xl text-forest">{harvest.type}</h3> <h3 className="link-draw inline pb-0.5 font-serif text-2xl text-heading">{harvest.type}</h3>
<p className="mt-2 flex items-center gap-2 text-sm text-ink/65"> <p className="mt-2 flex items-center gap-2 text-sm text-ink/65">
<HexMark className="h-2 w-2 text-honey" /> <HexMark className="h-2 w-2 text-honey" />
{t.erntenPage.batchCount(harvest.batches.length)} {t.erntenPage.batchCount(harvest.batches.length)}
@@ -78,7 +78,7 @@ export default async function ErntenPage() {
<svg <svg
aria-hidden="true" aria-hidden="true"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="mt-2 h-4 w-4 flex-shrink-0 text-forest transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1" className="mt-2 h-4 w-4 flex-shrink-0 text-heading transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.6" strokeWidth="1.6"
+54 -5
View File
@@ -22,6 +22,52 @@
/* Diagrammton, per dataviz-Validator geprueft: Helligkeitsband, Chroma /* Diagrammton, per dataviz-Validator geprueft: Helligkeitsband, Chroma
und >=3:1 gegen die helle Flaeche (#b88a44 erreichte nur 2,93:1). */ und >=3:1 gegen die helle Flaeche (#b88a44 erreichte nur 2,93:1). */
--color-chart-honey: #a0722f; --color-chart-honey: #a0722f;
/* Rollen statt reiner Markenfarben — sie kippen im Dunkelmodus, die
Markenflaechen nicht:
- heading: Ueberschriften, Links, Rahmen auf der Seitenflaeche
(hell: Waldgruen, dunkel: warmes Creme).
- surface: leicht abgesetzte Flaeche (hell: Creme, dunkel: angehobenes
Dunkelgruen).
- forest bleibt immer die dunkle Markenflaeche (Hero, Baender, Knoepfe,
Admin-Leiste), cream immer der helle Text darauf.
- honey-deep ist der feste dunkle Honigton fuer FLAECHEN (Badges,
Akzentknopf); honey-dark ist die Textvariante und wird dunkel heller. */
--color-heading: #24352b;
--color-surface: #f4efe5;
--color-honey-deep: #8a6531;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2324352b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
color-scheme: light;
}
/* Dunkle Darstellung — zwei Wege dorthin: ausdruecklich gewaehlt
(data-theme="dark", components/theme-toggle.tsx) oder "Automatisch" bei
dunkler Systemeinstellung. Beide Bloecke muessen identisch bleiben;
theme-tokens.test.ts prueft das. */
:root[data-theme="dark"] {
--color-background: #111713;
--color-ink: #e9e3d4;
--color-heading: #efe4c9;
--color-surface: #18201b;
--color-forest: #1b2820;
--color-honey-dark: #d3ae72;
--color-chart-honey: #c99a50;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e9e3d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-background: #111713;
--color-ink: #e9e3d4;
--color-heading: #efe4c9;
--color-surface: #18201b;
--color-forest: #1b2820;
--color-honey-dark: #d3ae72;
--color-chart-honey: #c99a50;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e9e3d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
color-scheme: dark;
}
} }
@theme inline { @theme inline {
@@ -34,6 +80,9 @@
--color-background: var(--color-background); --color-background: var(--color-background);
--color-ink: var(--color-ink); --color-ink: var(--color-ink);
--color-sage: var(--color-sage); --color-sage: var(--color-sage);
--color-heading: var(--color-heading);
--color-surface: var(--color-surface);
--color-honey-deep: var(--color-honey-deep);
--font-serif: var(--font-cormorant), Georgia, serif; --font-serif: var(--font-cormorant), Georgia, serif;
--font-sans: var(--font-inter), system-ui, sans-serif; --font-sans: var(--font-inter), system-ui, sans-serif;
@@ -47,8 +96,8 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
/* Browserflaechen tragen die Marke mit: Scrollbar im Waldton statt Grau. */ /* Browserflaechen tragen die Marke mit: Scrollbar im Waldton statt Grau. */
scrollbar-color: color-mix(in oklab, var(--color-forest) 45%, transparent) transparent; scrollbar-color: color-mix(in oklab, var(--color-heading) 45%, transparent) transparent;
accent-color: var(--color-forest); accent-color: var(--color-heading);
} }
body { body {
@@ -369,7 +418,7 @@ textarea::placeholder {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1; z-index: 1;
background: color-mix(in oklab, var(--color-cream) 70%, var(--color-background)); background: color-mix(in oklab, var(--color-surface) 70%, var(--color-background));
font-size: 0.68rem; font-size: 0.68rem;
font-weight: 500; font-weight: 500;
letter-spacing: 0.12em; letter-spacing: 0.12em;
@@ -385,7 +434,7 @@ textarea::placeholder {
.admin-content select { .admin-content select {
appearance: none; appearance: none;
padding-right: 2rem; padding-right: 2rem;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2324352b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>"); background-image: var(--select-arrow);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 0.6rem center; background-position: right 0.6rem center;
background-size: 0.8rem; background-size: 0.8rem;
@@ -410,5 +459,5 @@ textarea::placeholder {
text-transform: none; text-transform: none;
} }
.admin-content h2.tracking-wide-caps.text-ink\/65 { .admin-content h2.tracking-wide-caps.text-ink\/65 {
color: var(--color-forest); color: var(--color-heading);
} }
+1 -1
View File
@@ -60,7 +60,7 @@ export default async function HerkunftPage() {
className="mt-2 h-3.5 w-3.5 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90" className="mt-2 h-3.5 w-3.5 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90"
/> />
<div> <div>
<h3 className="text-2xl text-forest">{a.title}</h3> <h3 className="text-2xl text-heading">{a.title}</h3>
<p className="mt-2 text-ink/70 leading-relaxed">{a.text}</p> <p className="mt-2 text-ink/70 leading-relaxed">{a.text}</p>
</div> </div>
</div> </div>
+14 -14
View File
@@ -264,7 +264,7 @@ export default async function ProductDetailPage(
<p className="tracking-wide-caps text-xs text-honey-dark mb-4"> <p className="tracking-wide-caps text-xs text-honey-dark mb-4">
{company.name} · {t.common.categories[product.category]} {company.name} · {t.common.categories[product.category]}
</p> </p>
<h1 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-forest">{product.name}</h1> <h1 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-heading">{product.name}</h1>
{/* Ohne gepflegte Noten sonst ein leerer Absatz, der nur Abstand {/* Ohne gepflegte Noten sonst ein leerer Absatz, der nur Abstand
zwischen Ueberschrift und Bewertung erzeugt. */} zwischen Ueberschrift und Bewertung erzeugt. */}
{product.tasteTags.length > 0 ? ( {product.tasteTags.length > 0 ? (
@@ -282,7 +282,7 @@ export default async function ProductDetailPage(
)} )}
<div className="mt-8 flex items-baseline gap-3"> <div className="mt-8 flex items-baseline gap-3">
<span className="font-serif text-4xl text-forest tabular-nums"> <span className="font-serif text-4xl text-heading tabular-nums">
{formatPrice(product.priceCents, product.currency, locale)} {formatPrice(product.priceCents, product.currency, locale)}
</span> </span>
<span className="text-sm text-ink/65 tabular-nums">{product.weightGrams} g</span> <span className="text-sm text-ink/65 tabular-nums">{product.weightGrams} g</span>
@@ -292,7 +292,7 @@ export default async function ProductDetailPage(
</p> </p>
<Link <Link
href="/versand-zahlung" href="/versand-zahlung"
className="mt-1 inline-block py-1 text-xs text-ink/65 underline decoration-ink/30 hover:text-forest hover:decoration-forest" className="mt-1 inline-block py-1 text-xs text-ink/65 underline decoration-ink/30 hover:text-heading hover:decoration-heading"
> >
{t.productPage.shippingHint( {t.productPage.shippingHint(
baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents, "EUR", locale) baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents, "EUR", locale)
@@ -341,11 +341,11 @@ export default async function ProductDetailPage(
type="email" type="email"
required required
placeholder={t.productPage.emailPlaceholder} placeholder={t.productPage.emailPlaceholder}
className="flex-1 min-w-0 border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest" className="flex-1 min-w-0 border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
/> />
<button <button
type="submit" type="submit"
className="shrink-0 border border-ink/20 px-3 py-2 text-sm hover:border-forest" className="shrink-0 border border-ink/20 px-3 py-2 text-sm hover:border-heading"
> >
{t.productPage.notifySubmit} {t.productPage.notifySubmit}
</button> </button>
@@ -369,7 +369,7 @@ export default async function ProductDetailPage(
<input type="hidden" name="redirectPath" value={`/honig/${product.slug}`} /> <input type="hidden" name="redirectPath" value={`/honig/${product.slug}`} />
<button <button
type="submit" type="submit"
className="inline-flex items-center gap-2 py-1.5 text-sm text-ink/70 underline hover:text-forest" className="inline-flex items-center gap-2 py-1.5 text-sm text-ink/70 underline hover:text-heading"
> >
<svg <svg
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -387,7 +387,7 @@ export default async function ProductDetailPage(
</form> </form>
) : ( ) : (
<p className="mt-3 text-sm text-ink/65"> <p className="mt-3 text-sm text-ink/65">
<Link href="/konto" className="text-forest underline"> <Link href="/konto" className="text-heading underline">
{t.productPage.loginLinkLabel} {t.productPage.loginLinkLabel}
</Link> </Link>
, {t.productPage.loginToSave} , {t.productPage.loginToSave}
@@ -427,11 +427,11 @@ export default async function ProductDetailPage(
{isHoney ? ( {isHoney ? (
<div className="mt-6 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm"> <div className="mt-6 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm">
<span className="text-ink/65">{t.productPage.learnMoreLabel}</span> <span className="text-ink/65">{t.productPage.learnMoreLabel}</span>
<Link href="/wissen/warum-kristallisiert-honig" className="inline-block py-0.5 text-forest underline"> <Link href="/wissen/warum-kristallisiert-honig" className="inline-block py-0.5 text-heading underline">
{t.wissenArticles["warum-kristallisiert-honig"].title} {t.wissenArticles["warum-kristallisiert-honig"].title}
</Link> </Link>
{harvestWissenSlug ? ( {harvestWissenSlug ? (
<Link href={`/wissen/${harvestWissenSlug}`} className="inline-block py-0.5 text-forest underline"> <Link href={`/wissen/${harvestWissenSlug}`} className="inline-block py-0.5 text-heading underline">
{t.wissenArticles[harvestWissenSlug].title} {t.wissenArticles[harvestWissenSlug].title}
</Link> </Link>
) : null} ) : null}
@@ -441,7 +441,7 @@ export default async function ProductDetailPage(
{isHoney && product.batch ? ( {isHoney && product.batch ? (
<Link <Link
href={`/charge/${product.batch.code}`} href={`/charge/${product.batch.code}`}
className="mt-2.5 inline-block py-0.5 text-sm text-forest underline" className="mt-2.5 inline-block py-0.5 text-sm text-heading underline"
> >
{t.productPage.viewBatchOrigin} {t.productPage.viewBatchOrigin}
</Link> </Link>
@@ -451,7 +451,7 @@ export default async function ProductDetailPage(
<Container id="bewertungen" className="mt-20 max-w-3xl"> <Container id="bewertungen" className="mt-20 max-w-3xl">
<ScrollReveal> <ScrollReveal>
<h2 className="text-3xl md:text-4xl text-forest">{t.productPage.reviewsHeading}</h2> <h2 className="text-3xl md:text-4xl text-heading">{t.productPage.reviewsHeading}</h2>
</ScrollReveal> </ScrollReveal>
@@ -475,7 +475,7 @@ export default async function ProductDetailPage(
<div className="mt-10 border-t border-ink/10 pt-8"> <div className="mt-10 border-t border-ink/10 pt-8">
{!customer ? ( {!customer ? (
<p className="text-sm text-ink/70"> <p className="text-sm text-ink/70">
<Link href="/konto" className="text-forest underline"> <Link href="/konto" className="text-heading underline">
{t.productPage.loginLinkLabel} {t.productPage.loginLinkLabel}
</Link> </Link>
, {t.productPage.loginToReview} , {t.productPage.loginToReview}
@@ -516,7 +516,7 @@ export default async function ProductDetailPage(
name="comment" name="comment"
rows={3} rows={3}
placeholder={t.productPage.commentPlaceholder} placeholder={t.productPage.commentPlaceholder}
className="w-full border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-forest" className="w-full border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-heading"
/> />
<Button type="submit">{t.productPage.submitReview}</Button> <Button type="submit">{t.productPage.submitReview}</Button>
</form> </form>
@@ -532,7 +532,7 @@ export default async function ProductDetailPage(
<ScrollReveal <ScrollReveal
className={relatedProducts.length <= 2 ? "sm:col-span-2 lg:col-span-1" : "sm:col-span-2 lg:col-span-3"} className={relatedProducts.length <= 2 ? "sm:col-span-2 lg:col-span-1" : "sm:col-span-2 lg:col-span-3"}
> >
<h2 className="text-3xl md:text-4xl text-forest text-balance">{t.productPage.relatedProductsHeading}</h2> <h2 className="text-3xl md:text-4xl text-heading text-balance">{t.productPage.relatedProductsHeading}</h2>
</ScrollReveal> </ScrollReveal>
{relatedProducts.map((related, index) => ( {relatedProducts.map((related, index) => (
<ProductCard key={related.id} product={related} index={index} /> <ProductCard key={related.id} product={related} index={index} />
+1 -1
View File
@@ -31,7 +31,7 @@ export default async function ImkereiPage() {
<div className="mt-8 max-w-[62ch] space-y-5 text-ink/75 leading-relaxed"> <div className="mt-8 max-w-[62ch] space-y-5 text-ink/75 leading-relaxed">
{t.imkereiPage.paragraphs.map((paragraph, i) => ( {t.imkereiPage.paragraphs.map((paragraph, i) => (
// Erster Absatz als Einstieg: groesser, in Serifenschrift. // Erster Absatz als Einstieg: groesser, in Serifenschrift.
<p key={paragraph} className={i === 0 ? "font-serif text-2xl leading-snug text-forest" : undefined}> <p key={paragraph} className={i === 0 ? "font-serif text-2xl leading-snug text-heading" : undefined}>
{paragraph} {paragraph}
</p> </p>
))} ))}
+1 -1
View File
@@ -65,7 +65,7 @@ export default async function KontaktPage() {
<ScrollReveal delay={0.18} className="md:col-span-2"> <ScrollReveal delay={0.18} className="md:col-span-2">
<div className="h-full border border-honey/30 bg-honey/[0.06] px-8 py-10"> <div className="h-full border border-honey/30 bg-honey/[0.06] px-8 py-10">
<HexMark className="h-4 w-4 text-honey-dark" /> <HexMark className="h-4 w-4 text-honey-dark" />
<h2 className="mt-5 text-2xl text-forest">{t.kontaktPage.pickupHeading}</h2> <h2 className="mt-5 text-2xl text-heading">{t.kontaktPage.pickupHeading}</h2>
<p className="mt-3 text-ink/70 leading-relaxed">{t.kontaktPage.pickupText}</p> <p className="mt-3 text-ink/70 leading-relaxed">{t.kontaktPage.pickupText}</p>
</div> </div>
</ScrollReveal> </ScrollReveal>
+6 -6
View File
@@ -35,7 +35,7 @@ export default async function KontoAdressenPage() {
className={`relative flex flex-col justify-between gap-6 border p-6 transition-[border-color,box-shadow] duration-500 ease-[var(--ease-arrive)] ${ className={`relative flex flex-col justify-between gap-6 border p-6 transition-[border-color,box-shadow] duration-500 ease-[var(--ease-arrive)] ${
address.isDefault address.isDefault
? "border-honey/60 bg-honey/[0.05] shadow-[0_18px_34px_-26px_rgba(184,138,68,0.5)]" ? "border-honey/60 bg-honey/[0.05] shadow-[0_18px_34px_-26px_rgba(184,138,68,0.5)]"
: "border-ink/10 hover:border-forest/40" : "border-ink/10 hover:border-heading/40"
}`} }`}
> >
{address.isDefault ? ( {address.isDefault ? (
@@ -45,11 +45,11 @@ export default async function KontoAdressenPage() {
</span> </span>
) : null} ) : null}
<div className="flex gap-4"> <div className="flex gap-4">
<span className="flex-shrink-0 flex items-center justify-center w-11 h-11 border border-ink/15 text-forest"> <span className="flex-shrink-0 flex items-center justify-center w-11 h-11 border border-ink/15 text-heading">
<AddressIcon /> <AddressIcon />
</span> </span>
<div> <div>
<p className="font-serif text-xl text-forest">{address.label}</p> <p className="font-serif text-xl text-heading">{address.label}</p>
<p className="text-sm text-ink/70 mt-1"> <p className="text-sm text-ink/70 mt-1">
{address.firstName} {address.lastName} {address.firstName} {address.lastName}
<br /> <br />
@@ -63,7 +63,7 @@ export default async function KontoAdressenPage() {
{!address.isDefault ? ( {!address.isDefault ? (
<form action={setDefaultAddressAction}> <form action={setDefaultAddressAction}>
<input type="hidden" name="addressId" value={address.id} /> <input type="hidden" name="addressId" value={address.id} />
<button type="submit" className="text-forest hover:underline"> <button type="submit" className="text-heading hover:underline">
{t.addressesPage.setDefault} {t.addressesPage.setDefault}
</button> </button>
</form> </form>
@@ -87,7 +87,7 @@ export default async function KontoAdressenPage() {
</div> </div>
<FormPanel className="mt-14 max-w-xl"> <FormPanel className="mt-14 max-w-xl">
<h2 className="text-2xl text-forest mb-6">{t.addressesPage.addNewHeading}</h2> <h2 className="text-2xl text-heading mb-6">{t.addressesPage.addNewHeading}</h2>
<form action={addAddressAction} className="space-y-4"> <form action={addAddressAction} className="space-y-4">
<Field <Field
name="label" name="label"
@@ -148,7 +148,7 @@ function Field({
autoComplete={autoComplete} autoComplete={autoComplete}
inputMode={inputMode} inputMode={inputMode}
required={required} 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> </label>
); );
@@ -53,7 +53,7 @@ export default async function RetourePage(props: PageProps<"/konto/bestellungen/
<li key={item.id} className="flex items-center gap-3 py-3"> <li key={item.id} className="flex items-center gap-3 py-3">
<HexMark className="h-2.5 w-2.5 text-honey" /> <HexMark className="h-2.5 w-2.5 text-honey" />
<span className="tabular-nums text-ink/55">{item.quantity} ×</span> <span className="tabular-nums text-ink/55">{item.quantity} ×</span>
<span className="font-serif text-lg text-forest">{item.productName}</span> <span className="font-serif text-lg text-heading">{item.productName}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -71,7 +71,7 @@ export default async function RetourePage(props: PageProps<"/konto/bestellungen/
minLength={3} minLength={3}
rows={5} rows={5}
placeholder={t.retourePage.reasonPlaceholder} placeholder={t.retourePage.reasonPlaceholder}
className="mt-2 w-full border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-forest" className="mt-2 w-full border border-ink/20 bg-background px-4 py-3 text-sm focus:outline-none focus:border-heading"
/> />
</div> </div>
<Button type="submit">{t.retourePage.submit}</Button> <Button type="submit">{t.retourePage.submit}</Button>
+6 -6
View File
@@ -58,7 +58,7 @@ export default async function KontoBestellungenPage() {
> >
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<p className="text-forest">{order.orderNumber}</p> <p className="text-heading">{order.orderNumber}</p>
<p className="text-xs text-ink/65">{formatDate(order.createdAt, locale)}</p> <p className="text-xs text-ink/65">{formatDate(order.createdAt, locale)}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
@@ -81,7 +81,7 @@ export default async function KontoBestellungenPage() {
{order.items.map((item) => ( {order.items.map((item) => (
<li key={item.id}> <li key={item.id}>
{item.quantity} ×{" "} {item.quantity} ×{" "}
<Link href={`/honig/${item.product.slug}#bewertungen`} className="hover:text-forest hover:underline"> <Link href={`/honig/${item.product.slug}#bewertungen`} className="hover:text-heading hover:underline">
{item.productName} {item.productName}
</Link> </Link>
</li> </li>
@@ -116,7 +116,7 @@ export default async function KontoBestellungenPage() {
<> <>
{" "} {" "}
·{" "} ·{" "}
<a href={url} target="_blank" rel="noopener noreferrer" className="text-forest underline"> <a href={url} target="_blank" rel="noopener noreferrer" className="text-heading underline">
{t.shipmentInfo.trackPackage} {t.shipmentInfo.trackPackage}
</a> </a>
</> </>
@@ -132,12 +132,12 @@ export default async function KontoBestellungenPage() {
) : null} ) : null}
<div className="mt-4 flex flex-wrap items-center justify-between gap-4"> <div className="mt-4 flex flex-wrap items-center justify-between gap-4">
<p className="font-serif text-2xl text-forest tabular-nums">{formatPrice(order.totalCents, order.currency, locale)}</p> <p className="font-serif text-2xl text-heading tabular-nums">{formatPrice(order.totalCents, order.currency, locale)}</p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{order.invoice ? ( {order.invoice ? (
<Link <Link
href={`/rechnung/${order.invoice.invoiceNumber}`} href={`/rechnung/${order.invoice.invoiceNumber}`}
className="text-sm text-forest underline" className="text-sm text-heading underline"
> >
{t.ordersPage.viewInvoice} {t.ordersPage.viewInvoice}
</Link> </Link>
@@ -149,7 +149,7 @@ export default async function KontoBestellungenPage() {
) : RETURN_ELIGIBLE_STATUSES.includes(order.status) ? ( ) : RETURN_ELIGIBLE_STATUSES.includes(order.status) ? (
<Link <Link
href={`/konto/bestellungen/${order.id}/retoure`} href={`/konto/bestellungen/${order.id}/retoure`}
className="text-sm text-forest underline" className="text-sm text-heading underline"
> >
{t.ordersPage.requestReturn} {t.ordersPage.requestReturn}
</Link> </Link>
+10 -10
View File
@@ -35,7 +35,7 @@ export default async function KontoPage() {
<div className="mt-10 grid md:grid-cols-2 md:divide-x md:divide-ink/10 gap-12 md:gap-0"> <div className="mt-10 grid md:grid-cols-2 md:divide-x md:divide-ink/10 gap-12 md:gap-0">
<div className="md:pr-12"> <div className="md:pr-12">
<form action={loginAction} className="space-y-4"> <form action={loginAction} className="space-y-4">
<h2 className="text-3xl text-forest">{t.account.login}</h2> <h2 className="text-3xl text-heading">{t.account.login}</h2>
<Field name="email" label={t.account.emailLabel} type="email" required autoComplete="email" /> <Field name="email" label={t.account.emailLabel} type="email" required autoComplete="email" />
<PasswordField name="password" label={t.account.passwordLabel} required autoComplete="current-password" /> <PasswordField name="password" label={t.account.passwordLabel} required autoComplete="current-password" />
{/* Das Turnstile-Widget ist ein iframe mit FESTER Breite (rund {/* Das Turnstile-Widget ist ein iframe mit FESTER Breite (rund
@@ -50,7 +50,7 @@ export default async function KontoPage() {
) : null} ) : null}
<div className="flex flex-wrap items-center gap-x-4 gap-y-2"> <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<Button type="submit">{t.account.login}</Button> <Button type="submit">{t.account.login}</Button>
<Link href="/konto/passwort-vergessen" className="text-xs text-ink/65 hover:text-forest underline"> <Link href="/konto/passwort-vergessen" className="text-xs text-ink/65 hover:text-heading underline">
{t.account.forgotPassword} {t.account.forgotPassword}
</Link> </Link>
</div> </div>
@@ -72,7 +72,7 @@ export default async function KontoPage() {
</div> </div>
<a <a
href="/api/auth/entra/login" href="/api/auth/entra/login"
className="mt-6 inline-flex items-center justify-center gap-2 px-7 py-3 text-sm tracking-wide-caps transition-[color,background-color,border-color,transform] duration-300 active:scale-[0.97] bg-transparent text-forest border border-forest hover:bg-forest hover:text-cream" className="mt-6 inline-flex items-center justify-center gap-2 px-7 py-3 text-sm tracking-wide-caps transition-[color,background-color,border-color,transform] duration-300 active:scale-[0.97] bg-transparent text-heading border border-heading hover:bg-forest hover:text-cream"
> >
{t.account.ssoButton} {t.account.ssoButton}
</a> </a>
@@ -81,7 +81,7 @@ export default async function KontoPage() {
</div> </div>
<form action={registerAction} className="space-y-4 md:pl-12"> <form action={registerAction} className="space-y-4 md:pl-12">
<h2 className="text-3xl text-forest">{t.account.register}</h2> <h2 className="text-3xl text-heading">{t.account.register}</h2>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Field name="firstName" label={t.account.firstNameLabel} required autoComplete="given-name" /> <Field name="firstName" label={t.account.firstNameLabel} required autoComplete="given-name" />
<Field name="lastName" label={t.account.lastNameLabel} required autoComplete="family-name" /> <Field name="lastName" label={t.account.lastNameLabel} required autoComplete="family-name" />
@@ -128,7 +128,7 @@ export default async function KontoPage() {
{t.account.verifyEmailPrompt} {t.account.verifyEmailPrompt}
</p> </p>
<form action={resendVerificationEmailAction}> <form action={resendVerificationEmailAction}>
<button type="submit" className="text-forest underline whitespace-nowrap"> <button type="submit" className="text-heading underline whitespace-nowrap">
{t.account.resendVerification} {t.account.resendVerification}
</button> </button>
</form> </form>
@@ -174,7 +174,7 @@ export default async function KontoPage() {
<div className="mt-10 border border-ink/10 p-6 flex items-center justify-between gap-4"> <div className="mt-10 border border-ink/10 p-6 flex items-center justify-between gap-4">
<div> <div>
<h2 className="text-2xl text-forest">{t.account.newsletterHeading}</h2> <h2 className="text-2xl text-heading">{t.account.newsletterHeading}</h2>
<p className="mt-1 text-sm text-ink/70"> <p className="mt-1 text-sm text-ink/70">
{isSubscribed ? t.account.newsletterSubscribed : t.account.newsletterNotSubscribed} {isSubscribed ? t.account.newsletterSubscribed : t.account.newsletterNotSubscribed}
</p> </p>
@@ -188,7 +188,7 @@ export default async function KontoPage() {
<div className="mt-6 border border-ink/10 p-6 flex items-center justify-between gap-4"> <div className="mt-6 border border-ink/10 p-6 flex items-center justify-between gap-4">
<div> <div>
<h2 className="text-2xl text-forest">{t.account.logoutAllDevicesHeading}</h2> <h2 className="text-2xl text-heading">{t.account.logoutAllDevicesHeading}</h2>
<p className="mt-1 text-sm text-ink/70"> <p className="mt-1 text-sm text-ink/70">
{t.account.logoutAllDevicesDescription} {t.account.logoutAllDevicesDescription}
</p> </p>
@@ -206,10 +206,10 @@ export default async function KontoPage() {
</Button> </Button>
</form> </form>
<h2 className="text-2xl text-forest mb-4">{t.account.privacy.heading}</h2> <h2 className="text-2xl text-heading mb-4">{t.account.privacy.heading}</h2>
<div className="border border-ink/10 p-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center"> <div className="border border-ink/10 p-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div> <div>
<h3 className="text-lg text-forest">{t.account.privacy.downloadDataHeading}</h3> <h3 className="text-lg text-heading">{t.account.privacy.downloadDataHeading}</h3>
<p className="mt-1 text-sm text-ink/70">{t.account.privacy.downloadDataDescription}</p> <p className="mt-1 text-sm text-ink/70">{t.account.privacy.downloadDataDescription}</p>
</div> </div>
<LinkButton href="/api/konto/datenexport" variant="secondary" className="flex-shrink-0"> <LinkButton href="/api/konto/datenexport" variant="secondary" className="flex-shrink-0">
@@ -266,7 +266,7 @@ function Field({
type={type} type={type}
required={required} required={required}
autoComplete={autoComplete} autoComplete={autoComplete}
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> </label>
); );
+1 -1
View File
@@ -37,7 +37,7 @@ export default async function PasswortVergessenPage(props: PageProps<"/konto/pas
type="email" type="email"
required required
autoComplete="email" autoComplete="email"
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> </label>
{process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? ( {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? (
@@ -26,7 +26,7 @@ export default async function PasswortZuruecksetzenPage(
{!token ? ( {!token ? (
<p className="mt-6 text-sm text-ink/70"> <p className="mt-6 text-sm text-ink/70">
{t.passwortZuruecksetzenPage.incompleteLinkTextBefore} {t.passwortZuruecksetzenPage.incompleteLinkTextBefore}
<a href="/konto/passwort-vergessen" className="underline text-forest"> <a href="/konto/passwort-vergessen" className="underline text-heading">
{t.passwortZuruecksetzenPage.incompleteLinkLabel} {t.passwortZuruecksetzenPage.incompleteLinkLabel}
</a> </a>
{t.passwortZuruecksetzenPage.incompleteLinkTextAfter} {t.passwortZuruecksetzenPage.incompleteLinkTextAfter}
+1 -1
View File
@@ -38,7 +38,7 @@ export default async function ZweiFaktorCodePage() {
autoComplete="one-time-code" autoComplete="one-time-code"
autoFocus autoFocus
required required
className="mt-2 w-full border border-ink/20 bg-background px-4 py-4 text-center font-serif text-3xl tracking-[0.4em] tabular-nums focus:outline-none focus:border-forest" className="mt-2 w-full border border-ink/20 bg-background px-4 py-4 text-center font-serif text-3xl tracking-[0.4em] tabular-nums focus:outline-none focus:border-heading"
/> />
</label> </label>
{process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? ( {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? (
@@ -30,7 +30,7 @@ export default async function ZweiFaktorBackupCodesPage() {
{codes.map((code, i) => ( {codes.map((code, i) => (
<li <li
key={code} key={code}
className="flex items-center gap-3 border border-ink/10 bg-cream/50 px-3 py-3 font-mono text-sm tabular-nums tracking-wider text-forest" className="flex items-center gap-3 border border-ink/10 bg-surface/50 px-3 py-3 font-mono text-sm tabular-nums tracking-wider text-heading"
> >
<span className="text-[10px] text-ink/45 tabular-nums">{String(i + 1).padStart(2, "0")}</span> <span className="text-[10px] text-ink/45 tabular-nums">{String(i + 1).padStart(2, "0")}</span>
{code} {code}
@@ -50,7 +50,7 @@ export default async function ZweiFaktorEinrichtenPage() {
inputMode="numeric" inputMode="numeric"
autoComplete="one-time-code" autoComplete="one-time-code"
required required
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 tracking-widest focus:outline-none focus:border-forest" className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 tracking-widest focus:outline-none focus:border-heading"
/> />
</label> </label>
<Button type="submit">{t.twoFactorSetupPage.activate}</Button> <Button type="submit">{t.twoFactorSetupPage.activate}</Button>
+2 -2
View File
@@ -36,7 +36,7 @@ export default async function ZweiFaktorPage() {
{customer.twoFactorEnabled ? ( {customer.twoFactorEnabled ? (
<div className="mt-8 space-y-6"> <div className="mt-8 space-y-6">
<p className="flex items-center gap-3 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3"> <p className="flex items-center gap-3 text-sm text-heading bg-forest/5 border border-heading/20 px-4 py-3">
<ShieldIcon /> <ShieldIcon />
<span> <span>
{t.twoFactorPage.activeStatus(remainingBackupCodes)} {t.twoFactorPage.activeStatus(remainingBackupCodes)}
@@ -44,7 +44,7 @@ export default async function ZweiFaktorPage() {
</p> </p>
<form action={disableTwoFactorAction} className="space-y-4 border border-ink/10 p-6"> <form action={disableTwoFactorAction} className="space-y-4 border border-ink/10 p-6">
<h2 className="text-forest">{t.twoFactorPage.disableHeading}</h2> <h2 className="text-heading">{t.twoFactorPage.disableHeading}</h2>
<PasswordField <PasswordField
name="password" name="password"
label={t.twoFactorPage.passwordConfirmLabel} label={t.twoFactorPage.passwordConfirmLabel}
+12 -1
View File
@@ -15,6 +15,8 @@ import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { I18nProvider } from "@/i18n/i18n-provider"; import { I18nProvider } from "@/i18n/i18n-provider";
import { getBrandAssets } from "@/lib/brand"; import { getBrandAssets } from "@/lib/brand";
import { cookies } from "next/headers";
import { THEME_COOKIE, parseThemeChoice } from "@/lib/theme";
const cormorant = Cormorant_Garamond({ const cormorant = Cormorant_Garamond({
variable: "--font-cormorant", variable: "--font-cormorant",
@@ -48,6 +50,9 @@ export async function generateMetadata(): Promise<Metadata> {
export default async function RootLayout({ children }: LayoutProps<"/">) { export default async function RootLayout({ children }: LayoutProps<"/">) {
const locale = await getLocale(); const locale = await getLocale();
const dict = getDictionary(locale); const dict = getDictionary(locale);
// Gewaehlte Darstellung schon serverseitig setzen — sonst blitzte die Seite
// beim Laden hell auf, bevor ein Skript auf dunkel umschalten koennte.
const themeChoice = parseThemeChoice((await cookies()).get(THEME_COOKIE)?.value);
const [company, socialLinks, flash, brand] = await Promise.all([ const [company, socialLinks, flash, brand] = await Promise.all([
getCompanyInfo(), getCompanyInfo(),
getSocialLinks(), getSocialLinks(),
@@ -95,6 +100,7 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
return ( return (
<html <html
lang={locale} lang={locale}
data-theme={themeChoice === "system" ? undefined : themeChoice}
data-scroll-behavior="smooth" data-scroll-behavior="smooth"
className={`${cormorant.variable} ${inter.variable} h-full antialiased`} className={`${cormorant.variable} ${inter.variable} h-full antialiased`}
> >
@@ -137,7 +143,12 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
{dict.nav.skipToContent} {dict.nav.skipToContent}
</a> </a>
<HideOnAdmin> <HideOnAdmin>
<Header companyName={company.name} logoSrc={brand.logo} logoInvertedSrc={brand.logoInverted} /> <Header
companyName={company.name}
logoSrc={brand.logo}
logoInvertedSrc={brand.logoInverted}
themeChoice={themeChoice}
/>
</HideOnAdmin> </HideOnAdmin>
<main id="main-content" className="flex-1"> <main id="main-content" className="flex-1">
{children} {children}
+1 -1
View File
@@ -44,7 +44,7 @@ export default async function NewsletterBestaetigtPage(props: PageProps<"/newsle
<span aria-hidden="true" className="absolute -left-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-background" /> <span aria-hidden="true" className="absolute -left-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-background" />
<span aria-hidden="true" className="absolute -right-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-background" /> <span aria-hidden="true" className="absolute -right-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-background" />
<p className="text-sm text-ink/70">{t.newsletterBestaetigtPage.discountHeading}</p> <p className="text-sm text-ink/70">{t.newsletterBestaetigtPage.discountHeading}</p>
<p className="mt-2 font-serif text-2xl text-forest"> <p className="mt-2 font-serif text-2xl text-heading">
{t.newsletterBestaetigtPage.discountText(NEWSLETTER_DISCOUNT_PERCENT)} {t.newsletterBestaetigtPage.discountText(NEWSLETTER_DISCOUNT_PERCENT)}
</p> </p>
<p className="mt-4 font-mono text-3xl tracking-[0.2em] text-honey-dark select-all">{NEWSLETTER_DISCOUNT_CODE}</p> <p className="mt-4 font-mono text-3xl tracking-[0.2em] text-honey-dark select-all">{NEWSLETTER_DISCOUNT_CODE}</p>
+4 -4
View File
@@ -41,14 +41,14 @@ export default async function InvoicePage(props: PageProps<"/rechnung/[invoiceNu
<BackLink href="/konto/bestellungen" label={t.invoicePage.backToOrders} /> <BackLink href="/konto/bestellungen" label={t.invoicePage.backToOrders} />
<a <a
href={`/rechnung/${invoice.invoiceNumber}/pdf`} href={`/rechnung/${invoice.invoiceNumber}/pdf`}
className="mb-6 text-sm text-forest underline" className="mb-6 text-sm text-heading underline"
> >
{t.invoicePage.downloadPdf} {t.invoicePage.downloadPdf}
</a> </a>
</div> </div>
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<p className="text-2xl tracking-[0.2em] font-serif text-forest">{company.name.toUpperCase()}</p> <p className="text-2xl tracking-[0.2em] font-serif text-heading">{company.name.toUpperCase()}</p>
<p className="mt-2 text-sm text-ink/70"> <p className="mt-2 text-sm text-ink/70">
{company.address.street} {company.address.street}
<br /> <br />
@@ -56,7 +56,7 @@ export default async function InvoicePage(props: PageProps<"/rechnung/[invoiceNu
</p> </p>
</div> </div>
<div className="text-right text-sm text-ink/70"> <div className="text-right text-sm text-ink/70">
<p className="text-forest">{t.invoicePage.invoiceLabel(invoice.invoiceNumber)}</p> <p className="text-heading">{t.invoicePage.invoiceLabel(invoice.invoiceNumber)}</p>
<p>{t.invoicePage.orderLabel(order.orderNumber)}</p> <p>{t.invoicePage.orderLabel(order.orderNumber)}</p>
<p>{formatDate(invoice.issuedAt)}</p> <p>{formatDate(invoice.issuedAt)}</p>
</div> </div>
@@ -126,7 +126,7 @@ export default async function InvoicePage(props: PageProps<"/rechnung/[invoiceNu
</div> </div>
</div> </div>
))} ))}
<div className="flex justify-between text-forest pt-2 border-t border-ink/10"> <div className="flex justify-between text-heading pt-2 border-t border-ink/10">
<span>{t.invoicePage.totalLabel}</span> <span>{t.invoicePage.totalLabel}</span>
<span>{formatPrice(invoice.totalCents, order.currency)}</span> <span>{formatPrice(invoice.totalCents, order.currency)}</span>
</div> </div>
+9 -9
View File
@@ -109,8 +109,8 @@ export default async function ShopPage(props: PageProps<"/shop">) {
aria-current={isActive ? "page" : undefined} aria-current={isActive ? "page" : undefined}
className={`inline-flex h-11 items-center gap-2 px-5 text-sm tracking-wide-caps border transition-[color,background-color,border-color] duration-300 ${ className={`inline-flex h-11 items-center gap-2 px-5 text-sm tracking-wide-caps border transition-[color,background-color,border-color] duration-300 ${
isActive isActive
? "bg-forest text-cream border-forest" ? "bg-forest text-cream border-heading"
: "border-ink/15 text-ink/70 hover:border-forest hover:text-forest" : "border-ink/15 text-ink/70 hover:border-heading hover:text-heading"
}`} }`}
> >
{isActive ? <HexMark className="h-2.5 w-2.5 text-honey-light" /> : null} {isActive ? <HexMark className="h-2.5 w-2.5 text-honey-light" /> : null}
@@ -151,7 +151,7 @@ export default async function ShopPage(props: PageProps<"/shop">) {
name="q" name="q"
defaultValue={query ?? ""} defaultValue={query ?? ""}
placeholder={t.nav.searchPlaceholder} placeholder={t.nav.searchPlaceholder}
className="h-11 w-60 border border-ink/15 bg-background pl-10 pr-3 transition-colors placeholder:text-ink/45 hover:border-ink/35 focus:border-forest focus:outline-none" className="h-11 w-60 border border-ink/15 bg-background pl-10 pr-3 transition-colors placeholder:text-ink/45 hover:border-ink/35 focus:border-heading focus:outline-none"
/> />
</span> </span>
<label htmlFor="sortierung" className="ml-2 text-xs tracking-wide-caps text-ink/60"> <label htmlFor="sortierung" className="ml-2 text-xs tracking-wide-caps text-ink/60">
@@ -162,7 +162,7 @@ export default async function ShopPage(props: PageProps<"/shop">) {
id="sortierung" id="sortierung"
name="sortierung" name="sortierung"
defaultValue={sort ?? ""} defaultValue={sort ?? ""}
className="h-11 appearance-none border border-ink/15 bg-background pl-3 pr-9 transition-colors hover:border-ink/35 focus:border-forest focus:outline-none" className="h-11 appearance-none border border-ink/15 bg-background pl-3 pr-9 transition-colors hover:border-ink/35 focus:border-heading focus:outline-none"
> >
{sortOptions.map((option) => ( {sortOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -183,7 +183,7 @@ export default async function ShopPage(props: PageProps<"/shop">) {
</span> </span>
<button <button
type="submit" type="submit"
className="h-11 bg-forest px-5 text-xs tracking-wide-caps text-cream transition-colors hover:bg-honey-dark" className="h-11 bg-forest px-5 text-xs tracking-wide-caps text-cream transition-colors hover:bg-honey-deep"
> >
{t.shop.apply} {t.shop.apply}
</button> </button>
@@ -198,8 +198,8 @@ export default async function ShopPage(props: PageProps<"/shop">) {
aria-pressed={onlyAvailable} aria-pressed={onlyAvailable}
className={`ml-auto inline-flex h-11 items-center gap-2.5 px-4 text-xs tracking-wide-caps border transition-colors ${ className={`ml-auto inline-flex h-11 items-center gap-2.5 px-4 text-xs tracking-wide-caps border transition-colors ${
onlyAvailable onlyAvailable
? "bg-forest text-cream border-forest" ? "bg-forest text-cream border-heading"
: "border-ink/15 text-ink/70 hover:border-forest hover:text-forest" : "border-ink/15 text-ink/70 hover:border-heading hover:text-heading"
}`} }`}
> >
{/* Kleiner Schalter statt reinem Textknopf — der Zustand ist auf einen Blick lesbar. */} {/* Kleiner Schalter statt reinem Textknopf — der Zustand ist auf einen Blick lesbar. */}
@@ -208,7 +208,7 @@ export default async function ShopPage(props: PageProps<"/shop">) {
className={`relative h-4 w-7 rounded-full transition-colors ${onlyAvailable ? "bg-honey" : "bg-ink/15"}`} className={`relative h-4 w-7 rounded-full transition-colors ${onlyAvailable ? "bg-honey" : "bg-ink/15"}`}
> >
<span <span
className={`absolute top-0.5 h-3 w-3 rounded-full bg-cream shadow transition-[left] duration-300 ${ className={`absolute top-0.5 h-3 w-3 rounded-full bg-surface shadow transition-[left] duration-300 ${
onlyAvailable ? "left-3.5" : "left-0.5" onlyAvailable ? "left-3.5" : "left-0.5"
}`} }`}
/> />
@@ -222,7 +222,7 @@ export default async function ShopPage(props: PageProps<"/shop">) {
{t.shop.searchResults(query, products.length)}{" "} {t.shop.searchResults(query, products.length)}{" "}
<Link <Link
href={buildHref({ kategorie: category, sortierung: sort, verfuegbar: onlyAvailable ? "1" : undefined })} href={buildHref({ kategorie: category, sortierung: sort, verfuegbar: onlyAvailable ? "1" : undefined })}
className="underline hover:text-forest" className="underline hover:text-heading"
> >
{t.shop.clearSearch} {t.shop.clearSearch}
</Link> </Link>
+1 -1
View File
@@ -49,7 +49,7 @@ export default async function UnternehmenPage() {
{/* Geteilt: die Ueberschrift laeuft neben dem langen Formular mit. */} {/* Geteilt: die Ueberschrift laeuft neben dem langen Formular mit. */}
<div className="mt-24 grid gap-10 lg:grid-cols-12 lg:gap-16"> <div className="mt-24 grid gap-10 lg:grid-cols-12 lg:gap-16">
<ScrollReveal className="lg:col-span-4"> <ScrollReveal className="lg:col-span-4">
<h2 className="text-4xl leading-tight text-forest lg:sticky lg:top-32"> <h2 className="text-4xl leading-tight text-heading lg:sticky lg:top-32">
{t.unternehmenPage.inquiryHeading} {t.unternehmenPage.inquiryHeading}
</h2> </h2>
</ScrollReveal> </ScrollReveal>
+7 -7
View File
@@ -76,7 +76,7 @@ export function WarenkorbContent({
// verfuegbaren Honige, statt nur eines Satzes und eines Knopfs. // verfuegbaren Honige, statt nur eines Satzes und eines Knopfs.
<div className="mt-14 grid gap-12 lg:grid-cols-12 lg:gap-16"> <div className="mt-14 grid gap-12 lg:grid-cols-12 lg:gap-16">
<div className="lg:col-span-4"> <div className="lg:col-span-4">
<p className="font-serif text-2xl leading-snug text-forest">{t.cart.empty}</p> <p className="font-serif text-2xl leading-snug text-heading">{t.cart.empty}</p>
<LinkButton href="/shop" variant="secondary" className="mt-6"> <LinkButton href="/shop" variant="secondary" className="mt-6">
{t.cartPage.goToShop} {t.cartPage.goToShop}
</LinkButton> </LinkButton>
@@ -106,7 +106,7 @@ export function WarenkorbContent({
> >
<CartItemThumbnail image={item.image} className="w-20 h-20 flex-shrink-0 rounded-lg" /> <CartItemThumbnail image={item.image} className="w-20 h-20 flex-shrink-0 rounded-lg" />
<div className="flex-1 min-w-[160px]"> <div className="flex-1 min-w-[160px]">
<Link href={`/honig/${item.slug}`} className="link-draw pb-0.5 font-serif text-2xl text-forest"> <Link href={`/honig/${item.slug}`} className="link-draw pb-0.5 font-serif text-2xl text-heading">
{item.name} {item.name}
</Link> </Link>
<p className="text-sm text-ink/65">{item.weightGrams} g</p> <p className="text-sm text-ink/65">{item.weightGrams} g</p>
@@ -127,7 +127,7 @@ export function WarenkorbContent({
onChange={(quantity) => setQuantity(item.productId, quantity)} onChange={(quantity) => setQuantity(item.productId, quantity)}
max={itemStock ?? 10} max={itemStock ?? 10}
/> />
<span className="w-24 text-right font-serif text-xl text-forest tabular-nums"> <span className="w-24 text-right font-serif text-xl text-heading tabular-nums">
{formatPrice(item.priceCents * item.quantity, "EUR", locale)} {formatPrice(item.priceCents * item.quantity, "EUR", locale)}
</span> </span>
<button <button
@@ -166,7 +166,7 @@ export function WarenkorbContent({
initial={{ opacity: 0, scale: 0.96 }} initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }} transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="flex items-center justify-between rounded-sm bg-honey/10 px-3 py-2 text-forest" className="flex items-center justify-between rounded-sm bg-honey/10 px-3 py-2 text-heading"
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<HexCheck className="h-4 w-4 text-honey-dark" /> <HexCheck className="h-4 w-4 text-honey-dark" />
@@ -193,7 +193,7 @@ export function WarenkorbContent({
</div> </div>
</div> </div>
) : null} ) : null}
<div className="flex items-baseline justify-between text-forest pt-4 border-t border-ink/10"> <div className="flex items-baseline justify-between text-heading pt-4 border-t border-ink/10">
<span className="text-base">{t.cartPage.total}</span> <span className="text-base">{t.cartPage.total}</span>
<motion.span <motion.span
key={subtotal - bulkDiscountCents + shipping} key={subtotal - bulkDiscountCents + shipping}
@@ -208,7 +208,7 @@ export function WarenkorbContent({
<LinkButton href="/checkout" className="w-full mt-6"> <LinkButton href="/checkout" className="w-full mt-6">
{t.cart.checkout} {t.cart.checkout}
</LinkButton> </LinkButton>
<Link href="/versand-zahlung" className="mt-4 block text-center text-xs text-ink/70 hover:text-forest underline"> <Link href="/versand-zahlung" className="mt-4 block text-center text-xs text-ink/70 hover:text-heading underline">
{t.cartPage.pickupHint} {t.cartPage.pickupHint}
</Link> </Link>
<TrustBadges className="mt-6 pt-6 border-t border-ink/10" /> <TrustBadges className="mt-6 pt-6 border-t border-ink/10" />
@@ -218,7 +218,7 @@ export function WarenkorbContent({
{items.length > 0 && crossSellProducts.length > 0 ? ( {items.length > 0 && crossSellProducts.length > 0 ? (
<div className="mt-20"> <div className="mt-20">
<h2 className="text-3xl md:text-4xl text-forest mb-10">{t.productPage.relatedProductsHeading}</h2> <h2 className="text-3xl md:text-4xl text-heading mb-10">{t.productPage.relatedProductsHeading}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
{crossSellProducts.map((product, index) => ( {crossSellProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} /> <ProductCard key={product.id} product={product} index={index} />
+3 -3
View File
@@ -80,7 +80,7 @@ export default async function WissenArticlePage(
<Container className="max-w-2xl"> <Container className="max-w-2xl">
<BackLink href="/wissen" label={t.wissenArticlePage.backLabel} /> <BackLink href="/wissen" label={t.wissenArticlePage.backLabel} />
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.wissenArticlePage.eyebrow}</p> <p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.wissenArticlePage.eyebrow}</p>
<h1 className="text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] text-forest">{article.title}</h1> <h1 className="text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] text-heading">{article.title}</h1>
<div className="mt-10 max-w-[65ch] space-y-6 text-lg text-ink/80 leading-[1.75]"> <div className="mt-10 max-w-[65ch] space-y-6 text-lg text-ink/80 leading-[1.75]">
{article.body.map((paragraph, i) => ( {article.body.map((paragraph, i) => (
// Initiale wie im Buch: der erste Buchstabe des Artikels gross in Honig. // Initiale wie im Buch: der erste Buchstabe des Artikels gross in Honig.
@@ -99,13 +99,13 @@ export default async function WissenArticlePage(
return ( return (
<Link key={otherSlug} href={`/wissen/${otherSlug}`} className="group flex items-start justify-between gap-6 py-6"> <Link key={otherSlug} href={`/wissen/${otherSlug}`} className="group flex items-start justify-between gap-6 py-6">
<span> <span>
<span className="link-draw pb-0.5 font-serif text-2xl text-forest">{other.title}</span> <span className="link-draw pb-0.5 font-serif text-2xl text-heading">{other.title}</span>
<span className="mt-2 block text-sm text-ink/65 leading-relaxed">{other.excerpt}</span> <span className="mt-2 block text-sm text-ink/65 leading-relaxed">{other.excerpt}</span>
</span> </span>
<svg <svg
aria-hidden="true" aria-hidden="true"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="mt-2 h-4 w-4 flex-shrink-0 text-forest transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1" className="mt-2 h-4 w-4 flex-shrink-0 text-heading transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.6" strokeWidth="1.6"
+1 -1
View File
@@ -36,7 +36,7 @@ export default async function WissenPage() {
href={`/wissen/${slug}`} href={`/wissen/${slug}`}
className="group grid gap-3 py-8 md:grid-cols-12 md:items-baseline md:gap-8" className="group grid gap-3 py-8 md:grid-cols-12 md:items-baseline md:gap-8"
> >
<h2 className="text-2xl leading-tight text-forest text-balance md:col-span-5 md:text-3xl"> <h2 className="text-2xl leading-tight text-heading text-balance md:col-span-5 md:text-3xl">
<span className="link-draw pb-0.5">{article.title}</span> <span className="link-draw pb-0.5">{article.title}</span>
</h2> </h2>
<p className="max-w-[60ch] text-ink/70 leading-relaxed md:col-span-6">{article.excerpt}</p> <p className="max-w-[60ch] text-ink/70 leading-relaxed md:col-span-6">{article.excerpt}</p>
+10 -10
View File
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
/** /**
* Verlinkte Karte fuer die Konto-Uebersicht (Bestellungen, Adressen, ...). * 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. * Kartenstil, der auf jeder Konto-Seite gleich aussah.
*/ */
export function AccountNavCard({ 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 ${ 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 accent
? "border-honey/40 bg-honey/5 hover:border-honey hover:shadow-[0_22px_40px_-24px_rgba(184,138,68,0.45)]" ? "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}`} } ${className}`}
> >
{/* Icon in einer Wabe, die sich beim Hover mit Honig fuellt. */} {/* Icon in einer Wabe, die sich beim Hover mit Honig fuellt. */}
@@ -39,20 +39,20 @@ export function AccountNavCard({
/> />
<span <span
className={`relative transition-colors duration-500 group-hover:text-cream ${ className={`relative transition-colors duration-500 group-hover:text-cream ${
accent ? "text-honey-dark" : "text-forest" accent ? "text-honey-dark" : "text-heading"
}`} }`}
> >
{icon} {icon}
</span> </span>
</span> </span>
<span className="flex-1 pt-1"> <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 className="block mt-1.5 text-sm text-ink/70">{description}</span>
</span> </span>
<svg <svg
aria-hidden="true" aria-hidden="true"
viewBox="0 0 24 24" 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" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.6" strokeWidth="1.6"
@@ -68,9 +68,9 @@ const ORDER_STATUS_STYLES: Record<string, string> = {
BEZAHLT: "border-honey/40 text-honey-dark bg-honey/5", BEZAHLT: "border-honey/40 text-honey-dark bg-honey/5",
IN_VORBEREITUNG: "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", VERSANDBEREIT: "border-honey/40 text-honey-dark bg-honey/5",
VERSENDET: "border-forest/40 text-forest bg-forest/5", VERSENDET: "border-heading/40 text-heading bg-forest/5",
ABHOLBEREIT: "border-forest/40 text-forest bg-forest/5", ABHOLBEREIT: "border-heading/40 text-heading bg-forest/5",
ABGEHOLT: "border-forest/40 text-forest bg-forest/5", ABGEHOLT: "border-heading/40 text-heading bg-forest/5",
STORNIERT: "border-red-200 text-red-700 bg-red-50", STORNIERT: "border-red-200 text-red-700 bg-red-50",
}; };
@@ -182,7 +182,7 @@ export function OrderStatusTimeline({
</span> </span>
) : null} ) : null}
</div> </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} {step.label}
</p> </p>
</div> </div>
@@ -218,7 +218,7 @@ export function AccountEmptyState({
</svg> </svg>
<span className="relative">{icon}</span> <span className="relative">{icon}</span>
</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 ? ( {description ? (
<p className="mt-1 text-sm text-ink/70 max-w-sm mx-auto">{description}</p> <p className="mt-1 text-sm text-ink/70 max-w-sm mx-auto">{description}</p>
) : null} ) : null}
+2 -2
View File
@@ -32,7 +32,7 @@ export function AdminArchiveTabs({
const tabClass = (selected: boolean): string => const tabClass = (selected: boolean): string =>
`px-4 py-2 text-sm border-b-2 -mb-px transition-colors ${ `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 ( return (
@@ -72,7 +72,7 @@ export function AdminArchiveToggle({
{archivedManually ? ` · ${labels.manualHint}` : null} {archivedManually ? ` · ${labels.manualHint}` : null}
</span> </span>
) : null} ) : 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} {archivedAt ? labels.restore : labels.archive}
</button> </button>
</form> </form>
+1 -1
View File
@@ -148,7 +148,7 @@ export function AdminCommandPalette({
onMouseEnter={() => setActive(i)} onMouseEnter={() => setActive(i)}
onClick={() => go(entry)} onClick={() => go(entry)}
className={`flex w-full items-center gap-3 px-5 py-2.5 text-left text-sm transition-colors ${ 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 <HexMark
+1 -1
View File
@@ -102,7 +102,7 @@ export function AdminFieldset({
}) { }) {
return ( return (
<fieldset className={`space-y-4 ${last ? "" : "border-b border-ink/10 pb-8"} ${className}`}> <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} {children}
</fieldset> </fieldset>
); );
+1 -1
View File
@@ -95,7 +95,7 @@ export function AdminImageField({ name, label, initialValue }: AdminImageFieldPr
type="button" type="button"
onClick={() => inputRef.current?.click()} onClick={() => inputRef.current?.click()}
disabled={status === "uploading"} 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} {t.adminImageField.chooseFile}
</button> </button>
+1 -1
View File
@@ -111,7 +111,7 @@ export function AdminImageListField({ name, label, initialValues }: AdminImageLi
type="button" type="button"
onClick={() => inputRef.current?.click()} onClick={() => inputRef.current?.click()}
disabled={status === "uploading"} 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} {t.adminImageField.chooseFile}
</button> </button>
+5 -5
View File
@@ -20,7 +20,7 @@ export function AdminPageHeader({
return ( return (
<div className="mb-8"> <div className="mb-8">
<div className="flex flex-wrap items-center justify-between gap-4"> <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 ? ( {action ? (
<LinkButton href={action.href} className="px-5 py-2.5"> <LinkButton href={action.href} className="px-5 py-2.5">
{action.label} {action.label}
@@ -74,7 +74,7 @@ export function AdminSearchForm({
{children} {children}
<button <button
type="submit" 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} {submitLabel}
</button> </button>
@@ -90,7 +90,7 @@ export function AdminSearchForm({
export type AdminBadgeTone = "positive" | "pending" | "neutral" | "danger"; export type AdminBadgeTone = "positive" | "pending" | "neutral" | "danger";
const BADGE_TONES: Record<AdminBadgeTone, string> = { 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", pending: "border-honey/40 bg-honey/5 text-honey-dark",
neutral: "border-ink/20 text-ink/70", neutral: "border-ink/20 text-ink/70",
danger: "border-red-200 bg-red-50 text-red-700", 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 }) { export function AdminActionLink({ href, children }: { href: string; children: ReactNode }) {
return ( return (
<Link href={href} className="link-draw text-forest"> <Link href={href} className="link-draw text-heading">
{children} {children}
</Link> </Link>
); );
} }
const ACTION_TONES = { const ACTION_TONES = {
default: "text-forest", default: "text-heading",
muted: "text-ink/70", muted: "text-ink/70",
danger: "text-red-700", danger: "text-red-700",
} as const; } 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 { AdminCommandPalette, type PaletteEntry } from "@/components/admin-command-palette";
import { useI18n } from "@/i18n/i18n-provider"; import { useI18n } from "@/i18n/i18n-provider";
import type { Dictionary } from "@/i18n/dictionary-type"; 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). */ /** Was an einer Seite gerade auf Bearbeitung wartet (Zaehler in der Seitenleiste). */
export type AdminBadges = Partial<Record<string, number>>; export type AdminBadges = Partial<Record<string, number>>;
@@ -118,7 +120,7 @@ function NavLinks({
{active ? ( {active ? (
<motion.span <motion.span
layoutId="admin-nav-active" 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 }} transition={{ type: "spring", stiffness: 420, damping: 36 }}
/> />
) : null} ) : null}
@@ -131,7 +133,7 @@ function NavLinks({
<span className="flex-1">{item.label}</span> <span className="flex-1">{item.label}</span>
{count > 0 ? ( {count > 0 ? (
<span <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)} title={waitingLabel(count)}
> >
<span aria-hidden="true">{count}</span> <span aria-hidden="true">{count}</span>
@@ -175,6 +177,8 @@ function PaletteTrigger({ onClick, label, shortcut }: { onClick: () => void; lab
interface AdminShellProps { interface AdminShellProps {
adminName: string; adminName: string;
/** Gewaehlte Darstellung (Cookie), siehe lib/theme.ts. */
themeChoice?: ThemeChoice;
/** Logo fuer dunkle Flaechen, siehe lib/brand.ts. */ /** Logo fuer dunkle Flaechen, siehe lib/brand.ts. */
logoSrc?: string | null; logoSrc?: string | null;
badges?: AdminBadges; badges?: AdminBadges;
@@ -190,7 +194,14 @@ interface AdminShellProps {
* Shop-Kopf- und -Fusszeile blendet das Root-Layout hier aus * Shop-Kopf- und -Fusszeile blendet das Root-Layout hier aus
* (components/hide-on-admin.tsx). * (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 { t } = useI18n();
const groups = getAdminNavGroups(t); const groups = getAdminNavGroups(t);
const pathname = usePathname(); 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"> <div className="px-5 py-4 border-t border-cream/10">
{/* Der Shop-Kopf ist im Admin ausgeblendet (HideOnAdmin) — ohne diesen {/* Der Shop-Kopf ist im Admin ausgeblendet (HideOnAdmin) — ohne diesen
Link fuehrte kein Weg zurueck auf die Website. */} Link fuehrte kein Weg zurueck auf die Website. */}
<div className="mb-4 flex items-center justify-between gap-3">
<Link <Link
href="/" 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 <svg
width="14" width="14"
@@ -227,6 +239,8 @@ export function AdminShell({ adminName, logoSrc = null, badges = {}, logoutActio
</svg> </svg>
<span className="link-draw">{t.adminShell.backToWebsite}</span> <span className="link-draw">{t.adminShell.backToWebsite}</span>
</Link> </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> <p className="text-xs text-cream/60 truncate">{t.adminShell.loggedInAs(adminName)}</p>
<form action={logoutAction} className="mt-2"> <form action={logoutAction} className="mt-2">
<button type="submit" className="text-xs text-cream/80 underline hover:text-cream"> <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 ( 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"> <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" /> <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"> <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" type="button"
onClick={() => inputRef.current?.click()} onClick={() => inputRef.current?.click()}
disabled={status === "uploading"} 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} {t.adminVideoField.chooseFile}
</button> </button>
+2 -2
View File
@@ -48,7 +48,7 @@ export async function ApiCoreSyncResult({ searchParams }: { searchParams: SyncSe
</p> </p>
) : null} ) : null}
{done ? ( {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> <p>
{t.resultHarvests( {t.resultHarvests(
count(searchParams.hNew), count(searchParams.hNew),
@@ -92,7 +92,7 @@ export async function ApiCoreSyncPanel({ returnTo }: { returnTo: string }) {
return ( return (
<div className="bg-background border border-ink/10 p-6"> <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> <p className="text-sm text-ink/65 mb-4">{t.intro}</p>
<form action={syncApiCoreAction}> <form action={syncApiCoreAction}>
<input type="hidden" name="returnTo" value={returnTo} /> <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 ( return (
<Link <Link
href={href} 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"> <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" /> <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> </defs>
{/* Fluegel — kippen um ihre Unterkante */} {/* 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]"> <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="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="#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="var(--color-heading)" strokeWidth="1.1" />
</g> </g>
{/* Stachel */} {/* Stachel */}
<path d="M9.6 19.2 5.2 20l4.4.9z" fill="#24352B" /> <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> </g>
<ellipse cx="20" cy="20" rx="10.5" ry="6.8" fill="none" stroke="#24352B" strokeWidth="1.2" /> <ellipse cx="20" cy="20" rx="10.5" ry="6.8" fill="none" stroke="#24352B" strokeWidth="1.2" />
{/* Kopf und Fuehler */} {/* Kopf und Fuehler */}
<circle cx="32" cy="18.8" r="4.3" fill="#24352B" /> <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="#24352B" strokeWidth="1" strokeLinecap="round" /> <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> </svg>
); );
} }
+5 -5
View File
@@ -65,7 +65,7 @@ export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: s
if (status === "success") { if (status === "success") {
return ( return (
<p className="text-forest"> <p className="text-heading">
{t.businessInquiryForm.successMessage} {t.businessInquiryForm.successMessage}
</p> </p>
); );
@@ -95,14 +95,14 @@ export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: s
{t.unternehmenPage.packages.map((p, i) => ( {t.unternehmenPage.packages.map((p, i) => (
<label <label
key={p.key} 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" /> <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="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 <span
aria-hidden="true" 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>
<span className="mt-2 text-ink/70 leading-relaxed">{p.text}</span> <span className="mt-2 text-ink/70 leading-relaxed">{p.text}</span>
@@ -151,7 +151,7 @@ function TextField({
name={name} name={name}
type={type} type={type}
required={required} 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> </label>
); );
+1 -1
View File
@@ -25,7 +25,7 @@ export function CopyCodeButton({ code, label, copiedLabel }: { code: string; lab
<button <button
type="button" type="button"
onClick={copy} 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" aria-live="polite"
> >
<AnimatePresence mode="wait" initial={false}> <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 AUTO_HIDE_MS: Record<Flash["kind"], number> = { success: 6000, info: 8000, error: 12000 };
const STYLES: Record<Flash["kind"], string> = { 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", info: "bg-background text-ink border-ink/20",
error: "bg-red-50 text-red-800 border-red-300", error: "bg-red-50 text-red-800 border-red-300",
}; };
+4 -4
View File
@@ -73,7 +73,7 @@ export async function OriginStory() {
return ( return (
// Randlos geteilt: das Foto fuellt die linke Haelfte bis an den // Randlos geteilt: das Foto fuellt die linke Haelfte bis an den
// Fensterrand, der Text steht daneben — kein Rahmen, keine Karte. // 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"> <ScrollReveal className="h-full">
<ParallaxMedia className="h-full min-h-[24rem] md:min-h-[40rem]"> <ParallaxMedia className="h-full min-h-[24rem] md:min-h-[40rem]">
<ContentImage <ContentImage
@@ -129,7 +129,7 @@ export async function Philosophy() {
delay={i * 0.06} 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" 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> <p className="mt-3 max-w-[34ch] text-sm leading-relaxed text-ink/70">{item.text}</p>
</ScrollReveal> </ScrollReveal>
))} ))}
@@ -151,7 +151,7 @@ export async function BeekeeperNote() {
const [content, company] = await Promise.all([getContentValues(["imkerei.image"], locale), getCompanyInfo()]); const [content, company] = await Promise.all([getContentValues(["imkerei.image"], locale), getCompanyInfo()]);
const quote = t.imkereiPage.paragraphs[0]; const quote = t.imkereiPage.paragraphs[0];
return ( 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" /> <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"> <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"> <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>
<ScrollReveal delay={0.1} className="md:col-span-7"> <ScrollReveal delay={0.1} className="md:col-span-7">
<blockquote> <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> <span aria-hidden="true" className="mr-1 text-honey">„</span>
{quote} {quote}
<span aria-hidden="true" className="ml-0.5 text-honey">“</span> <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 content = await getContentValues(["home.processTimeline.steps"], locale);
const steps = parseListNumbered(content["home.processTimeline.steps"]); const steps = parseListNumbered(content["home.processTimeline.steps"]);
return ( return (
<section className="py-24 bg-cream/60"> <section className="py-24 bg-surface/60">
<Container> <Container>
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
@@ -86,7 +86,7 @@ export async function Sustainability() {
const content = await getContentValues(["home.sustainability.items"], locale); const content = await getContentValues(["home.sustainability.items"], locale);
const items = parseListTitled(content["home.sustainability.items"]); const items = parseListTitled(content["home.sustainability.items"]);
return ( return (
<section className="py-28 bg-cream/60"> <section className="py-28 bg-surface/60">
{/* Geteilt statt Viererreihe: die Ueberschrift laeuft mit, die Punkte {/* Geteilt statt Viererreihe: die Ueberschrift laeuft mit, die Punkte
werden als ruhige Liste gelesen. */} werden als ruhige Liste gelesen. */}
<Container className="grid gap-12 lg:grid-cols-12 lg:gap-16"> <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"> <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" /> <HexMark className="mt-2 h-3 w-3 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90" />
<div> <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> <p className="mt-2 max-w-[55ch] text-ink/70 leading-relaxed">{item.text}</p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -38,7 +38,7 @@ export function LanguageSwitcher({ className = "" }: { className?: string }) {
aria-current={code === locale} aria-current={code === locale}
title={localeLabels[code]} title={localeLabels[code]}
className={`uppercase tracking-wide transition-colors disabled:opacity-50 ${ 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} {code}
+2 -2
View File
@@ -23,14 +23,14 @@ export function LegalPage({
return ( return (
<div className="py-20"> <div className="py-20">
<Container className="max-w-3xl"> <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 ? ( {noticeText ? (
<p className="mt-2 text-xs text-honey-dark tracking-wide-caps">{noticeText}</p> <p className="mt-2 text-xs text-honey-dark tracking-wide-caps">{noticeText}</p>
) : null} ) : null}
<div className="mt-10 space-y-10"> <div className="mt-10 space-y-10">
{sections.map((section) => ( {sections.map((section) => (
<section key={section.heading}> <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"> <div className="mt-3 space-y-3 text-sm text-ink/70 leading-relaxed">
{section.paragraphs.map((p, i) => ( {section.paragraphs.map((p, i) => (
<p key={i}>{p}</p> <p key={i}>{p}</p>
+1 -1
View File
@@ -32,7 +32,7 @@ export function Logomark({
} }
const hexColor = variant === "inverted" ? "#F4EFE5" : "#B88A44"; const hexColor = variant === "inverted" ? "#F4EFE5" : "#B88A44";
const treeColor = variant === "inverted" ? "#F4EFE5" : "#24352B"; const treeColor = variant === "inverted" ? "#F4EFE5" : "var(--color-heading)";
return ( return (
<svg viewBox="0 0 100 100" className={className} fill="none" aria-hidden="true"> <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> </svg>
<motion.p <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 }} initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.45 }} transition={{ delay: 0.45 }}
@@ -115,7 +115,7 @@ export function NewsletterSignup({ turnstileSiteKey }: { turnstileSiteKey?: stri
placeholder={t.home.newsletter.emailPlaceholder} placeholder={t.home.newsletter.emailPlaceholder}
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} 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"> <Button type="submit" disabled={status === "loading"} className="flex-shrink-0 whitespace-nowrap">
{status === "loading" ? t.home.newsletter.subscribing : t.home.newsletter.subscribe} {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>
<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} {link.label}
</span> </span>
<span className="mt-3 block max-w-[40ch] text-sm leading-relaxed text-ink/65">{link.text}</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} minLength={minLength}
autoComplete={autoComplete} autoComplete={autoComplete}
onChange={showStrength ? (e) => setValue(e.target.value) : undefined} 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 <button
type="button" type="button"
onClick={() => setVisible((v) => !v)} onClick={() => setVisible((v) => !v)}
aria-label={visible ? "Passwort verbergen" : "Passwort anzeigen"} 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 />} {visible ? <EyeOffIcon /> : <EyeIcon />}
</button> </button>
+1 -1
View File
@@ -3,7 +3,7 @@ import { Logomark } from "@/components/logomark";
const SURFACES = { const SURFACES = {
forest: "bg-forest text-cream", forest: "bg-forest text-cream",
cream: "bg-cream text-forest", cream: "bg-surface text-heading",
honey: "bg-honey-pale/60 text-forest", honey: "bg-honey-pale/60 text-forest",
} as const; } as const;
+1 -1
View File
@@ -88,7 +88,7 @@ function ProcessStep({
</svg> </svg>
</motion.span> </motion.span>
<span className="font-serif text-sm text-honey-dark tabular-nums">{step.number}</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> <p className="mt-2 text-sm text-ink/70 leading-relaxed">{step.text}</p>
</li> </li>
); );
+8 -8
View File
@@ -41,7 +41,7 @@ export function ProductTeaserLink({
return ( return (
<Link <Link
href={`/honig/${slug}`} 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"> <span className="block overflow-hidden rounded-lg">
<ContentImage <ContentImage
@@ -53,8 +53,8 @@ export function ProductTeaserLink({
/> />
</span> </span>
<span className="flex-1"> <span className="flex-1">
<span className="block font-serif text-xl text-forest">{name}</span> <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-forest"> <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*$/, "")} {t.product.viewProduct.replace(/\s*→\s*$/, "")}
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -101,7 +101,7 @@ export function TasteScale({
<span <span
key={i} key={i}
className={`relative h-1.5 w-6 overflow-hidden rounded-full ${ 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 ? ( {i < value ? (
@@ -295,14 +295,14 @@ export function ProductCard({
{t.product.outOfStock} {t.product.outOfStock}
</span> </span>
) : stock <= 5 ? ( ) : 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)} {t.product.onlyLeft(stock)}
</span> </span>
) : null} ) : null}
</Link> </Link>
<div className="mt-5 flex flex-1 flex-col"> <div className="mt-5 flex flex-1 flex-col">
<Link href={`/honig/${product.slug}`} className="self-start"> <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> </Link>
<p className="text-sm text-ink/70 mt-1"> <p className="text-sm text-ink/70 mt-1">
{product.tasteTags.join(" · ")} {product.tasteTags.join(" · ")}
@@ -319,7 +319,7 @@ export function ProductCard({
</p> </p>
<div className="mt-auto flex items-baseline justify-between pt-3"> <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="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)} {formatPrice(product.priceCents, product.currency, locale)}
</span> </span>
</div> </div>
@@ -331,7 +331,7 @@ export function ProductCard({
) : null} ) : null}
<Link <Link
href={`/honig/${product.slug}`} 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"> <span className="link-draw pb-0.5">
{product.category === "HONIG" ? t.product.toHoney : t.product.toProduct} {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-label={`${isImageSlide(slide) ? `Produktbild ${index + 1}` : slide.label} – Bild ${index + 1} von ${slides.length} anzeigen`}
aria-current={index === activeIndex} aria-current={index === activeIndex}
className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-colors ${ 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) ? ( {isImageSlide(slide) ? (
+1 -1
View File
@@ -22,7 +22,7 @@ export function QuantityStepper({
size?: "sm" | "md"; size?: "sm" | "md";
}) { }) {
const buttonSize = size === "sm" ? "w-8 h-8" : "w-10 h-10"; 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 ( return (
<div className="inline-flex items-stretch border border-ink/20 bg-background"> <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 ( return (
<div className="mt-20"> <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"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
{products.map((product) => ( {products.map((product) => (
<ProductCard key={product.id} product={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> = { const STATUS_TEXT: Record<DhlTrackingStatus, string> = {
"pre-transit": "text-ink/70", "pre-transit": "text-ink/70",
transit: "text-honey-dark", transit: "text-honey-dark",
delivered: "text-forest", delivered: "text-heading",
failure: "text-red-700", failure: "text-red-700",
unknown: "text-ink/70", unknown: "text-ink/70",
}; };
@@ -50,7 +50,7 @@ export function ShipmentTrackingHistory({
if (result.kind !== "ok") { if (result.kind !== "ok") {
return ( return (
<section className="mt-5 border-t border-ink/10 pt-5"> <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"> <p className="mt-2 text-xs text-ink/65 leading-relaxed">
{result.kind === "not-found" ? labels.notYetScanned : labels.unavailable} {result.kind === "not-found" ? labels.notYetScanned : labels.unavailable}
</p> </p>
@@ -62,7 +62,7 @@ export function ShipmentTrackingHistory({
return ( return (
<section className="mt-5 border-t border-ink/10 pt-5" aria-label={labels.ariaLabel}> <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"> <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> <span className={`text-xs ${STATUS_TEXT[info.status]}`}>{labels.statuses[info.status]}</span>
</div> </div>
@@ -99,7 +99,7 @@ export function ShipmentTrackingHistory({
strokeWidth="2" strokeWidth="2"
/> />
</svg> </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"> <p className="mt-0.5 text-xs text-ink/55">
<time dateTime={event.timestamp.toISOString()}>{formatDateTime(event.timestamp, locale)}</time> <time dateTime={event.timestamp.toISOString()}>{formatDateTime(event.timestamp, locale)}</time>
{event.location ? ` · ${event.location}` : null} {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 { CartFlight, onCartLanded } from "@/components/cart-flight";
import { HexMark } from "@/components/honeycomb"; import { HexMark } from "@/components/honeycomb";
import type { Dictionary } from "@/i18n/dictionary-type"; import type { Dictionary } from "@/i18n/dictionary-type";
import { ThemeToggle } from "@/components/theme-toggle";
import type { ThemeChoice } from "@/lib/theme";
const FOCUSABLE_SELECTOR = const FOCUSABLE_SELECTOR =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; '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. */} {/* 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} /> <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="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} {companyName}
</span> </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} {t.footer.tagline}
</span> </span>
</span> </span>
@@ -166,7 +168,7 @@ function SearchButton() {
autoFocus autoFocus
autoComplete="off" autoComplete="off"
placeholder={t.nav.searchPlaceholder} 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 ? ( {visibleSuggestions.length > 0 ? (
<motion.ul <motion.ul
@@ -214,7 +216,7 @@ function SearchButton() {
type="button" type="button"
onClick={() => (open ? closeSearch() : setOpen(true))} onClick={() => (open ? closeSearch() : setOpen(true))}
aria-label={t.nav.openSearch} 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}> <AnimatePresence mode="wait" initial={false}>
<motion.svg <motion.svg
@@ -253,7 +255,7 @@ function HighlightMatch({ text, query }: { text: string; query: string }) {
return ( return (
<> <>
{text.slice(0, index)} {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)} {text.slice(index + needle.length)}
</> </>
); );
@@ -265,7 +267,7 @@ function AccountIconButton() {
<Link <Link
href="/konto" href="/konto"
aria-label={t.nav.account} 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"> <svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="8" r="4" /> <circle cx="12" cy="8" r="4" />
@@ -301,7 +303,7 @@ function CartIconButton() {
onClick={openCart} onClick={openCart}
aria-label={t.nav.openCart(count)} aria-label={t.nav.openCart(count)}
data-cart-target="" 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 <motion.svg
animate={basketControls} animate={basketControls}
@@ -326,7 +328,7 @@ function CartIconButton() {
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.4, opacity: 0 }} exit={{ scale: 0.4, opacity: 0 }}
transition={{ type: "spring", stiffness: 520, damping: 18 }} 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} {count}
</motion.span> </motion.span>
@@ -380,10 +382,12 @@ export function Header({
companyName, companyName,
logoSrc = null, logoSrc = null,
logoInvertedSrc = null, logoInvertedSrc = null,
themeChoice = "system",
}: { }: {
companyName: string; companyName: string;
logoSrc?: string | null; logoSrc?: string | null;
logoInvertedSrc?: string | null; logoInvertedSrc?: string | null;
themeChoice?: ThemeChoice;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const pathname = usePathname(); const pathname = usePathname();
@@ -430,7 +434,7 @@ export function Header({
href={item.href} href={item.href}
aria-current={active ? "page" : undefined} aria-current={active ? "page" : undefined}
className={`link-draw pb-1 text-sm whitespace-nowrap transition-colors ${ 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]} {t.nav[item.key]}
@@ -440,13 +444,16 @@ export function Header({
</nav> </nav>
<div className="flex items-center gap-2"> <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"> <LinkButton href="/shop" variant="primary" className="whitespace-nowrap">
{t.nav.discoverHoney} {t.nav.discoverHoney}
</LinkButton> </LinkButton>
</div> </div>
<div className="hidden xl:flex items-center gap-1"> <div className="hidden xl:flex items-center gap-1">
<LanguageSwitcher /> <LanguageSwitcher />
<ThemeToggle initial={themeChoice} className="text-ink/70 hover:text-heading" />
<SearchButton /> <SearchButton />
<AccountIconButton /> <AccountIconButton />
</div> </div>
@@ -455,7 +462,7 @@ export function Header({
<button <button
ref={menuButtonRef} ref={menuButtonRef}
aria-label={t.nav.openMenu} 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)} onClick={() => setMobileOpen(true)}
> >
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"> <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> </span>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<LanguageSwitcher className="!text-cream/70" /> <LanguageSwitcher className="!text-cream/70" />
<ThemeToggle initial={themeChoice} className="text-cream/70 hover:text-cream" />
<button <button
aria-label={t.nav.closeMenu} aria-label={t.nav.closeMenu}
className="w-10 h-10 flex items-center justify-center" 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" 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"> <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} {t.cart.title}
{items.length > 0 ? ( {items.length > 0 ? (
<span className="font-sans text-xs text-ink/55 tabular-nums">({cartItemCount(items)})</span> <span className="font-sans text-xs text-ink/55 tabular-nums">({cartItemCount(items)})</span>
@@ -634,7 +642,7 @@ export function CartDrawer() {
<button <button
aria-label={t.cart.close} aria-label={t.cart.close}
onClick={closeCart} 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"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M6 6l12 12M18 6L6 18" /> <path d="M6 6l12 12M18 6L6 18" />
@@ -655,7 +663,7 @@ export function CartDrawer() {
strokeDasharray="6 5" strokeDasharray="6 5"
/> />
</svg> </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"> <LinkButton href="/shop" variant="secondary" onClick={closeCart} className="mt-8">
{t.nav.discoverHoney} {t.nav.discoverHoney}
</LinkButton> </LinkButton>
@@ -676,7 +684,7 @@ export function CartDrawer() {
> >
<CartItemThumbnail image={item.image} className="w-16 h-16 flex-shrink-0 rounded-lg" /> <CartItemThumbnail image={item.image} className="w-16 h-16 flex-shrink-0 rounded-lg" />
<div className="flex-1"> <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> <p className="text-xs text-ink/65">{item.weightGrams} g</p>
{/* Live-Bestand trifft erst NACH dem ersten Rendern ein (siehe {/* Live-Bestand trifft erst NACH dem ersten Rendern ein (siehe
use-cart-stock.ts) — der Wrapper bleibt beim Uebergang von use-cart-stock.ts) — der Wrapper bleibt beim Uebergang von
@@ -726,7 +734,7 @@ export function CartDrawer() {
key={total} key={total}
initial={{ opacity: 0.4, y: -4 }} initial={{ opacity: 0.4, y: -4 }}
animate={{ opacity: 1, y: 0 }} 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)} {formatPrice(total, "EUR", locale)}
</motion.span> </motion.span>
+1 -1
View File
@@ -38,7 +38,7 @@ export function StickyAddToCartBar({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm text-ink">{product.name}</p> <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> </div>
{inStock ? ( {inStock ? (
<Button <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 ( return (
<li key={badge.label} className="flex items-center gap-2 text-xs text-ink/70"> <li key={badge.label} className="flex items-center gap-2 text-xs text-ink/70">
{badge.href ? ( {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} {content}
</Link> </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"; "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> = { 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`, 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-forest border border-forest hover:text-cream focus-visible:text-cream`, 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]", 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, // Fuer dunkle Flaechen (Hero, Waldgruen-Abschnitte, Menue): Creme-Rahmen,
// fuellt sich mit Creme, Text wechselt auf Waldgruen. // 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`, 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. // 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 = const buttonBase =
@@ -108,7 +108,7 @@ export function SectionHeading({
Ueberschrift traegt den Abschnitt allein. */} Ueberschrift traegt den Abschnitt allein. */}
<Heading <Heading
className={`text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] ${ 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} {title}
+6
View File
@@ -20,6 +20,12 @@ export const de: Dictionary = {
harvestsText: "Jede Ernte mit Datum, Standort und Charge, zurückverfolgbar bis ins Glas.", harvestsText: "Jede Ernte mit Datum, Standort und Charge, zurückverfolgbar bis ins Glas.",
originText: "Wo unsere Bienenvölker stehen und was die Landschaft um Altenmünster prägt.", originText: "Wo unsere Bienenvölker stehen und was die Landschaft um Altenmünster prägt.",
}, },
theme: {
light: "Hell",
dark: "Dunkel",
system: "Automatisch",
toggleLabel: (current, next) => `Darstellung: ${current}. Wechseln zu ${next}`,
},
nav: { nav: {
shop: "Shop", shop: "Shop",
harvests: "Unsere Ernten", harvests: "Unsere Ernten",
+6
View File
@@ -20,6 +20,12 @@ export const en: Dictionary = {
harvestsText: "Every harvest with date, location and batch, traceable down to the jar.", harvestsText: "Every harvest with date, location and batch, traceable down to the jar.",
originText: "Where our colonies live and what shapes the landscape around Altenmünster.", originText: "Where our colonies live and what shapes the landscape around Altenmünster.",
}, },
theme: {
light: "Light",
dark: "Dark",
system: "Automatic",
toggleLabel: (current, next) => `Appearance: ${current}. Switch to ${next}`,
},
nav: { nav: {
shop: "Shop", shop: "Shop",
harvests: "Our Harvests", harvests: "Our Harvests",
+6
View File
@@ -58,6 +58,12 @@ export interface Dictionary {
harvestsText: string; harvestsText: string;
originText: string; originText: string;
}; };
theme: {
light: string;
dark: string;
system: string;
toggleLabel: (current: string, next: string) => string;
};
nav: { nav: {
shop: string; shop: string;
harvests: string; harvests: string;
+43
View File
@@ -0,0 +1,43 @@
import { describe, test, expect } from "vitest";
import { readFileSync } from "node:fs";
import path from "node:path";
import { parseThemeChoice, nextThemeChoice } from "./theme";
describe("parseThemeChoice", () => {
test("uebernimmt nur light und dark", () => {
expect(parseThemeChoice("light")).toBe("light");
expect(parseThemeChoice("dark")).toBe("dark");
});
test("faellt bei fehlendem oder fremdem Wert auf system zurueck", () => {
expect(parseThemeChoice(undefined)).toBe("system");
expect(parseThemeChoice("")).toBe("system");
expect(parseThemeChoice("dark; path=/")).toBe("system");
expect(parseThemeChoice("<script>")).toBe("system");
});
});
describe("nextThemeChoice", () => {
test("wechselt reihum Automatisch -> Hell -> Dunkel -> Automatisch", () => {
expect(nextThemeChoice("system")).toBe("light");
expect(nextThemeChoice("light")).toBe("dark");
expect(nextThemeChoice("dark")).toBe("system");
});
});
describe("Dunkelmodus-Tokens in globals.css", () => {
test("gewaehlter und automatischer Dunkelblock sind identisch", () => {
const css = readFileSync(path.join(process.cwd(), "src/app/globals.css"), "utf8");
const explicit = css.match(/:root\[data-theme="dark"\] \{([\s\S]*?)\n\}/)?.[1];
const system = css.match(/:root:not\(\[data-theme="light"\]\) \{([\s\S]*?)\n \}/)?.[1];
expect(explicit).toBeTruthy();
expect(system).toBeTruthy();
const normalize = (block: string) =>
block
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
expect(normalize(system!)).toBe(normalize(explicit!));
});
});
+21
View File
@@ -0,0 +1,21 @@
/**
* Darstellung (hell/dunkel). "system" folgt der Einstellung des Geraets
* (prefers-color-scheme, siehe globals.css) und wird NICHT als Cookie
* gespeichert — nur eine ausdrueckliche Wahl landet im Cookie, damit der
* Server data-theme gleich richtig ausliefert und nichts hell aufblitzt.
*/
export type ThemeChoice = "light" | "dark" | "system";
export const THEME_COOKIE = "theme";
export const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
const CYCLE: ThemeChoice[] = ["system", "light", "dark"];
/** Cookie-Wert pruefen: alles ausser "light"/"dark" gilt als "system". */
export function parseThemeChoice(value: string | undefined): ThemeChoice {
return value === "light" || value === "dark" ? value : "system";
}
export function nextThemeChoice(current: ThemeChoice): ThemeChoice {
return CYCLE[(CYCLE.indexOf(current) + 1) % CYCLE.length];
}