// src/home.jsx — Home screen.
// Novel moment: "Ask Opa" — an AI command bar with suggested prompts that compose a mini-cart across stores.
// Also: hero store tiles, quick-start picks, trending, and a bodega-aware "what's open right now" rail.
// AskResultCard — cross-store results grouped by store. Each cluster renders its
// store chip (primary gets a "Best match" badge) followed by product tiles that
// each label which store they came from.
function AskResultCard({ answer, onNav, addToCart, onAddAll, onClose }) {
if (!answer) return null;
// Normalize legacy {store, items} shape into {clusters: [...]}.
const clusters = answer.clusters || [{ store: answer.store, items: answer.items, primary: true }];
const flat = clusters.flatMap(c => c.items.map(id => ({ storeId: c.store.id, productId: id })));
const totalCount = flat.length;
const { reason } = answer;
return (
Opa picked · {totalCount} items
{clusters.length > 1 && (
· {clusters.length} stores
)}
onAddAll?.(clusters)}
style={{
height: 32, padding: "0 14px", borderRadius: 999,
background: "var(--accent)", color: "#fff",
fontWeight: 600, fontSize: 12.5,
display: "inline-flex", alignItems: "center", gap: 6,
}}>
Add all {totalCount}
{reason}
{clusters.map((cluster, ci) => {
const products = cluster.items.map(id => PRODUCT_INDEX[id]).filter(Boolean);
return (
{/* Store chip — primary gets a "Best match" badge */}
onNav({ screen: "store", storeId: cluster.store.id })}
style={{
flexShrink: 0, width: 188, scrollSnapAlign: "start",
display: "flex", flexDirection: "column", gap: 0,
background: cluster.primary ? "var(--navy-fill)" : "var(--surface-2)",
color: cluster.primary ? "#fff" : "var(--ink)",
borderRadius: 14, overflow: "hidden", textAlign: "left",
border: cluster.primary ? "1px solid var(--navy)" : "1px solid var(--line)",
position: "relative",
}}>
{cluster.primary && (
Best match
)}
{ci === 0 ? "From" : "Also from"}
{cluster.store.name}
{cluster.store.eta}
{/* Product tiles for this cluster */}
{products.map(p => (
onNav({ screen: "product", storeId: cluster.store.id, productId: p.id })}
style={{ display: "block", position: "relative" }}>
{cluster.store.name.split(" ")[0]}
{p.name}
{money(p.price)}
addToCart(cluster.store.id, p.id, 1)}
style={{ width: 30, height: 30, borderRadius: 999,
background: "var(--navy-fill)", color: "#fff",
display: "inline-flex", alignItems: "center", justifyContent: "center",
boxShadow: "var(--shadow-sm)" }}>
))}
);
})}
);
}
function AskOpa({ onNav, onAddMany, addToCart, answer, setAnswer }) {
const [q, setQ] = React.useState("");
const [state, setState] = React.useState("idle"); // idle | thinking | answer
const inputRef = React.useRef(null);
const suggestions = [
{ label: "Hangover stuff under $20", q: "i need hangover stuff under $20 from a place nearby" },
{ label: "Something for pasta night", q: "pasta night for 4, under $40, from one store" },
{ label: "Game-day snacks + a 12-pack", q: "game-day snacks plus a 12-pack of modelo" },
{ label: "Tools to hang a picture", q: "what do i need to hang 3 frames on drywall" },
];
const respond = async (query) => {
setQ(query);
setState("thinking");
// Mocked response — in a real build this would route to claude.complete with a tool that returns SKU ids.
// Simulate latency.
await new Promise(r => setTimeout(r, 900));
const storeOf = (id) => STORES.find(s => s.id === id);
let picks;
if (/hangover|tired|headache/i.test(query)) {
picks = { clusters: [
{ store: storeOf("kalymnos"), primary: true,
items: ["drk-gatorade", "deli-eggcheese", "drk-poland", "drk-ouj", "snk-peanuts", "cof-coldbrew"] },
{ store: storeOf("liu"),
items: ["hh-advil", "drk-celsius"] }
],
reason: "Advil + Gatorade + a Bacon-Egg-Cheese for the reset, then water, OJ, a cold brew, and shells to break. Liu's is the only late-night nearby that stocks Celsius." };
} else if (/pasta/i.test(query)) {
picks = { clusters: [
{ store: storeOf("kalymnos"), primary: true,
items: ["pan-pasta", "pan-olive", "pan-butter", "prd-tomato", "deli-italian", "snk-oreo"] },
{ store: storeOf("fig"),
items: ["bw-meiomi", "drk-topo"] }
],
reason: "Rao's jar, good olive oil, Kerrygold, vine tomatoes, a backup hero, and Oreos for after. Fig & Thistle pulls the Pinot because their wall is better." };
} else if (/game|super bowl|modelo|beer/i.test(query)) {
picks = { clusters: [
{ store: storeOf("kalymnos"), primary: true,
items: ["bw-modelo", "snk-doritos", "snk-flamin", "snk-peanuts", "snk-popchips", "drk-topo", "deli-custom"] },
{ store: storeOf("fig"),
items: ["bw-whiteclaw"] }
],
reason: "Modelo 12-pack, four bags that matter, sparkling water, and a build-your-own hero for halftime. White Claw plan B from Fig & Thistle." };
} else if (/hang|picture|frame|drywall|tool|hammer/i.test(query)) {
picks = { clusters: [
{ store: storeOf("arbor"), primary: true,
items: ["hw-hammer", "hw-hooks", "hw-anchors", "hw-level", "hw-pencil"] },
{ store: storeOf("kalymnos"),
items: ["hw-tape", "hh-lightbulb"] }
],
reason: "Arbor carries the hammer, picture hooks, drywall anchors, level, and pencil. Kalymnos is the corner-shop grab for painter's tape and a fresh bulb." };
} else if (/dinner/i.test(query)) {
picks = { clusters: [
{ store: storeOf("kalymnos"), primary: true,
items: ["pan-pasta", "pan-olive", "prd-tomato", "pan-butter", "bw-meiomi", "snk-oreo"] }
],
reason: "Marinara, olive oil, vine tomatoes, butter, a decent Pinot, and cookies." };
} else {
picks = { clusters: [
{ store: storeOf("kalymnos"), primary: true,
items: ["snk-popchips", "snk-oreo", "bw-meiomi", "drk-topo", "snk-peanuts", "cof-coldbrew", "drk-celsius"] }
],
reason: "Movie-night default — Kalymnos carries all of it." };
}
setAnswer(picks);
setState("answer");
};
return (
{/* decorative — stronger warm accent in dark mode */}
{/* hairline warm sheen along the top edge */}
Ask Opa · new
Tell us what you need.
We'll read the aisles.
3,500 local stores. One sentence. We'll build the cart — you just check it.
setQ(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && q.trim()) respond(q); }}
placeholder="a hangover kit · pasta night for 4 · supplies for the new puppy…"
style={{
flex: 1, height: 48,
background: "transparent", border: 0, outline: 0,
color: "#fff", fontSize: 15, fontFamily: "inherit",
}} />
q.trim() && respond(q)}
style={{ height: 44, padding: "0 18px", borderRadius: 12,
background: "var(--accent)", color: "#fff", fontWeight: 600, fontSize: 14,
display: "inline-flex", alignItems: "center", gap: 6 }}>
Ask
{suggestions.map(s => (
respond(s.q)}
style={{
padding: "7px 13px", borderRadius: 999,
background: "rgba(255,255,255,0.07)", border: "1px solid rgba(255,255,255,0.16)",
color: "#fff", fontSize: 12.5, fontWeight: 500,
}}>{s.label}
))}
{state === "thinking" && (
Reading aisles at nearby stores…
)}
);
}
function StoreCard({ store, onOpen, big }) {
return (
{ e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "var(--shadow-md)"; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = "none"; e.currentTarget.style.boxShadow = "none"; }}>
{/* Title on its own line — no inline rating, so long names don't wrap to 3 lines. */}
{store.name}
{/* Rating + tagline row */}
{store.rating && }
{store.rating && store.tagline && · }
{store.tagline && (
{store.tagline}
)}
}>{store.eta}
}>{store.distance}
{store.tags && store.tags.slice(0, 2).map(t =>
{t} )}
);
}
// Mocked order-history + saved lists, distinct visual shape from product grids.
// Two tabs in one strip: "Buy again" is timestamped item tiles; "Your usuals"
// shows multi-thumbnail list cards with a rebuild-cart CTA.
const BUY_AGAIN = [
{ id: "cof-coldbrew", storeId: "kalymnos", last: "Tue · 2 days ago" },
{ id: "deli-italian", storeId: "kalymnos", last: "Mon · 3 days ago" },
{ id: "drk-topo", storeId: "kalymnos", last: "Fri · last week" },
{ id: "snk-oreo", storeId: "kalymnos", last: "Thu · last week" },
{ id: "hh-advil", storeId: "liu", last: "Sun · 5 days ago" },
{ id: "bw-modelo", storeId: "kalymnos", last: "Sat · last week" },
{ id: "pan-pasta", storeId: "kalymnos", last: "Wed · last week" },
{ id: "drk-celsius", storeId: "liu", last: "Mon · last week" },
];
const USUALS = [
{ id: "sunday-am", label: "Sunday breakfast", subtitle: "Opa keeps this list warm.",
items: ["deli-eggcheese", "cof-latte", "drk-ouj", "snk-popchips"], storeId: "kalymnos" },
{ id: "pantry-reup", label: "Pantry re-up", subtitle: "Every other Tuesday, like clockwork.",
items: ["pan-pasta", "pan-olive", "pan-butter", "hh-tp", "hh-ptowel", "hh-tide"], storeId: "kalymnos" },
{ id: "wed-lunch", label: "Wednesday lunch", subtitle: "Italian combo, chips, topo.",
items: ["deli-italian", "snk-flamin", "drk-topo"], storeId: "kalymnos" },
];
function PantryStrip({ onNav, addToCart, addMany, isMobile }) {
const [tab, setTab] = React.useState("again");
return (
Your pantry
Pick up where you left off.
{[
{ id: "again", label: `Buy again · ${BUY_AGAIN.length}` },
{ id: "usuals", label: `Your usuals · ${USUALS.length}` },
].map(t => (
setTab(t.id)}
className="btn-reset"
style={{
padding: "8px 16px", borderRadius: 999, fontSize: 12.5, fontWeight: 600,
background: tab === t.id ? "var(--navy-fill)" : "transparent",
color: tab === t.id ? "var(--navy-fill-ink, #fff)" : "var(--ink-2)",
}}>
{t.label}
))}
{tab === "again" && (
{BUY_AGAIN.map(b => {
const p = PRODUCT_INDEX[b.id]; if (!p) return null;
const store = STORES.find(s => s.id === b.storeId);
return (
onNav({ screen: "product", storeId: b.storeId, productId: p.id })}
style={{ flexShrink: 0 }}>
{b.last}
{p.name}
{store?.name} · {money(p.price)}
addToCart(b.storeId, p.id, 1)}
style={{ flexShrink: 0, width: 34, height: 34, borderRadius: 999,
background: "var(--navy-fill)", color: "#fff",
display: "inline-flex", alignItems: "center", justifyContent: "center",
boxShadow: "var(--shadow-sm)" }}>
);
})}
)}
{tab === "usuals" && (
{USUALS.map(u => {
const items = u.items.map(id => PRODUCT_INDEX[id]).filter(Boolean);
const total = items.reduce((s, p) => s + p.price, 0);
return (
{/* Mini collage — 4 up */}
{items.slice(0, 4).map((p, i) => (
))}
{items.length} items · ~{money(total)}
addMany(u.storeId, u.items)}
style={{
height: 34, padding: "0 14px", borderRadius: 999,
background: "var(--navy-fill)", color: "#fff",
fontWeight: 600, fontSize: 12.5,
display: "inline-flex", alignItems: "center", gap: 6,
}}>
Rebuild cart
);
})}
)}
);
}
// Time-aware band — surfaces a contextual "what's open late / breakfast soon / etc."
// Currently hard-mocked to late-night for the demo so it always renders the same
// moment regardless of actual clock.
function LateNightBand({ onNav }) {
const late = STORES.filter(s => /Late-night|24hr/i.test((s.tags || []).join(" ") + " " + (s.tagline || s.hours || "")));
const list = late.length ? late : STORES.slice(0, 2);
return (
It's 11:34 PM. {" "}
Two stores near you are still open — {list.map(s => s.name).join(" and ")} stays on until close.
{list.slice(0, 2).map(s => (
onNav({ screen: "store", storeId: s.id })}
style={{
padding: "7px 13px", borderRadius: 999,
background: "var(--surface)", border: "1px solid var(--line)",
fontSize: 12.5, fontWeight: 500, color: "var(--ink)",
display: "inline-flex", alignItems: "center", gap: 6,
}}>
{s.name.split(" ")[0]}
))}
);
}
function Home({ onNav, cart, addToCart, addMany }) {
const isMobile = useMedia("(max-width: 720px)");
const isWide = useMedia("(min-width: 1200px)");
const nearby = STORES;
const hero = STORES[0];
const [answer, setAnswer] = React.useState(null);
return (
{answer &&
clusters.forEach(c => addMany(c.store.id, c.items))}
onClose={() => setAnswer(null)} />}
{/* Quick categories strip */}
{[
{ id: "deli", label: "Deli", icon: "Baguette", tint: "oklch(93% 0.06 80)" },
{ id: "coffee", label: "Coffee", icon: "Coffee", tint: "oklch(92% 0.04 60)" },
{ id: "drinks", label: "Drinks", icon: "Snow", tint: "oklch(92% 0.05 210)" },
{ id: "beer", label: "Beer & Wine", icon: "Beer", tint: "oklch(92% 0.07 100)" },
{ id: "snacks", label: "Snacks", icon: "Cookie", tint: "oklch(92% 0.06 40)" },
{ id: "produce", label: "Produce", icon: "Apple", tint: "oklch(93% 0.08 140)" },
{ id: "pantry", label: "Pantry", icon: "Box", tint: "oklch(93% 0.04 90)" },
{ id: "household", label: "Household", icon: "Pill", tint: "oklch(93% 0.03 280)" },
].slice(0, isMobile ? 8 : 8).map(c => {
const Icon = Icons[c.icon];
return (
onNav({ screen: "store", storeId: hero.id, category: c.id })}
style={{
display: "flex", flexDirection: "column", alignItems: "center", gap: 10,
padding: "16px 8px", borderRadius: "var(--radius)",
background: "var(--surface)", border: "1px solid var(--line)",
transition: "transform 0.12s",
}}
onMouseEnter={(e) => { e.currentTarget.style.transform = "translateY(-2px)"; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = "none"; }}>
{c.label}
);
})}
{/* Stores open right now — horizontal slider on both mobile and desktop */}
onNav({ screen: "store", storeId: s.id })} />
{/* Curated bundles — bodega-style */}
Editor-curated, one tap to cart.} />
{BUNDLES.map((b, idx) => {
const items = b.items.map(id => PRODUCT_INDEX[id]).filter(Boolean);
const total = items.reduce((s,p)=>s+p.price,0);
// Each bundle gets a different visual character
const themes = [
{ tint: "oklch(94% 0.06 80)", ink: "oklch(35% 0.10 60)", pat: "wave" },
{ tint: "oklch(93% 0.05 220)", ink: "oklch(35% 0.12 240)", pat: "grid" },
{ tint: "oklch(94% 0.07 130)", ink: "oklch(38% 0.10 145)", pat: "dots" },
];
const th = themes[idx % themes.length];
return (
{/* Stylized header band */}
Bundle · {items.length} items
{b.label}
{/* Body */}
{b.desc}
{/* Item thumbnails */}
{/* Item names list (dense, mono) */}
{items.map(p => (
{p.name}
{money(p.price)}
))}
{money(total)}
before delivery
}
onClick={() => addMany("kalymnos", b.items)}>Add bundle
);
})}
{/* Two CTA cards — eco / community */}
{/* Most shopped near you — horizontal slider, breathable */}
p.trending).slice(0, 12)}
isMobile={isMobile}
cart={cart}
onOpen={(p) => onNav({ screen: "product", storeId: "kalymnos", productId: p.id })}
addToCart={addToCart}
/>
{/* Worth your attention — promoted long-form/video carousel */}
{/* Warm end-of-page moment so the scroll doesn't cliff-dive into nothing. */}
);
}
function MiniProductCard({ p, onOpen, qty, onAdd, onRemove, pattern = "morph", storeName }) {
return (
{storeName && (
{storeName}
)}
{p.name}
{p.unit}
);
}
// ─── StoresOpenSlider ─────────────────────────────────────────────────────
// Horizontal-scroll rail that works on both mobile and desktop. Desktop shows
// chevron controls; mobile relies on swipe. Card width scales per viewport.
function StoresOpenSlider({ stores, isMobile, onOpen }) {
const scrollerRef = React.useRef(null);
const [canLeft, setCanLeft] = React.useState(false);
const [canRight, setCanRight] = React.useState(true);
const cardW = isMobile ? 240 : 300;
const update = React.useCallback(() => {
const el = scrollerRef.current; if (!el) return;
setCanLeft(el.scrollLeft > 4);
setCanRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 4);
}, []);
React.useEffect(() => {
const el = scrollerRef.current; if (!el) return;
update();
el.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
return () => {
el.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, [update]);
const step = (dir) => {
const el = scrollerRef.current; if (!el) return;
el.scrollBy({ left: dir * el.clientWidth * 0.8, behavior: "smooth" });
};
return (
{!isMobile && (
<>
step(-1)} disabled={!canLeft}
style={{ width: 32, height: 32, borderRadius: 99,
border: "1px solid var(--line)", background: "var(--surface)",
display: "grid", placeItems: "center",
opacity: canLeft ? 1 : 0.35, transition: "opacity 160ms" }}>
step(1)} disabled={!canRight}
style={{ width: 32, height: 32, borderRadius: 99,
border: "1px solid var(--line)", background: "var(--surface)",
display: "grid", placeItems: "center",
opacity: canRight ? 1 : 0.35, transition: "opacity 160ms" }}>
>
)}
}>See all 412
} />
{stores.map(s => (
onOpen(s)} />
))}
);
}
// ─── HomeFarewell ─────────────────────────────────────────────────────────
// Warm end-of-page moment so the home screen doesn't cliff-dive into nothing.
// Personality more than CTA — but it does surface a one-tap Ask Opa shortcut.
function HomeFarewell({ onNav, isMobile }) {
const hour = new Date().getHours();
const when = hour < 5 ? "late tonight"
: hour < 11 ? "this morning"
: hour < 15 ? "this afternoon"
: hour < 19 ? "tonight"
: "tonight";
return (
That's everything {when}
Still not sure?{" "}
Ask Opa — it's the whole block in one sentence.
3,500 local stores. One question. We'll read the aisles for you and build the cart.
{ window.scrollTo({ top: 0, behavior: "smooth" }); }}
style={{ height: 44, padding: "0 18px", borderRadius: 999,
background: "var(--accent)", color: "#fff", fontSize: 14, fontWeight: 600,
display: "inline-flex", alignItems: "center", gap: 8 }}>
Ask Opa
onNav({ screen: "search" })}
style={{ height: 44, padding: "0 18px", borderRadius: 999,
background: "rgba(255,255,255,0.08)",
border: "1px solid rgba(255,255,255,0.18)",
color: "#fff", fontSize: 14, fontWeight: 500,
display: "inline-flex", alignItems: "center", gap: 8 }}>
Search stores
);
}
Object.assign(window, { Home, MiniProductCard, StoresOpenSlider, HomeFarewell });
// ─── MostShoppedSlider ─────────────────────────────────────────────────────
// Horizontal scroll carousel. Desktop shows ~4.5 cards visible → inviting scroll.
// Mobile shows ~2.4 cards. Cards are larger + more breathable than the old grid.
function MostShoppedSlider({ products, isMobile, cart, onOpen, addToCart }) {
const scrollerRef = React.useRef(null);
const [canScrollLeft, setCanScrollLeft] = React.useState(false);
const [canScrollRight, setCanScrollRight] = React.useState(true);
const updateArrows = React.useCallback(() => {
const el = scrollerRef.current;
if (!el) return;
setCanScrollLeft(el.scrollLeft > 4);
setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 4);
}, []);
React.useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
updateArrows();
el.addEventListener("scroll", updateArrows, { passive: true });
window.addEventListener("resize", updateArrows);
return () => {
el.removeEventListener("scroll", updateArrows);
window.removeEventListener("resize", updateArrows);
};
}, [updateArrows]);
const scrollBy = (dir) => {
const el = scrollerRef.current;
if (!el) return;
el.scrollBy({ left: dir * (el.clientWidth * 0.8), behavior: "smooth" });
};
const cardWidth = isMobile ? 168 : 232;
return (
{!isMobile && (
<>
scrollBy(-1)} disabled={!canScrollLeft}
style={{ width: 32, height: 32, borderRadius: 99,
border: "1px solid var(--line)", background: "var(--surface)",
display: "grid", placeItems: "center",
opacity: canScrollLeft ? 1 : 0.35,
cursor: canScrollLeft ? "pointer" : "default",
transition: "opacity 160ms" }}>
scrollBy(1)} disabled={!canScrollRight}
style={{ width: 32, height: 32, borderRadius: 99,
border: "1px solid var(--line)", background: "var(--surface)",
display: "grid", placeItems: "center",
opacity: canScrollRight ? 1 : 0.35,
cursor: canScrollRight ? "pointer" : "default",
transition: "opacity 160ms" }}>
>
)}
}>More
} />
{products.map(p => (
onOpen(p)}
qty={cart.items.find(i => i.productId === p.id)?.qty || 0}
onAdd={() => addToCart("kalymnos", p.id, 1)}
onRemove={() => addToCart("kalymnos", p.id, -1)} />
))}
);
}
Object.assign(window, { MostShoppedSlider });
// ─── PickupBanner ──────────────────────────────────────────────────────────
// Persistent strip on home when there's a pickup waiting. Real action.
function PickupBanner({ onNav }) {
const [dismissed, setDismissed] = React.useState(false);
const isMobile = useMedia("(max-width: 720px)");
if (dismissed) return null;
return (
{/* Subtle diagonal accent stripe */}
Pickup ready at Kalymnos
#4821
·
3 items
·
held until 8:30 PM
Show your name at the counter — Maria knows you're on the way.
onNav({ screen: "store", storeId: "kalymnos" })}
style={{ flexShrink: 0, position: "relative", zIndex: 1 }}>
{isMobile ? "Go" : "Get directions"}
setDismissed(true)}
title="Dismiss"
style={{ width: 28, height: 28, display: "grid", placeItems: "center",
color: "var(--ink-4)", flexShrink: 0, position: "relative", zIndex: 1 }}>
);
}
// ─── CtaCard ───────────────────────────────────────────────────────────────
// Full-bleed image-less promo card. Tone drives palette; stat anchors a number.
function CtaCard({ tone = "green", eyebrow, title, body, stat, ctaLabel, icon = "Leaf" }) {
const palettes = {
green: {
bg: "var(--ok-tint)",
border: "oklch(from var(--ok) l c h / 0.22)",
ink: "var(--ink)",
eyebrow: "var(--ok)",
iconBg: "var(--surface)",
iconColor: "var(--ok)",
btn: "primary",
pattern: "var(--ok)",
},
navy: {
bg: "var(--navy-fill)",
border: "transparent",
ink: "#fff",
eyebrow: "oklch(from #fff l c h / 0.7)",
iconBg: "rgba(255,255,255,0.10)",
iconColor: "#fff",
btn: "inverse",
pattern: "rgba(255,255,255,0.6)",
},
}[tone];
const Icon = Icons[icon] || Icons.Leaf;
return (
{/* decorative arc */}
{eyebrow}
);
}
// ─── WorthYourAttention ────────────────────────────────────────────────────
// Promoted stories / video carousel. Every card has the "Promoted" tag.
function WorthYourAttention({ isMobile, onNav }) {
const items = [
{ id: "verde", brand: "Verde Corner Produce", kind: "Video · 0:42",
hed: "Watch how Hudson Valley apples get from orchard to your bag in 14 hours.",
kicker: "This week's harvest", img: { url: "https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?w=800&auto=format" }, color: "oklch(48% 0.12 140)" },
{ id: "fig", brand: "Fig & Thistle Wine", kind: "Story · 3 min read",
hed: "Three under-$22 reds the staff is fighting over right now.",
kicker: "Buyer's notes · Nov", img: { url: "https://images.unsplash.com/photo-1547595628-c61a29f496f0?w=800&auto=format" }, color: "oklch(35% 0.09 20)" },
{ id: "kalymnos", brand: "Kalymnos Deli", kind: "Recipe",
hed: "Maria's hangover egg-cheese: 4 ingredients, 6 minutes, $9 cart.",
kicker: "From the deli counter", img: { url: "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=800&auto=format" }, color: "oklch(45% 0.10 60)" },
{ id: "elmo", brand: "Elmo's Chicken & Fixins", kind: "Video · 1:08",
hed: "We brined 200 thighs this morning. Here's why Tuesday is the night.",
kicker: "Behind the counter", img: { url: "https://images.unsplash.com/photo-1532550907401-a500c9a57435?w=800&auto=format" }, color: "oklch(40% 0.13 30)" },
];
return (
}>All stories} />
{items.map(it => (
onNav({ screen: "store", storeId: it.id })}
style={{
textAlign: "left", display: "flex", flexDirection: "column", gap: 0,
borderRadius: "var(--radius-lg)", overflow: "hidden",
background: "var(--surface)", border: "1px solid var(--line)",
scrollSnapAlign: "start",
transition: "transform .15s, box-shadow .15s",
}}
onMouseEnter={(e) => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "var(--shadow-md)"; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = "none"; e.currentTarget.style.boxShadow = "none"; }}>
{ e.currentTarget.style.display = "none"; }}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />
{/* Promoted tag */}
Promoted
{/* Kind chip */}
{it.kind.startsWith("Video") && (
)}
{/* Bottom text overlay */}
{it.kicker} · {it.kind}
{it.hed}
))}
);
}
Object.assign(window, { PickupBanner, CtaCard, WorthYourAttention });
// ─── BundleBackdrop ────────────────────────────────────────────────────────
// Decorative pattern fills for bundle headers — wave / grid / dots.
function BundleBackdrop({ pattern, color }) {
const stroke = `oklch(from ${color} l c h / 0.18)`;
if (pattern === "wave") {
return (
{[...Array(7)].map((_, i) => (
))}
);
}
if (pattern === "grid") {
return (
);
}
// dots
return (
{[...Array(7)].map((_, row) =>
[...Array(17)].map((__, col) => (
))
)}
);
}
window.BundleBackdrop = BundleBackdrop;