Move the food-icon backdrop onto the empty basket/orders cards and animate it
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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,17 @@
|
|||||||
@use '../../styles/variables' as *;
|
@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 {
|
.backdrop {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -11,7 +21,12 @@
|
|||||||
.icon {
|
.icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: $color-primary;
|
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 {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
import styles from './MenuBackdrop.module.scss'
|
import styles from './MenuBackdrop.module.scss'
|
||||||
|
|
||||||
const PizzaIcon = () => (
|
const PizzaIcon = () => (
|
||||||
@@ -47,20 +49,17 @@ const ICONS = [PizzaIcon, BurgerIcon, FriesIcon]
|
|||||||
|
|
||||||
// Fixed, hand-placed positions rather than random ones — random values would
|
// Fixed, hand-placed positions rather than random ones — random values would
|
||||||
// differ between the server-rendered and first client-rendered HTML and
|
// 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 = [
|
const PLACEMENTS = [
|
||||||
{ top: '6%', left: '6%', size: 72, rotate: -14 },
|
{ top: '10%', left: '8%', size: 32, rotate: -14, delay: 0 },
|
||||||
{ top: '14%', left: '86%', size: 56, rotate: 20 },
|
{ top: '14%', left: '82%', size: 26, rotate: 18, delay: 0.5 },
|
||||||
{ top: '30%', left: '46%', size: 64, rotate: -6 },
|
{ top: '40%', left: '20%', size: 24, rotate: 8, delay: 1.1 },
|
||||||
{ top: '46%', left: '10%', size: 52, rotate: 10 },
|
{ top: '55%', left: '80%', size: 30, rotate: -10, delay: 0.3 },
|
||||||
{ top: '58%', left: '90%', size: 68, rotate: -18 },
|
{ top: '78%', left: '14%', size: 26, rotate: 16, delay: 1.6 },
|
||||||
{ top: '72%', left: '30%', size: 58, rotate: 14 },
|
{ top: '82%', left: '58%', size: 22, rotate: -20, delay: 0.8 },
|
||||||
{ top: '82%', left: '68%', size: 60, rotate: -8 },
|
{ top: '22%', left: '48%', size: 20, rotate: 6, delay: 1.3 },
|
||||||
{ top: '4%', left: '40%', size: 44, rotate: 8 },
|
{ top: '62%', left: '44%', size: 24, rotate: -6, delay: 0.2 },
|
||||||
{ 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 },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const MenuBackdrop = () => (
|
export const MenuBackdrop = () => (
|
||||||
@@ -71,13 +70,16 @@ export const MenuBackdrop = () => (
|
|||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
style={{
|
style={
|
||||||
top: placement.top,
|
{
|
||||||
left: placement.left,
|
top: placement.top,
|
||||||
width: placement.size,
|
left: placement.left,
|
||||||
height: placement.size,
|
width: placement.size,
|
||||||
transform: `rotate(${placement.rotate}deg)`,
|
height: placement.size,
|
||||||
}}
|
animationDelay: `${placement.delay}s`,
|
||||||
|
'--icon-rotate': `${placement.rotate}deg`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon />
|
<Icon />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
border: 1px dashed $color-border;
|
border: 1px dashed $color-border;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
background: $color-surface;
|
background: $color-surface;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
animation: empty-state-enter 0.32s cubic-bezier(0.2, 0, 0, 1) both;
|
animation: empty-state-enter 0.32s cubic-bezier(0.2, 0, 0, 1) both;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { MenuBackdrop } from '#/components/ordering/MenuBackdrop.tsx'
|
||||||
import type { CustomerOrderSummary } from '#/domain/ordering.ts'
|
import type { CustomerOrderSummary } from '#/domain/ordering.ts'
|
||||||
import { formatPrice } from '#/lib/format.ts'
|
import { formatPrice } from '#/lib/format.ts'
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ export const OrdersListView = ({ orders }: OrdersListViewProps) => {
|
|||||||
<h1 className={styles.heading}>Your orders</h1>
|
<h1 className={styles.heading}>Your orders</h1>
|
||||||
{displayedOrders.length === 0 ? (
|
{displayedOrders.length === 0 ? (
|
||||||
<div className={styles.empty}>
|
<div className={styles.empty}>
|
||||||
|
<MenuBackdrop />
|
||||||
<div className={styles.emptyIcon}>
|
<div className={styles.emptyIcon}>
|
||||||
<OrdersIcon />
|
<OrdersIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
border: 1px dashed $color-border;
|
border: 1px dashed $color-border;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
background: $color-surface;
|
background: $color-surface;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
|
|||||||
|
|
||||||
import { BasketIcon } from '#/components/basket/BasketLink.tsx'
|
import { BasketIcon } from '#/components/basket/BasketLink.tsx'
|
||||||
import { CheckoutSection } from '#/components/ordering/CheckoutSection.tsx'
|
import { CheckoutSection } from '#/components/ordering/CheckoutSection.tsx'
|
||||||
|
import { MenuBackdrop } from '#/components/ordering/MenuBackdrop.tsx'
|
||||||
import { AnimatedNumber } from '#/components/ui/AnimatedNumber.tsx'
|
import { AnimatedNumber } from '#/components/ui/AnimatedNumber.tsx'
|
||||||
import type { MenuItemWithRecipe } from '#/domain/ordering.ts'
|
import type { MenuItemWithRecipe } from '#/domain/ordering.ts'
|
||||||
import { useBasket } from '#/lib/basket.tsx'
|
import { useBasket } from '#/lib/basket.tsx'
|
||||||
@@ -71,6 +72,7 @@ const BasketPage = () => {
|
|||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={transition}
|
transition={transition}
|
||||||
>
|
>
|
||||||
|
<MenuBackdrop />
|
||||||
<BasketIcon />
|
<BasketIcon />
|
||||||
<p className={styles.emptyTitle}>Your basket is empty</p>
|
<p className={styles.emptyTitle}>Your basket is empty</p>
|
||||||
<p className={styles.emptyText}>
|
<p className={styles.emptyText}>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
import { MenuBackdrop } from '#/components/ordering/MenuBackdrop.tsx'
|
|
||||||
import { MenuList } from '#/components/ordering/MenuList.tsx'
|
import { MenuList } from '#/components/ordering/MenuList.tsx'
|
||||||
import { useBasket } from '#/lib/basket.tsx'
|
import { useBasket } from '#/lib/basket.tsx'
|
||||||
import { getBrand } from '#/server/brand.ts'
|
import { getBrand } from '#/server/brand.ts'
|
||||||
@@ -14,7 +13,6 @@ const OrderingPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.page}>
|
<main className={styles.page}>
|
||||||
<MenuBackdrop />
|
|
||||||
<p className={styles.intro}>
|
<p className={styles.intro}>
|
||||||
Zero cooks, all machine. A robotic kitchen grills, assembles, and
|
Zero cooks, all machine. A robotic kitchen grills, assembles, and
|
||||||
plates your tasty menu in real time — watch it happen live, from
|
plates your tasty menu in real time — watch it happen live, from
|
||||||
|
|||||||
Reference in New Issue
Block a user