From 2f37b6bba1822b1adad3e1a63f7e6f8ddb8f31a2 Mon Sep 17 00:00:00 2001 From: lukas Date: Sun, 13 Sep 2026 20:21:10 +0200 Subject: [PATCH] Move the food-icon backdrop onto the empty basket/orders cards and animate it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confine MenuBackdrop to whatever card renders it instead of the whole viewport: it's now position: absolute inset:0 within its container (previously position: fixed, spanning the page) with a gentle floating animation per icon (staggered delay, respects prefers-reduced-motion). Wired it into the empty basket and empty orders states instead of the menu list page. Also gives .empty on both pages position: relative + z-index: 0 so it forms its own stacking context — without an explicit z-index, the backdrop's z-index: -1 child was escaping to the document root and painting either fully behind everything or (on the basket page, where motion.div meant `.empty` never picked up a context on its own) above the card's own text, depending on the surrounding markup. Co-Authored-By: Claude Sonnet 5 --- .../ordering/MenuBackdrop.module.scss | 19 ++++++++- src/components/ordering/MenuBackdrop.tsx | 42 ++++++++++--------- .../orders/OrdersListView.module.scss | 3 ++ src/components/orders/OrdersListView.tsx | 2 + src/routes/basket.module.scss | 3 ++ src/routes/basket.tsx | 2 + src/routes/index.tsx | 2 - 7 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/components/ordering/MenuBackdrop.module.scss b/src/components/ordering/MenuBackdrop.module.scss index 7d9e8d5..e194e38 100644 --- a/src/components/ordering/MenuBackdrop.module.scss +++ b/src/components/ordering/MenuBackdrop.module.scss @@ -1,7 +1,17 @@ @use '../../styles/variables' as *; +@keyframes menu-backdrop-float { + 0%, + 100% { + transform: translateY(0) rotate(var(--icon-rotate, 0deg)); + } + 50% { + transform: translateY(-6px) rotate(calc(var(--icon-rotate, 0deg) + 6deg)); + } +} + .backdrop { - position: fixed; + position: absolute; inset: 0; z-index: -1; overflow: hidden; @@ -11,7 +21,12 @@ .icon { position: absolute; color: $color-primary; - opacity: 0.08; + opacity: 0.16; + transform: rotate(var(--icon-rotate, 0deg)); + + @media (prefers-reduced-motion: no-preference) { + animation: menu-backdrop-float 5s ease-in-out infinite; + } svg { width: 100%; diff --git a/src/components/ordering/MenuBackdrop.tsx b/src/components/ordering/MenuBackdrop.tsx index 538ad43..5670449 100644 --- a/src/components/ordering/MenuBackdrop.tsx +++ b/src/components/ordering/MenuBackdrop.tsx @@ -1,3 +1,5 @@ +import type { CSSProperties } from 'react' + import styles from './MenuBackdrop.module.scss' const PizzaIcon = () => ( @@ -47,20 +49,17 @@ const ICONS = [PizzaIcon, BurgerIcon, FriesIcon] // Fixed, hand-placed positions rather than random ones — random values would // differ between the server-rendered and first client-rendered HTML and -// trigger a hydration mismatch. +// trigger a hydration mismatch. Positioned as percentages of the containing +// card, not the viewport, so this only ever covers whatever box renders it. const PLACEMENTS = [ - { top: '6%', left: '6%', size: 72, rotate: -14 }, - { top: '14%', left: '86%', size: 56, rotate: 20 }, - { top: '30%', left: '46%', size: 64, rotate: -6 }, - { top: '46%', left: '10%', size: 52, rotate: 10 }, - { top: '58%', left: '90%', size: 68, rotate: -18 }, - { top: '72%', left: '30%', size: 58, rotate: 14 }, - { top: '82%', left: '68%', size: 60, rotate: -8 }, - { top: '4%', left: '40%', size: 44, rotate: 8 }, - { top: '38%', left: '78%', size: 46, rotate: -22 }, - { top: '64%', left: '52%', size: 40, rotate: 16 }, - { top: '90%', left: '12%', size: 50, rotate: -10 }, - { top: '20%', left: '20%', size: 42, rotate: 24 }, + { top: '10%', left: '8%', size: 32, rotate: -14, delay: 0 }, + { top: '14%', left: '82%', size: 26, rotate: 18, delay: 0.5 }, + { top: '40%', left: '20%', size: 24, rotate: 8, delay: 1.1 }, + { top: '55%', left: '80%', size: 30, rotate: -10, delay: 0.3 }, + { top: '78%', left: '14%', size: 26, rotate: 16, delay: 1.6 }, + { top: '82%', left: '58%', size: 22, rotate: -20, delay: 0.8 }, + { top: '22%', left: '48%', size: 20, rotate: 6, delay: 1.3 }, + { top: '62%', left: '44%', size: 24, rotate: -6, delay: 0.2 }, ] export const MenuBackdrop = () => ( @@ -71,13 +70,16 @@ export const MenuBackdrop = () => ( diff --git a/src/components/orders/OrdersListView.module.scss b/src/components/orders/OrdersListView.module.scss index 503d804..5cc21b4 100644 --- a/src/components/orders/OrdersListView.module.scss +++ b/src/components/orders/OrdersListView.module.scss @@ -22,6 +22,8 @@ } .empty { + position: relative; + z-index: 0; display: flex; flex-direction: column; align-items: center; @@ -31,6 +33,7 @@ border: 1px dashed $color-border; border-radius: $radius; background: $color-surface; + overflow: hidden; @media (prefers-reduced-motion: no-preference) { animation: empty-state-enter 0.32s cubic-bezier(0.2, 0, 0, 1) both; diff --git a/src/components/orders/OrdersListView.tsx b/src/components/orders/OrdersListView.tsx index ddb69d6..5875ef3 100644 --- a/src/components/orders/OrdersListView.tsx +++ b/src/components/orders/OrdersListView.tsx @@ -1,6 +1,7 @@ import { Link } from '@tanstack/react-router' import { useEffect, useRef, useState } from 'react' +import { MenuBackdrop } from '#/components/ordering/MenuBackdrop.tsx' import type { CustomerOrderSummary } from '#/domain/ordering.ts' import { formatPrice } from '#/lib/format.ts' @@ -64,6 +65,7 @@ export const OrdersListView = ({ orders }: OrdersListViewProps) => {

