feat(ui): Fundament und Einkaufserlebnis visuell aufgewertet

- Fundament: feine Papierkoernung, Scrollbar/Cursor/Fokusring/Markierung in
  Markenfarben, gleich breite Ziffern fuer Preise, einlaufende Unterstriche
- Knoepfe fuellen sich beim Hover von unten wie ein Glas (Honig/Waldgruen),
  geben beim Klick nach
- Wabenmotiv aus dem Logo als Komponente (Raster, Markierung, Haekchen)
- In den Warenkorb: ein Honigtropfen fliegt im Bogen zum Korb, der Korb
  wippt, der Zaehler springt, erst dann oeffnet die Schublade; Knopf zeigt
  gezeichnetes Haekchen. Reduzierte Bewegung: Schublade oeffnet sofort.
- Produktkarten: Honigschimmer, Anheben, Serifen-Preis, gestaffeltes
  Erscheinen, Pfeil-Links
- Produktseite: mitlaufendes Bild auf Wabenraster, groesserer Titel und
  Preis, sich fuellende Geschmacksskala, Waben-Haekchen statt "✓"
- Shop-Filter als einheitliche Werkzeugleiste mit Schalter
- Warenkorb-Schublade: Dialog-Semantik, animierte Positionen, tickende
  Summe, gestalteter leerer Zustand

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-25 04:56:03 +02:00
1 parent 839bbff595
commit 3cb639c154
11 files changed
+621 -123

No files matched your search

+73 -28
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import { HexMark } from "@/components/honeycomb";
import { Container, SectionHeading } from "@/components/ui";
import { ScrollReveal } from "@/components/scroll-reveal";
import { ProductCard } from "@/components/product-card";
@@ -106,19 +107,21 @@ export default async function ShopPage(props: PageProps<"/shop">) {
<Link
key={filter.label}
href={href}
className={`text-sm tracking-wide-caps px-4 py-2 border transition-colors ${
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 ${
isActive
? "bg-forest text-cream border-forest"
: "border-ink/20 text-ink/70 hover:border-forest"
: "border-ink/15 text-ink/70 hover:border-forest hover:text-forest"
}`}
>
{isActive ? <HexMark className="h-2.5 w-2.5 text-honey-light" /> : null}
{filter.label}
</Link>
);
})}
</div>
<div className="mt-6 flex flex-wrap items-center gap-4">
<div className="mt-5 flex flex-wrap items-center gap-3 border-t border-ink/10 pt-5">
{/* GET-Formular statt Client-Component: funktioniert ohne JavaScript,
die Auswahl wird per Seiten-Neuladen als Query-Parameter uebernommen.
Die Suche ist bewusst Teil DIESES Formulars (statt nur der kleinen
@@ -131,28 +134,58 @@ export default async function ShopPage(props: PageProps<"/shop">) {
<label htmlFor="shop-search" className="sr-only">
{t.shop.searchInputLabel}
</label>
<input
id="shop-search"
type="search"
name="q"
defaultValue={query ?? ""}
placeholder={t.nav.searchPlaceholder}
className="border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
/>
<label htmlFor="sortierung">{t.shop.sortLabel}</label>
<select
id="sortierung"
name="sortierung"
defaultValue={sort ?? ""}
className="border border-ink/20 bg-background px-2 py-2"
<span className="relative">
<svg
viewBox="0 0 24 24"
className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ink/45"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
aria-hidden="true"
>
<circle cx="11" cy="11" r="6.5" />
<path d="m16 16 4.5 4.5" strokeLinecap="round" />
</svg>
<input
id="shop-search"
type="search"
name="q"
defaultValue={query ?? ""}
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"
/>
</span>
<label htmlFor="sortierung" className="ml-2 text-xs tracking-wide-caps text-ink/60">
{t.shop.sortLabel}
</label>
<span className="relative">
<select
id="sortierung"
name="sortierung"
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"
>
{sortOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<svg
viewBox="0 0 24 24"
className="pointer-events-none absolute right-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink/55"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
aria-hidden="true"
>
<path d="m6 9 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<button
type="submit"
className="h-11 bg-forest px-5 text-xs tracking-wide-caps text-cream transition-colors hover:bg-honey-dark"
>
{sortOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<button type="submit" className="border border-ink/20 px-3 py-2 hover:border-forest">
{t.shop.apply}
</button>
</form>
@@ -163,12 +196,24 @@ export default async function ShopPage(props: PageProps<"/shop">) {
sortierung: sort,
verfuegbar: onlyAvailable ? undefined : "1",
})}
className={`text-sm tracking-wide-caps px-4 py-2 border transition-colors ${
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 ${
onlyAvailable
? "bg-forest text-cream border-forest"
: "border-ink/20 text-ink/70 hover:border-forest"
: "border-ink/15 text-ink/70 hover:border-forest hover:text-forest"
}`}
>
{/* Kleiner Schalter statt reinem Textknopf — der Zustand ist auf einen Blick lesbar. */}
<span
aria-hidden="true"
className={`relative h-4 w-7 rounded-full transition-colors ${onlyAvailable ? "bg-honey" : "bg-ink/15"}`}
>
<span
className={`absolute top-0.5 h-3 w-3 rounded-full bg-cream shadow transition-[left] duration-300 ${
onlyAvailable ? "left-3.5" : "left-0.5"
}`}
/>
</span>
{t.shop.onlyAvailable}
</Link>
</div>
@@ -186,8 +231,8 @@ export default async function ShopPage(props: PageProps<"/shop">) {
) : null}
<h2 className="sr-only">{t.shop.productsHeading}</h2>
<div className="mt-14 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
{products.map((product) => (
<ProductCard key={product.id} product={product} reviewSummary={reviewSummaries.get(product.id)} />
{products.map((product, index) => (
<ProductCard key={product.id} product={product} reviewSummary={reviewSummaries.get(product.id)} index={index} />
))}
</div>
{products.length === 0 ? (