fix: eliminate checkout/cart layout shift and low-contrast muted text
Live Lighthouse + performance-trace audits against the running dev app found two real, measurable defects: - /checkout and /warenkorb rendered the cart-store's default empty state before Zustand's persist middleware finished reading localStorage, so any customer who already had items in their cart saw an "empty cart" flash before the real form appeared (CLS 0.82). Fixed by tracking hasHydrated in the store and rendering the existing route skeleton until rehydration completes. Verified via Chrome DevTools performance trace: CLS 0.82 -> 0.00 on both pages. - axe-core (via Lighthouse) flagged text-ink/60 on the cream background at 4.09:1 contrast, under the 4.5:1 WCAG AA minimum for normal-size text. Bumped to text-ink/70 (5.61:1) everywhere the same token is used at text-xs/text-sm across 7 files. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
6143e5704d
commit
0402f7510d
8 files changed
+43
-12
No files matched your search
@@ -7,6 +7,7 @@ import { CartItemThumbnail } from "@/components/cart-item-thumbnail";
|
||||
import { QuantityStepper } from "@/components/quantity-stepper";
|
||||
import { ProductCard } from "@/components/product-card";
|
||||
import { useCartStore, cartTotalCents, cartItemCount } from "@/store/cart-store";
|
||||
import WarenkorbLoading from "./loading";
|
||||
import { useCartStock } from "@/hooks/use-cart-stock";
|
||||
import { formatPrice } from "@/lib/format";
|
||||
import { resolveShippingCostCents, getNextShippingTier, type ShippingTierLike } from "@/lib/shipping-calc";
|
||||
@@ -27,6 +28,7 @@ export function WarenkorbContent({
|
||||
}) {
|
||||
const { t, locale } = useI18n();
|
||||
const items = useCartStore((s) => s.items);
|
||||
const hasHydrated = useCartStore((s) => s.hasHydrated);
|
||||
const setQuantity = useCartStore((s) => s.setQuantity);
|
||||
const removeItem = useCartStore((s) => s.removeItem);
|
||||
// Live-Bestand je Position — derselbe Hinweis wie auf der Produktkarte/
|
||||
@@ -53,6 +55,12 @@ export function WarenkorbContent({
|
||||
.filter((product) => !cartProductIds.has(product.id))
|
||||
.slice(0, CROSS_SELL_LIMIT);
|
||||
|
||||
// Siehe checkout-form.tsx: ohne diese Weiche flackert kurz die
|
||||
// "leer"-Meldung auf, bevor der Cart-Store aus localStorage nachliest.
|
||||
if (!hasHydrated) {
|
||||
return <WarenkorbLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<Container>
|
||||
@@ -122,7 +130,7 @@ export function WarenkorbContent({
|
||||
<span>{shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}</span>
|
||||
</div>
|
||||
{nextTier && nextTier.unitsUntilNext <= 2 ? (
|
||||
<p className="text-xs text-ink/60">
|
||||
<p className="text-xs text-ink/70">
|
||||
{t.cartPage.nextTierHint(nextTier.unitsUntilNext, formatPrice(nextTier.priceCents, "EUR", locale))}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -134,7 +142,7 @@ export function WarenkorbContent({
|
||||
<span>-{formatPrice(bulkDiscountCents, "EUR", locale)}</span>
|
||||
</div>
|
||||
) : nextBulkDiscount && nextBulkDiscount.unitsUntilNext <= 2 ? (
|
||||
<p className="text-xs text-ink/60">
|
||||
<p className="text-xs text-ink/70">
|
||||
{t.cartPage.bulkDiscountProgress(nextBulkDiscount.unitsUntilNext, nextBulkDiscount.percent)}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -146,7 +154,7 @@ export function WarenkorbContent({
|
||||
<LinkButton href="/checkout" className="w-full mt-6">
|
||||
{t.cart.checkout}
|
||||
</LinkButton>
|
||||
<Link href="/versand-zahlung" className="mt-4 block text-center text-xs text-ink/60 hover:text-forest underline">
|
||||
<Link href="/versand-zahlung" className="mt-4 block text-center text-xs text-ink/70 hover:text-forest underline">
|
||||
{t.cartPage.pickupHint}
|
||||
</Link>
|
||||
<TrustBadges className="mt-6 pt-6 border-t border-ink/10" />
|
||||
|
||||
Reference in new issue
Block a user