Files
Wabenhain/src/components/quantity-stepper.tsx
T
Nick SzaunigandClaude Opus 5.5 064ca3ff6d
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 4m11s
feat(ui): Kopfzeile, Fusszeile und Details mit mehr Leben
- Kopfzeile: aktive Seite mit Honig-Unterstrich (auch im Handy-Menue),
  einlaufende Unterstriche beim Hover, beim Scrollen Glas und Schatten;
  weicht beim Runterscrollen aus und ist beim Hochscrollen sofort zurueck
  (steht still, solange Warenkorb oder Menue offen sind)
- Fusszeile mit Wabenraster und einlaufenden Link-Unterstrichen
- Mengensteller: gezeichnete Plus/Minus, taktiles Druecken, tickende Zahl
- Newsletter als eigene Flaeche mit Wabenraster; Erfolg zeichnet ein
  Haekchen in eine Wabe, Fehler werden als Alert angesagt
- 404: Wabe mit einer fehlenden Zelle als Bild fuer die fehlende Seite

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 08:05:01 +02:00

67 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { AnimatePresence, motion } from "motion/react";
/**
* Gemeinsamer Mengen-Steller (−/Zahl/+), bisher als Inline-Markup nur auf der
* Produktdetailseite vorhanden, waehrend Warenkorb-Drawer und Warenkorb-Seite
* noch ein natives <select> nutzten — vereinheitlicht das Bedienelement.
*
* Die Zahl "tickt" bei jeder Aenderung, die Knoepfe geben beim Druecken nach
* — die Menge fuehlt sich an wie ein Zaehlwerk statt wie ein Textfeld.
*/
export function QuantityStepper({
quantity,
onChange,
max = 10,
size = "md",
}: {
quantity: number;
onChange: (quantity: number) => void;
max?: number;
size?: "sm" | "md";
}) {
const buttonSize = size === "sm" ? "w-8 h-8" : "w-10 h-10";
const buttonClass = `${buttonSize} flex items-center justify-center text-ink/70 transition-[color,background-color,transform] duration-200 hover:bg-honey/10 hover:text-forest active:scale-90 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-ink/70 disabled:active:scale-100`;
return (
<div className="inline-flex items-stretch border border-ink/20 bg-background">
<button
type="button"
onClick={() => onChange(Math.max(1, quantity - 1))}
disabled={quantity <= 1}
aria-label="Menge verringern"
className={buttonClass}
>
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
<path d="M5 12h14" strokeLinecap="round" />
</svg>
</button>
<span className="relative w-8 overflow-hidden flex items-center justify-center text-sm tabular-nums" aria-live="polite">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={quantity}
initial={{ y: -12, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 12, opacity: 0 }}
transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}
>
{quantity}
</motion.span>
</AnimatePresence>
</span>
<button
type="button"
onClick={() => onChange(Math.min(max, quantity + 1))}
disabled={quantity >= max}
aria-label="Menge erhöhen"
className={buttonClass}
>
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
<path d="M5 12h14M12 5v14" strokeLinecap="round" />
</svg>
</button>
</div>
);
}