Your orders

{displayedOrders.length === 0 ? (
+
diff --git a/src/routes/basket.module.scss b/src/routes/basket.module.scss index 648a789..dd35430 100644 --- a/src/routes/basket.module.scss +++ b/src/routes/basket.module.scss @@ -20,6 +20,8 @@ } .empty { + position: relative; + z-index: 0; display: flex; flex-direction: column; align-items: center; @@ -29,6 +31,7 @@ border: 1px dashed $color-border; border-radius: $radius; background: $color-surface; + overflow: hidden; svg { width: 2.5rem; diff --git a/src/routes/basket.tsx b/src/routes/basket.tsx index d1b0203..5e18a7e 100644 --- a/src/routes/basket.tsx +++ b/src/routes/basket.tsx @@ -3,6 +3,7 @@ import { AnimatePresence, motion, useReducedMotion } from 'framer-motion' import { BasketIcon } from '#/components/basket/BasketLink.tsx' import { CheckoutSection } from '#/components/ordering/CheckoutSection.tsx' +import { MenuBackdrop } from '#/components/ordering/MenuBackdrop.tsx' import { AnimatedNumber } from '#/components/ui/AnimatedNumber.tsx' import type { MenuItemWithRecipe } from '#/domain/ordering.ts' import { useBasket } from '#/lib/basket.tsx' @@ -71,6 +72,7 @@ const BasketPage = () => { exit={{ opacity: 0 }} transition={transition} > +

Your basket is empty

diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 7e9272e..d1f4018 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,6 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { MenuBackdrop } from '#/components/ordering/MenuBackdrop.tsx' import { MenuList } from '#/components/ordering/MenuList.tsx' import { useBasket } from '#/lib/basket.tsx' import { getBrand } from '#/server/brand.ts' @@ -14,7 +13,6 @@ const OrderingPage = () => { return (

-

Zero cooks, all machine. A robotic kitchen grills, assembles, and plates your tasty menu in real time — watch it happen live, from