Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 4m11s
- 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]>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
"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>
|
||
);
|
||
}
|