+
+
+
₹48,200
+
+ +32% vs last week
-
+
{[34, 46, 40, 58, 52, 70, 88].map((h, i) => (
))}
- >
+
+
),
};
+
+
diff --git a/src/components/home/voice/FloatingRequestCards.tsx b/src/components/home/voice/FloatingRequestCards.tsx
index c682b91..7b751ba 100644
--- a/src/components/home/voice/FloatingRequestCards.tsx
+++ b/src/components/home/voice/FloatingRequestCards.tsx
@@ -5,45 +5,50 @@ import { motion, useReducedMotion } from "framer-motion";
// The kinds of things people actually say to Nearle — products *and* local
// services.
//
-// Positions are percentages of the phone box, and every card is placed so it
-// never covers the live screen — the demo always stays readable:
-// · xl+ : three cards drift in the wide gutter left of the phone, two sit
-// clear above and below it.
-// · below xl there is no gutter, so the visible cards sit above and below
-// the device instead of beside it.
+// Placement is anchored off the phone box's own edges rather than guessed
+// percentages: `right-full` puts a card's right edge exactly on the phone's
+// left edge, `left-full` the mirror. Plus a margin, that means a card can never
+// cover the live screen at any width — the demo always stays readable.
+// · lg+ : three cards drift in the left gutter at staggered heights, two in
+// the right gutter between them.
+// · below lg there is no gutter, so the two shortest cards sit clear above
+// and below the device instead of beside it.
+//
+// The longer labels are kept on the left, where the grid gap and the text
+// column's own slack leave more room to bleed into than the page margin does.
const CARDS = [
{
emoji: "🥛",
text: "Order two litres of milk",
- pos: "left-[-4%] top-[-7%] xl:left-[-50%] xl:top-[12%]",
- drift: { y: [-9, 9, -9], x: [0, 5, 0] },
+ pos: "left-[-4%] top-[-8%] lg:left-auto lg:right-full lg:top-[7%] lg:mr-4",
+ drift: { y: [-9, 9, -9], x: [0, 5, 0], rotate: [-1, 1, -1] },
duration: 15,
delay: 0,
always: true,
},
{
emoji: "💊",
- text: "Order medicine for headache",
- pos: "right-[-4%] bottom-[-6%] xl:right-[-6%] xl:bottom-[-7%]",
- drift: { y: [-10, 7, -10], x: [0, -5, 0] },
+ text: "Order medicine",
+ pos: "right-[-4%] bottom-[-7%] lg:bottom-auto lg:right-auto lg:left-full lg:top-[62%] lg:ml-4",
+ drift: { y: [-10, 7, -10], x: [0, -5, 0], rotate: [1, -1, 1] },
duration: 16,
delay: 2.1,
always: true,
},
{
- emoji: "🍅",
- text: "Need ingredients for biryani",
- pos: "right-[-6%] top-[-9%]",
- drift: { y: [8, -8, 8], x: [0, -6, 0] },
+ emoji: "🔧",
+ text: "Book an electrician",
+ pos: "left-full top-[17%] ml-4",
+ drift: { y: [8, -8, 8], x: [0, -6, 0], rotate: [-1.2, 0.8, -1.2] },
duration: 18,
delay: 1.4,
always: false,
},
{
- emoji: "🔧",
- text: "Book an electrician tomorrow",
- pos: "left-[-56%] top-[45%]",
- drift: { y: [7, -10, 7], x: [0, 6, 0] },
+ emoji: "🍅",
+ text: "Need ingredients for biryani",
+ pos: "right-full top-[42%] mr-4",
+ drift: { y: [7, -10, 7], x: [0, 6, 0], rotate: [0.9, -1.1, 0.9] },
duration: 20,
delay: 0.7,
always: false,
@@ -51,8 +56,8 @@ const CARDS = [
{
emoji: "🥣",
text: "Need breakfast for four",
- pos: "left-[-52%] top-[77%]",
- drift: { y: [9, -7, 9], x: [0, 4, 0] },
+ pos: "right-full top-[77%] mr-4",
+ drift: { y: [9, -7, 9], x: [0, 4, 0], rotate: [-0.8, 1.2, -0.8] },
duration: 19,
delay: 1,
always: false,
@@ -69,7 +74,7 @@ export function FloatingRequestCards() {
{CARDS.map((card, i) => (
(null);
const screenRef = useRef(null);
@@ -57,72 +59,91 @@ export function PhoneStage({ className = "" }: { className?: string }) {
animates box-shadow (which repaints every frame). */}
{/* Float — vertical drift */}
- {/* Sway — slow rotateY on a deliberately different period, so the two
- never sync into one obvious loop */}
+ {/* Pitch — a very slow rotateX on a third, co-prime period (13s vs the
+ float's 7s and the sway's 11s), so the three never resolve into one
+ visible loop. */}
- {/* Layer 0 — the provided iPhone SVG, verbatim and permanent */}
-
-
- {/* Layer 1 — the live app, clipped exactly to the screen rect */}
-
- {scale > 0 && (
-
-
-
- )}
-
+ {/* Layer 0 — the provided iPhone SVG, verbatim and permanent */}
+
- {/* Layer 2 — notch + status bar, extracted from the same SVG, so the
- app sits *under* the notch exactly like real iOS */}
-
+ {/* Layer 1 — the live app, clipped exactly to the screen rect */}
+
+ {scale > 0 && (
+
+
+
+ )}
+
+
+ {/* Layer 2 — notch + status bar, extracted from the same SVG, so the
+ app sits *under* the notch exactly like real iOS */}
+
+
+ {/* Layer 3 — Real-Time Live Clock Overlay */}
+
+ {liveTime || "9:41"}
+
+
diff --git a/src/components/home/voice/VoiceScreen.tsx b/src/components/home/voice/VoiceScreen.tsx
index 24a8a90..1e0101a 100644
--- a/src/components/home/voice/VoiceScreen.tsx
+++ b/src/components/home/voice/VoiceScreen.tsx
@@ -58,6 +58,7 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
const showRequest =
stage !== "launch" && !onHomeScreen && stage !== "listening" && stage !== "speaking";
const fulfilmentSteps = isBooking ? SERVICE_FULFILMENT_STEPS : FULFILMENT_STEPS;
+ const itemsSubtotal = request.items.reduce((sum, item) => sum + item.price, 0);
// On the final beat every fulfilment step is done — nothing left pulsing.
const fulfilDone = stage === "delivered" ? fulfilmentSteps.length : fulfilStep;
@@ -228,6 +229,20 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {